How we track what our users actually do in the product. The quick-start block below explains the three thresholds every reader needs; the collapsible sections cover the full pipeline (ingestion, aliases, URL parsing, KPIs) when you need to debug or extend.
Everything else in this page is just plumbing around these three numbers.
Minimum active time for a feature to count as "used" in a day. Below 60s = ignored (noise filter).
Legacy threshold behind the Activation Rate KPI and the Product Adoption "≥1h share" column. Not validated against churn — correlation analysis shows the real predictors are core-CRM activation (≥60s binary) and the 30d health slope. Slated for calibration by the Health Lab.
Rolling window used by Depth / adoption tables / health scoring.
Open a section to see ingestion plumbing, alias tables, URL parser rules, and KPI definitions.
How a browser event becomes a row in product_adoption_daily.
The deployed GoHighLevel agency custom JS sends usage events to /api/usage/batch with location, feature_key, day, sec and url.
POST /api/usage/batch calls Supabase RPC gocroco_usage_ingest_batch and stores usage into feature_daily, feature_monthly, user_feature_lifetime and events. Custom links are reparsed from URL so feature_key becomes the custom feature id.
normalize_feature_key(raw) maps aliases from feature_catalog to canonical keys used for adoption metrics.
product_adoption_daily drives charts/tables. Unknown keys are grouped into Other and exposed in the Other breakdown card.
Production endpoint: https://gocroco.vercel.app/api/usage/batch
| Field | Required | Notes |
|---|---|---|
| Yes | User email in GHL context. | |
| location_id | Yes | Primary account scope for aggregation. |
| feature_key | Yes | Raw feature key before SQL normalization. |
| day | Optional | ISO date bucket, defaults to Europe/Paris current day. |
| sec | Yes | Tracked active duration in seconds. |
| url | Optional | Used for events/user_last_seen diagnostics and geo context. |
23 canonical keys from lib/features.ts — imported live.
| # | Label | Feature key | Notes |
|---|---|---|---|
| 1 | Dashboard | dashboard | Tracked in current feature list. |
| 2 | Conversations | conversations | Tracked in current feature list. |
| 3 | Calendars | calendars | Tracked in current feature list. |
| 4 | Contacts | contacts | Tracked in current feature list. |
| 5 | Opportunities | opportunities | Tracked in current feature list. |
| 6 | Payments | payments | Tracked in current feature list. |
| 7 | Marketing | marketing | Tracked in current feature list. |
| 8 | Canonical key. Raw `emails` is aliased to `email`. | ||
| 9 | Automation | automation | Tracked in current feature list. |
| 10 | Reputation | reputation | Tracked in current feature list. |
| 11 | Blogs | blogs | Tracked in current feature list. |
| 12 | Memberships | memberships | Tracked in current feature list. |
URL parser aliases + aggregation aliases (lib/featureAggregation.ts) + Supabase feature catalog aliases.
| Raw | Canonical | Source |
|---|---|---|
| emails | Aggregation alias (lib/featureAggregation.ts) | |
| qr-codes | marketing | URL parser + aggregation alias + DB alias |
| qr-code | marketing | Aggregation alias (lib/featureAggregation.ts) |
| page-builder | funnels-websites | URL parser + aggregation alias + DB alias |
| quiz-builder | survey-builder | URL parser + aggregation alias + DB alias |
| quiz-builder-v2 | survey-builder | URL parser + aggregation alias + DB alias |
| survey-builder-v2 | survey-builder | Aggregation alias (lib/featureAggregation.ts) |
| form-builder-v2 | form-builder | URL parser + aggregation alias + DB alias |
| workflow | automation | Aggregation alias + DB alias |
| analytics | dashboard | URL parser + aggregation alias + DB alias |
How URLs are mapped to a feature_key. Same indexOf('location') logic matches /v2/ and legacy paths.
| URL pattern | feature_key | Notes |
|---|---|---|
| /v2/location/{id}/marketing/emails/... | Special rule in parseFeatureFromUrl(). | |
| /location/{id}/emails/... | Same indexOf('location') logic, matches /v2/ and legacy paths. | |
| /v2/location/{id}/custom-menu-link/{custom_feature_id} | {custom_feature_id} | feature_raw keeps custom-menu-link. |
| /v2/location/{id}/custom-page-link/{custom_feature_id} | {custom_feature_id} | feature_raw keeps custom-page-link. |
| Unknown feature segment | other | Fallback in parseFeatureFromUrl(). |
The metrics powering the /product-adoption dashboard.
| KPI | Definition | Where to see it | Source |
|---|---|---|---|
| Activation Rate (legacy) | New users reaching ≥ 2 features OR ≥ 3600s of total usage within 7 days of first event. The 1h threshold is uncalibrated — prefer the Health Score v3 Activation pillar for churn-predictive signal. | /product-adoption KPI grid | lib/analytics/product/queries/kpis.ts |
| Activation Funnel | 5 steps: first event → 2+ features → 5+ features → ≥1h total → active on day 7. The ≥1h step is a legacy threshold, not empirically validated. | /product-adoption Funnel panel | lib/analytics/product/queries/funnel.ts |
| Retention Heatmap | Weekly cohorts — % of users from week N still active in week N+k. | /product-adoption Retention heatmap | components/... RetentionHeatmap |
| Feature Depth | Distinct features touched ≥ 60s in the last 30 days (capped at 5, Settings + AI Agents excluded). | Health Score (Depth pillar) + Product Adoption feature breakdown | scripts/migrations/086_unify_health_to_v3.sql |
| CSM Signals | At-risk locations (score dropping), drifting users, declining features week-over-week. | /product-adoption CSM Signals panel | CsmSignalsPanel component |
Null and unknown feature keys end up here. Investigate new entries to decide if they deserve canonicalization.
Anything not recognized after aggregation is grouped into Other.
-- Inspect Other breakdown by hours (last 30 days)
SELECT feature_key, round(sum(time_sec) / 3600.0, 2) AS hours
FROM public.product_adoption_daily
WHERE event_date >= current_date - interval '30 days'
AND feature_key NOT IN (
'dashboard','conversations','calendars','contacts','opportunities',
'payments','marketing','email','emails','automation','reputation',
'blogs','memberships','integration','settings','funnels-websites',
'form-builder','survey-builder',
'66a97ca06e16787562476424','5e9efed3-e795-4fe6-8e56-9f9bb6c730fb',
'0bae2e9d-79d4-4513-84e1-a3056dc45acb','other'
)
GROUP BY 1
ORDER BY hours DESC;Things to keep in mind when debugging unexpected numbers.
Settings and AI Agents are excluded from the used_any_feature flag used by the Health Score Activation pillar — this exclusion happens in SQL (migration 086), not in the TS lib. A user who only opens Settings will show as "no feature activated" in the health score.
The deployed GHL custom JS is managed outside git. Documentation and ingest contract can drift if sync discipline is not enforced.
Historical rows may still contain custom-menu-link / custom-page-link from before the API normalization fix. New ingests rewrite these to custom feature IDs.
The Product Adoption page shows an Other breakdown panel to identify which keys are driving the catch-all bucket.