Symptom
On the day-0 (post-checkout) activation path, the two wizard steps that require an authenticated API call — brief and alerts — fail together, while power (no network call) succeeds. Nothing lands in Convex for the affected users.
Evidence (Umami funnel + Convex cross-check, 2026-07-24 21:01 → 2026-07-25 17:48 UTC)
failed in the exit summary means a genuinely errored write — outcomeStatus() maps skipped→pending, and finalizeAndShowSummary only marks the current step failed when transient === 'failed' (ProActivationInterstitial.ts:498-509). It is not "user couldn't be bothered".
Sessions bucketed by entitlement age at mount:
| session |
entitlement created |
mounted |
age |
exit |
Convex rows ever |
| 66f373dc |
07-25 16:12 |
16:14 |
2 min |
v=1 f=2 p=0 |
none |
| 4a787d0d |
07-25 17:47 |
17:48 |
1 min |
v=0 f=2 p=1 |
none |
| bda23952 |
07-25 03:06 |
03:07 |
1 min |
v=0 f=1 p=2 |
none |
| 98878a21 |
07-25 01:11 |
01:12 |
1 min |
v=1 f=1 p=1 |
none |
| d1389af0 |
07-24 18:57 |
21:01 |
124 min |
v=2 f=1 p=0 |
created 21:05, outside the wizard |
Contrast — the retro/backfill path (onlyIfUnactivated: true), entitlement 67–96 h old:
| server-recorded outcome |
rows created during the flow |
confirmed [alerts, power] |
web_push channel + full rule, 14:27:20 ✅ |
confirmed [brief, alerts] |
email + web_push channels + full rule, 17:13:36–54 ✅ |
confirmed [] (skipped all) |
none — correct ✅ |
4 of 4 sessions that mounted within 2 minutes of entitlement creation had at least one network-backed step fail; 2 had both fail. 0 of 3 aged-entitlement sessions had any failure. Every retro-path confirm produced exactly the right rows, so the confirm→write path itself is sound.
power succeeding while brief and alerts both fail points at a shared precondition, not two independent bugs — and rules out push-permission denial as the sole explanation (that would not affect brief).
Candidate mechanisms
Both steps funnel through an authenticated fetch:
assertExpectedAccount (notification-channels.ts:53-57) throws Authenticated account changed during notification setup when getCurrentClerkUser()?.id !== expectedUserId. Right after the checkout-return reload, getCurrentClerkUser() may still be null → undefined !== expectedUserId → throws before any network call. The mount gate only requires currentAccountKey !== null from auth-state, which can resolve before the Clerk user object hydrates.
authFetch (notification-channels.ts:59-81) — getClerkToken() null, single 2 s retry, then throw new Error('Not authenticated (Clerk token null after retry)').
- The just-created entitlement not yet visible to
/api/notification-channels → non-2xx → throw.
Not yet reproduced; needs a real day-0 checkout to confirm which.
Why nobody noticed
Three independent blind spots, all on the failing path:
- The day-0 path writes no server-side record at all.
onlyIfUnactivated: false leaves expectedActivationKey/activationClaimNonce undefined, so claimProActivationPresentation / confirmProActivationPresentation / recordProActivationOutcome are all skipped (ProActivationInterstitial.ts:1191, :1305). proActivationPresentations and report-activation-lift.mjs cover only the retro cohort — the one that works.
- Failures are logged to Umami as skips.
handleSkip (:527-532) fires pro-activation-step-skipped unconditionally while recording the outcome as failed. There is no step-failed event in the catalog; failures survive only as an aggregate count on pro-activation-exit.
- The error text is nowhere.
console.warn('[pro-activation] brief confirm failed', err) is a Sentry breadcrumb only — sentry-init.ts has no captureConsole integration, so no event is ever created.
Suggested order
Sentry.captureException in the confirmBrief / confirmAlerts catch blocks, tagged with the step id — cheapest change, tells us which of the three mechanisms it is.
- Add a
pro-activation-step-failed event so the funnel stops reporting failures as skips.
- Extend the outcome ledger to the day-0 path so the failing cohort is visible server-side.
- Fix the root cause once (1) identifies it.
Sample-size caveat
7 sessions with recorded outcomes across both arms. The split is clean and the mechanism is identifiable, but this is one day of data and the bug is not yet reproduced.
Symptom
On the day-0 (post-checkout) activation path, the two wizard steps that require an authenticated API call —
briefandalerts— fail together, whilepower(no network call) succeeds. Nothing lands in Convex for the affected users.Evidence (Umami funnel + Convex cross-check, 2026-07-24 21:01 → 2026-07-25 17:48 UTC)
failedin the exit summary means a genuinely errored write —outcomeStatus()maps skipped→pending, andfinalizeAndShowSummaryonly marks the current step failed whentransient === 'failed'(ProActivationInterstitial.ts:498-509). It is not "user couldn't be bothered".Sessions bucketed by entitlement age at mount:
Contrast — the retro/backfill path (
onlyIfUnactivated: true), entitlement 67–96 h old:[alerts, power]web_pushchannel +fullrule, 14:27:20 ✅[brief, alerts]email+web_pushchannels +fullrule, 17:13:36–54 ✅[](skipped all)4 of 4 sessions that mounted within 2 minutes of entitlement creation had at least one network-backed step fail; 2 had both fail. 0 of 3 aged-entitlement sessions had any failure. Every retro-path confirm produced exactly the right rows, so the confirm→write path itself is sound.
powersucceeding whilebriefandalertsboth fail points at a shared precondition, not two independent bugs — and rules out push-permission denial as the sole explanation (that would not affectbrief).Candidate mechanisms
Both steps funnel through an authenticated fetch:
assertExpectedAccount(notification-channels.ts:53-57) throwsAuthenticated account changed during notification setupwhengetCurrentClerkUser()?.id !== expectedUserId. Right after the checkout-return reload,getCurrentClerkUser()may still benull→undefined !== expectedUserId→ throws before any network call. The mount gate only requirescurrentAccountKey !== nullfrom auth-state, which can resolve before the Clerk user object hydrates.authFetch(notification-channels.ts:59-81) —getClerkToken()null, single 2 s retry, thenthrow new Error('Not authenticated (Clerk token null after retry)')./api/notification-channels→ non-2xx →throw.Not yet reproduced; needs a real day-0 checkout to confirm which.
Why nobody noticed
Three independent blind spots, all on the failing path:
onlyIfUnactivated: falseleavesexpectedActivationKey/activationClaimNonceundefined, soclaimProActivationPresentation/confirmProActivationPresentation/recordProActivationOutcomeare all skipped (ProActivationInterstitial.ts:1191,:1305).proActivationPresentationsandreport-activation-lift.mjscover only the retro cohort — the one that works.handleSkip(:527-532) firespro-activation-step-skippedunconditionally while recording the outcome asfailed. There is nostep-failedevent in the catalog; failures survive only as an aggregate count onpro-activation-exit.console.warn('[pro-activation] brief confirm failed', err)is a Sentry breadcrumb only —sentry-init.tshas no captureConsole integration, so no event is ever created.Suggested order
Sentry.captureExceptionin theconfirmBrief/confirmAlertscatch blocks, tagged with the step id — cheapest change, tells us which of the three mechanisms it is.pro-activation-step-failedevent so the funnel stops reporting failures as skips.Sample-size caveat
7 sessions with recorded outcomes across both arms. The split is clean and the mechanism is identifiable, but this is one day of data and the bug is not yet reproduced.