Skip to content

P0: fresh Pro purchases get 15 min of broken Pro — poisoned free entitlement cache + dead cache-sync (missing DODO_PAYMENTS_ENVIRONMENT on Vercel) #5600

Description

@koala73

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:

  1. 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 nullundefined !== 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.
  2. authFetch (notification-channels.ts:59-81) — getClerkToken() null, single 2 s retry, then throw new Error('Not authenticated (Clerk token null after retry)').
  3. 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:

  1. 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.
  2. 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.
  3. 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

  1. Sentry.captureException in the confirmBrief / confirmAlerts catch blocks, tagged with the step id — cheapest change, tells us which of the three mechanisms it is.
  2. Add a pro-activation-step-failed event so the funnel stops reporting failures as skips.
  3. Extend the outcome ledger to the day-0 path so the failing cohort is visible server-side.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions