Repository navigation
feat(analytics): instrument the signup → strategy funnel - #482
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
devcommits (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()callsThe audit found several events that already existed but fired from paths production never takes —
brand_createdfires 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:utils/billing-navigation.tsgoToBilling(source)— all 14 upgrade CTAsutils/social-navigation.tsutils/social-connect-attempt.tsutils/strategy-funnel.tsutils/follower-bucket.tsconstants/PlanPricing.tsgoToBillingis the main one. Fourteen sites calledrouter.push("/billing")and none recorded the click — and fourteen hand-written calls would rot the moment a fifteenth door shipped. It also carries?from=, sopaywall_viewed.triggerfinally 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.
DrawerMenuItemandcomponents/menueach 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.AIModelSelectorgainssurface/taskprops (5 call sites). All five chat surfaces mount the same component, so without them every model action reported identically.taskmatters because one stored preference is resolved per task.Two defects fixed, both found while instrumenting
calendar_item_rescheduledwas over-counting. It fired inMonthViewbefore calling the move, but the screen silently rejectsscheduled/postedcontent — so every rejected drag counted as a successful reschedule. Reporting moved tohandleMoveItem, the only place that knows the outcome. The rejection also now explains itself rather than the chip springing back in silence.subscription_startedfinally carries revenueThe 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 toconstants/PlanPricing.tsso 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 --noEmitclean.jest: 163 passing. Three suites inshared-libs/firestore/crowdy-chat/testsfail to load on a missing@firebase/rules-unit-testingdev dependency — pre-existing, in files this PR does not touch, and the package is not inpackage.json.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