Skip to content

feat(analytics): instrument the signup → strategy funnel - #482

Merged
rahul-sinha1908 merged 2 commits into
devfrom
feature/funnel-analytics-events
Oct 28, 2026
Merged

rahul-sinha1908 merged 2 commits into
devfrom
feature/funnel-analytics-events

Conversation

@rahul-sinha1908

@rahul-sinha1908 rahul-sinha1908 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Part 4 of 5. Unblocked — idivarts/shared-constants#11, idivarts/shared-libs#83 and idivarts/shared-uis#84 are merged, and the pointers here now sit on their dev commits (f9bc03b, 4d64fb4, e294276). Typecheck re-run against the merged submodules: clean. Only idivarts/backend-sls#421 is still open, and it is independent of this one.

Implements the reviewed funnel spec. 60 files.

Why this is mostly structure, not sixty track() calls

The audit found several events that already existed but fired from paths production never takes — brand_created fires only on the legacy landing screen, and before the API call, so a failed creation counted as a brand. Rather than add more of those, the navigation and the choke points do the reporting:

New file What it owns
utils/billing-navigation.ts goToBilling(source) — all 14 upgrade CTAs
utils/social-navigation.ts the 5 doors into Connected Accounts
utils/social-connect-attempt.ts the attempt, parked across the OAuth round trip
utils/strategy-funnel.ts start→formed timing, across a navigation
utils/follower-bucket.ts bucketing, so no raw follower count is reported
constants/PlanPricing.ts one price number for the table and the event

goToBilling is the main one. Fourteen sites called router.push("/billing") and none recorded the click — and fourteen hand-written calls would rot the moment a fifteenth door shipped. It also carries ?from=, so paywall_viewed.trigger finally names the door; it had two possible values for fourteen doors, so it could not say why anyone was looking at pricing.

The menu is two calls, not forty. DrawerMenuItem and components/menu each funnel every entry through one handler. Both report before the lock check — a tap on a plan-gated row is the highest-intent signal the menu produces, and the early return meant it produced nothing at all.

AIModelSelector gains surface / task props (5 call sites). All five chat surfaces mount the same component, so without them every model action reported identically. task matters because one stored preference is resolved per task.

Two defects fixed, both found while instrumenting

  • calendar_item_rescheduled was over-counting. It fired in MonthView before calling the move, but the screen silently rejects scheduled/posted content — so every rejected drag counted as a successful reschedule. Reporting moved to handleMoveItem, the only place that knows the outcome. The rejection also now explains itself rather than the chip springing back in silence.
  • The analytics empty state was a dead end — it told the user to connect an account and gave them no button. It has one now.

subscription_started finally carries revenue

The registry has always declared value / currency; the watcher never passed them, so the primary conversion event reported no revenue and nothing downstream could bid or report on value. Prices moved to constants/PlanPricing.ts so the pricing table and the event read one number. The file says plainly that the real fix is server-side emission from the webhook — only that sees proration, coupons and tax.

Verification

  • tsc --noEmit clean.
  • jest: 163 passing. Three suites in shared-libs/firestore/crowdy-chat/tests fail to load on a missing @firebase/rules-unit-testing dev dependency — pre-existing, in files this PR does not touch, and the package is not in package.json.
  • Registry cross-check: 82 declared, 78 fire here, 3 from backend-sls, 1 deliberately deferred.

Not verified in a simulator — per the repo's standing note, local iOS builds are unusable on this machine, so CI is the first real build.

🤖 Generated with Claude Code

Implements the reviewed funnel spec across 60 files. An audit of the live
funnel found that most of it was unmeasured, and several events that did exist
fired from paths production never takes — brand_created, for instance, fires
only on the legacy landing screen, and before the API call, so a failed
creation counted as a brand.

Structural changes, rather than a track() per call site:

- utils/billing-navigation.ts — goToBilling(source). Every upgrade CTA used to
  call router.push("/billing") directly: fourteen sites, none of which
  recorded the click. Fourteen hand-written calls would rot the moment a
  fifteenth door shipped, so the navigation itself reports, and carries
  ?from= so paywall_viewed finally names the door (it had two possible values
  for fourteen doors).
- utils/social-navigation.ts — the same for the five doors into Connected
  Accounts.
- utils/social-connect-attempt.ts — parks the attempt in storage across the
  OAuth round trip. On web the callback lands in a NEW TAB, so nothing
  in-memory survives; this is the only way the success/failure events can name
  the entry point, time the round trip, or know whether it was the brand's
  first account.
- DrawerMenuItem and components/menu both press through ONE handler, so two
  track() calls cover every menu entry present and future. Both report BEFORE
  the lock check: a tap on a plan-gated row is the highest-intent signal the
  menu produces, and returning early meant it produced nothing.
- AIModelSelector gains surface/task props. All five chat surfaces mount it,
  so without them every model action in the app reported identically.

Two defects fixed along the way, both found while instrumenting:

- calendar_item_rescheduled fired in MonthView BEFORE calling the move, but
  the screen silently rejects scheduled/posted content — so every rejected
  drag counted as a successful reschedule. Reporting moved to the screen's
  handleMoveItem, which is the only place that knows the outcome, and the
  rejection now explains itself instead of the chip springing back in silence.
- The analytics empty state told the user to connect an account and gave them
  no way to do it. It has a CTA now.

subscription_started finally carries value/currency. The registry has always
declared them and the watcher never passed them, so the primary conversion
event reported no revenue at all. Prices moved to constants/PlanPricing.ts so
the pricing table and the event read one number; the real fix is server-side
emission from the Razorpay/RevenueCat webhook, which the file says.

Submodule pointers bumped to the feature branches of
idivarts/shared-constants#11, idivarts/shared-libs#83 and
idivarts/shared-uis#84. They need re-bumping to dev once those merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
shared-constants #11, shared-libs #83 and shared-uis #84 are merged, so the
pointers move off their feature branches and onto dev. The earlier bump was to
branch commits only so this PR could be reviewed whole; left there, the
pointers would break the moment those branches were deleted.

  shared-constants → f9bc03b
  shared-libs      → 4d64fb4
  shared-uis       → e294276

Typecheck re-run against the merged submodules: clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rahul-sinha1908
rahul-sinha1908 merged commit 1b383ec into dev Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant