Appendix: Future Usage Tracking
The next phase can include:
- Full event tracking (logins + features) •
- Custom analytics queries
- HubSpot sync of feature usage
- HubSpot dashboards for real adoption
- Extendable architecture for any future event
- Zero ongoing cost (self-hosted analytics pipeline)
We will track “Feature Usage Events”, meaning anytime the user performs a meaningful action inside the app. Examples:
- “runtimes_update”
- “maintenance_log_create”
- “report_generate”
- “crew_update”
- “settings_change”
RECOMMENDED STRUCTURE:
- feature_group: "vessel"
- feature_action: "create"
- feature_name: "vessel_create"
We encode the full event in one string: event: “vessel_create”
metadata: {
vesselId: 123,
screen: “VesselEditor”,
source: “web”
}
FEATURE TRACKING UTILITY
/lib/analytics/featureTracker.js
import { EventLogger } from “./eventLogger”;
export const FeatureTracker = {
async track(userId, companyId, featureName, metadata = {}) {
return EventLogger.track({
userId,
companyId,
event: `feature_${featureName}`,
metadata
});
}
};
Usage: FeatureTracker.track(user.id, user.companyId, “vessel_create”, { vesselId });
FRONTEND / APP INTEGRATION
When a vessel is created example
Inside the create vessel handler:
import { FeatureTracker } from “@/lib/analytics/featureTracker”;
await FeatureTracker.track(
user.id,
user.companyId,
“vessel_create”,
{ method: “manual” }
);
NEW ANALYTIC QUERIES
We would then add an API to measure feature usage at company level. /lib/analytics/featureQueries.js
import db from “../db”;
export async function getFeatureUsage(companyId) {
const { rows } = await db.query(
`SELECT event, COUNT(*) AS count
FROM analytics_events
WHERE company_id = $1
AND event LIKE ‘feature_%’
GROUP BY event
ORDER BY count DESC`,
[companyId]
);
return rows;
}
export async function getFeatureUsageLast30(companyId) {
const { rows } = await db.query(
`SELECT event, COUNT(*) AS count
FROM analytics_events
WHERE company_id = $1
AND event LIKE ‘feature_%’
AND timestamp > NOW() – INTERVAL ’30 days’
GROUP BY event
ORDER BY count DESC`,
[companyId]
);
return rows;
}
HUBSPOT SYNC OF FEATURE METRICS
We won’t store every feature in HubSpot — that would be too many properties. Instead, we send a compressed view into a single property. Something like:
HubSpot Property: vv_top_features_last_30d (type: text)
Which would contain something like:
- vessel_create: 42
- engine_add: 18
- maintenance_log_create: 14
- settings_update: 3
Added to the nightly sync:
import { getFeatureUsageLast30 } from “@/lib/analytics/featureQueries”;
const features = await getFeatureUsageLast30(companyId);
const formatted = features
- map(f =>`${f.event.replace(‘feature_’, ”)}: ${f.count}`)
- join(“\n”);
await updateHubSpotCompany(hubspotId, {
vv_top_features_last_30d: formatted
});
HEALTH SCORE INCLUDING FEATURES
Feature Adoption (Core Features + Weighted Importance)
So now the formula includes:
Category
- Core Feature Usage
- Breadth of Features
- Frequency of Use
- Recency of Feature Use
- Critical Feature Completion
Why it matters
- Predicts long-term stickiness
- Indicates maturity & onboarding depth
- Shows engagement trends
- Measures freshness
- ContactMust-have actions clients should do
We would define Core Features
- Steph to help define these in due course
Tier 1 (Critical)
(40% weight total)
- • vessel_create
- • engine_add
- • maintenance_log_create
- • crew_update
- • settings_update
Tier 2 (Important)
(20% weight total)
- • vessel_update
- • engine_update
- • fuel_tank_update
- • water_tank_update
- • sewage_system_update
Tier 3 (Secondary)
(10% weight total)
- • screen_open_*
- • report_generate
- • photo_upload
- • file_upload
- -
Weighted Health Score Formula: Final health score = 0–100.
health_score =
- 0.40 * core_feature_score +
- 0.20 * important_feature_score +
- 0.10 * secondary_feature_score +
- 0.15 * login_activity_score +
- 0.10 * recency_score
- 0.05 * vessel_count_score
How Each Score Is Calculated
Core Feature Adoption Score (0–100)
- Based on usage in the last 30 days:
- core_usage = number of core feature events in last 30 days
- expected_core_usage = 10 per month
- core_feature_score = min(core_usage / expected_core_usage, 1) * 100
- Important Feature Score (0–100)
- important_usage = count(important features last 30 days)
- expected_important_usage = 5 per month
- important_feature_score = min(important_usage / expected_important_usage, 1) * 100
- Secondary Feature Score (0–100)
- secondary_usage = count(secondary features last 30 days)
- expected_secondary_usage = 8 per month
- secondary_feature_score = min(secondary_usage / expected_secondary_usage, 1) * 100
- Login Activity Score (0–100). Same as v1 but normalized:
- login_activity_score = min(logins_30_days / 30, 1) * 100
- Recency Score (0–100) – days_since_last_login:
- if < 7 days → 100
- if 7–14 days → 70
- if 14–30 days → 40
- if > 30 days → 0
- Vessel Count Score (0–100)
- vessel_count_score = min(vessel_count / 10, 1) * 100
- Combined Score. The final score is:
- 0.40 * core_feature_score +
- 0.20 * important_feature_score +
- 0.10 * secondary_feature_score +
- 0.15 * login_activity_score +
- 0.10 * recency_score +
- 0.05 * vessel_count_score
- Implementation Code: /lib/analytics/healthScoreV2.js
export function computeHealthScoreV2({
coreUsage,
importantUsage,
secondaryUsage,
logins30,
lastLogin,
vesselCount
}) {
// Normalize feature usage
const coreFeatureScore = Math.min(coreUsage / 10, 1) * 100;
const importantFeatureScore = Math.min(importantUsage / 5, 1) * 100;
const secondaryFeatureScore = Math.min(secondaryUsage / 8, 1) * 100;
// Login score
const loginScore = Math.min(logins30 / 30, 1) * 100;
// Recency score
const now = Date.now();
const daysSinceLast = lastLogin
? (now – new Date(lastLogin).getTime()) / 86400000
: 999;
let recencyScore = 0;
if (daysSinceLast < 7) recencyScore = 100;
else if (daysSinceLast < 14) recencyScore = 70;
else if (daysSinceLast < 30) recencyScore = 40;
// Vessel count score
const vesselScore = Math.min(vesselCount / 10, 1) * 100;
// Weighted score
const finalScore =
coreFeatureScore * 0.40 +
importantFeatureScore * 0.20 +
secondaryFeatureScore * 0.10 +
loginScore * 0.15 +
recencyScore * 0.10 +
vesselScore * 0.05;
return Math.round(finalScore);
}
Could also add Churn Prediction in future
This version goes beyond “health score rules” into actual predictive modeling — detecting accounts that are likely to churn before they churn, based on historical behavior patterns. We would not need a full ML infrastructure but we would need to run this with:
- A small Python script (cron job)
- A lightweight model file saved locally (pickle)
- PostgreSQL / existing analytics DB
- HubSpot as the visualization layer
The goal would be to build a machine-learning model that outputs churn_risk_score which would be a value between 0–100 representing probability of churn in the next 30–90 days. We could then sync this directly into HubSpot Companies so CS + Sales could see:
- High-risk accounts
- Declining feature usage
- At-risk companies needing outreach
- Renewal timing actions
Machine Learning Approach
Either Logistic Regression (FAST + stable + interpretable) or Random Forest (more accurate but slightly heavier)
Both can run on a cron job easily. The ML model is trained on historical user & account activity, predicting:
Did this account churn? (yes/no) We use your historical data on:
- Accounts that canceled
- Accounts that stopped using the product
- Accounts that downgraded
- Accounts that stopped paying
- Support tickets (optional)
Required features for the Model that could produce extremely strong churn prediction signals:
Usage Behavior (all from our v2 system)
- logins_30_days
- monthly_active_users
- days_since_last_login
- number_vessels
- core_feature_usage_last_30d
- important_feature_usage_last_30d
- secondary_feature_usage_last_30d
Velocity / Trend Features
Comparing to previous 30-day periods:
- % change in logins
- % change in core feature usage
- % change in total activity
- number of days active this month vs last month
Engagement Breadth
- number of unique features used in last 30 days
- number of new features adopted
Account Metadata
- plan type (free/pro/enterprise)
- account age (in months)
- number of users in the account
- industry type (optional)
Support Signals (optional)
- number of open tickets
- days since last support interaction
DATA MODEL: TRAINING SET
We create a dataset using historical accounts.
- Each row is 1 company
Columns are all features above
- Label = 1 if churned, 0 if retained
Churn definition:
- No usage for 60+ days
- OR account canceled
- OR account downgraded
- OR overdue invoices > 60 days
THE TRAINING PIPELINE (PYTHON)
/ml/train_churn_model.py
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import pickle
# Load data from DB export
df = pd.read_csv(“training_data.csv”)
# Separate inputs + label
X = df.drop(columns=[“churn_flag”])
y = df[“churn_flag”]
# Train/validation split
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train model
model = RandomForestClassifier(
n_estimators=200,
max_depth=12,
class_weight=”balanced”
)
model.fit(X_train, y_train)
# Save the model
with open(“churn_model.pkl”, “wb”) as f:
pickle.dump(model, f)
Daily Churn Prediction Pipeline
Cron job loads:
- Each active company’s metrics
- The ML model (churn_model.pkl)
Predicts churn risk:
- prob = model.predict_proba(company_feature_vector)[0][1]
- churn_risk_score = int(prob * 100)
Syncs to HubSpot – HubSpot Property: vv_churn_risk_score (0–100)
Also helpful: vv_churn_risk_label = “High” / “Medium” / “Low”
Rules:
- ≥ 70 → HIGH RISK
- 40–69 → MEDIUM
- < 40 → LOW
Implementation Code (Sample Prediction Script)
/ml/run_churn_prediction.py
import pickle
import pandas as pd
from hubspot_sync import update_hubspot_company
from queries import fetch_company_features
# Load ML model
model = pickle.load(open(“churn_model.pkl”, “rb”))
companies = fetch_company_features() # returns rows of features
for company in companies:
company_id = company[“company_id”]
hubspot_id = company[“hubspot_id”]
# Convert to model input
feature_vector = pd.DataFrame([company])
# Predict churn probability
prob = model.predict_proba(feature_vector)[0][1]
churn_score = int(prob * 100)
# Sync to HubSpot
update_hubspot_company(hubspot_id, {
“vv_churn_risk_score”: churn_score,
“vv_churn_risk_label”(
“High” if churn_score >= 70
else “Medium” if churn_score >= 40
else “Low”
)
})
print(f”Updated HubSpot churn score for {company_id}: {churn_score}”)
TEAM ACTIONS TRIGGERED AUTOMATICALLY
Using HubSpot workflows:
HIGH RISK (≥70)
- Notification immediately
- Create “Churn Prevention Task”
- Trigger automated email “We noticed you haven’t logged in recently…”
- Show them onboarding videos / feature tips
MEDIUM RISK (40–69)
- Add to “monitor” list
- Suggest check-in call
- Analyze decline in feature usage
LOW RISK (<40)
- Healthy; no action
Could also add Adoption Score in future
HubSpot Property: vv_feature_adoption_score (0–100)
Algorithm example:
- mostCommonFeatures = ['logentry_create', 'task_add', 'maintenance_log_create']
- score = (clientUsageOfThese / globalUsageOfThese) * 100
This shows if a client is adopting core features.
Could also add Real-Time Feature Alerts
We can use HubSpot Workflows to notify Customer Service if:
- “Client hasn’t created a vessel in 60 days”
- “Client has never opened Engines page”
- “Client actively using feature engine_add in last 7 days”