feat(observability): instrument the checkout funnel with started/completed/failed events#935
feat(observability): instrument the checkout funnel with started/completed/failed events#935FagnerMartinsBrack wants to merge 1 commit into
Conversation
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 5m 28s —— View job Claude Code Review — PR #935Reviewed the full diff ( High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1.
|
Claude Code Review — PR #935Reviewed the full diff ( High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1.
|
|
@claude Resolve conflicts. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 10m 14s —— View job Conflicts resolved ✅
Rebased
Verification
Force-pushed with |
e8174d9 to
ef7b2f4
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 4m 47s —— View job Claude Code Review — PR #935 (re-review @
|
|
@claude Resolve conflicts. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 13m 46s —— View job Conflicts resolved ✅
Rebased One real conflict, in The conflict was two independent, intentional changes hitting the same lines:
Resolution — kept both: each site is now Verification
Force-pushed with |
ef7b2f4 to
3319453
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude encountered an error —— View job Claude Code Review — PR #935 (re-review @
|
3319453 to
b0cb4f7
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 4m 14s —— View job Claude Code Review — PR #935 (re-review @
|
Claude Code Review — PR #935 (re-review @
|
|
@claude Resolve conflicts. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 15m 56s —— View job Conflicts resolved ✅
Rebased
5 conflicted files — all resolved additively (the two features are independent; nothing dropped):
The PR branched before #941's Verification
Force-pushed with · branch |
b0cb4f7 to
64568b5
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 3m 30s —— View job Claude Code Review — PR #935 (re-review @
|
Claude Code Review — PR #935 (re-review @
|
|
@claude Resolve conflicts. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 11m 8s —— View job Conflicts resolved ✅
Rebased One conflicted file —
The PR branched before the rename, so its side still referenced the now-deleted Verification
Force-pushed with · branch |
64568b5 to
bf078bf
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 3m 3s —— View job Claude Code Review — PR #935 (re-review @
|
High Priority Issues (Must Fix)None found Medium Priority IssuesNone found Low Priority Suggestions1.
|
|
@claude Resolve conflicts. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 12m 42s —— View job Conflicts resolved ✅
Rebased One conflicted file —
The PR branched before the middleware relocation, so its side still passed analytics into One environment step: main's Verification
Force-pushed with |
…leted/failed events Emit three subscriptions-stream events from the hutch web app: checkout_started (POST /account/subscribe, all three Stripe-Checkout entry paths with a variant), checkout_completed (GET /auth/checkout/success paid path — the missing paid-conversion event), and checkout_return_failed (each failure branch with a reason). Reuses the existing initEmitSubscriptionEvent factory and SUBSCRIPTION_EVENTS constants; the composition root supplies a typed subscription logger like conversionLogger. Adds a "Checkout funnel per day" dashboard widget sourced from the hutch handler log group (widget count 27 → 28). Claude-Session: https://claude.ai/code/session_01En2uUiiwQoRQKWenCnkPi9
bf078bf to
3176748
Compare
|
@claude Review this PR. Use your Read tool to read the local file Context
|
|
Claude finished @FagnerMartinsBrack's task in 5m 4s —— View job Claude Code Review — PR #935 (re-review @
|
Claude Code Review — PR #935 (re-review @
|

Checkout funnel instrumentation (R2)
Hypothesis
The stage between the /account Subscribe click and revenue is completely dark. If we emit structured events at checkout start, checkout completion, and every checkout-return failure, we can see exactly where the 0% trial→paid conversion is leaking — and measure any future fix instead of guessing.
Evidence (30d, Jun 4 – Jul 4 2026)
upsertActivedirectly) — the two known conversions (3e9c7e5c Jun 21, af194ac4 Jun 23) are invisible to every dashboard.What this changes
Three new events on the existing
subscriptionsstream, emitted by the hutch web app (not the subscription Lambdas):checkout_startedPOST /account/subscribe, after the Stripe Checkout session + pending-signup writeuser_id,variant∈trial_checkout|cancelled_resubscribe|card_decline_fallback,checkout_session_idcheckout_completedGET /auth/checkout/successpaid path — the missing paid-conversion eventuser_id,subscription_id,checkout_session_idcheckout_return_failedGET /auth/checkout/successreason∈invalid_query(400) |session_not_found(404) |not_paid(402) |replayed(409), optionaluser_id/checkout_session_idPlumbing reuses the existing
initEmitSubscriptionEventfactory andSUBSCRIPTION_EVENTSconstants; the composition root suppliesHutchLogger.fromJSON<SubscriptionLogEvent>()exactly like the conversion logger. One new dashboard widget "Checkout funnel per day" (timeSeries, sourced from the hutch handler log group) charts all three events; widget count 27 → 28.Deliberately not covered: the one-click resubscribe success path (
subscriptions.createwith a saved card, no Checkout UI) — flagged as follow-up.Estimated result
No behaviour change for users; pure instrumentation. Next trial-end cohort (~28 users/month at current volume) produces a complete started→completed/failed funnel, letting us attribute the 4-clicks→0-conversions gap to a concrete branch (abandoned Checkout, not-paid returns, replayed links, or a silent server error).
How to measure
CloudWatch Logs Insights against
/aws/lambda/hutch-handler:Funnel-per-day view is the new dashboard widget. Conversion rate =
checkout_completed/ distinctuser_idoncheckout_started.Merge-conflict note
The dashboard drift test asserts an exact widget count (
toHaveLength(28)after this PR; main was 27). Sibling PRs #916–#918 may bump the same assertion. Resolution is mechanical: take both widget additions, set the count to the actualwidgets.length, merge the description-text category list, and re-runpnpm nx run hutch:check— the geometric no-overlap test catches any real collision (if two PRs used the same slot, move this widget to a full-width row at x:0, y:130).https://claude.ai/code/session_01En2uUiiwQoRQKWenCnkPi9