A signup that hit a network error, the 8s timeout or a 5xx was handed straight
to a `mailto:` composer. That path is lossy by design: it only works if the user
actually presses send, and if we keep reconciling the support inbox into Brevo
list 4 forever (AGE-61's hourly job). 20 of 21 signups were lost that way before
that reconciler existed, and Play's active base is ~100% on v0.4.10+ — so this
was current builds leaking, not just the ~436 stale sideloads.
Now:
- Failed-but-retryable signups are persisted on-device
(`opencode.waitlist.pending.v1`, AsyncStorage) and retried on every app
foreground (`app/_layout.tsx`) and on the Add Connection screen mount.
- 4xx stays non-retryable: the server will never accept that address, so we ask
the user to fix it instead of queueing garbage forever.
- `mailto:` is now only ever opened by an explicit user tap ("Still not working?
Email us instead"), shown after 3 failed attempts, or offered in an alert when
device storage itself refuses the write — never as the silent default.
- The UI tells the truth: "Saved on this device — we'll finish signing you up as
soon as you're back online" instead of implying it was sent.
- `WaitlistResult.fallback` -> `retryable`, `shouldFallbackToMailto` ->
`isRetryableFailure`: the decision is about retry, not about mail.
Queue policy: dedupe by email, cap 5 entries, 30-day TTL, corrupt/foreign JSON
is discarded rather than replayed. Storage and the clock are injected so the
whole thing runs under `node --test` (16 new tests, incl. the acceptance case:
offline signup -> queued -> reconnect -> reaches the server, no mail client).
Also commits the AGE-61 measurement artifacts that were only ever local
(`distribution/waitlist-signup-path-coverage.md`, `scripts/play-version-share.mjs`)
and updates the doc's "current builds still leak" section, which this fixes.
Refs AGE-87, AGE-61.
Co-authored-by: engineer <engineer@macbookpro.lan>
3.7 KiB
3.7 KiB
Waitlist signup-path coverage — how much of the install base can't sign up in-app
Measured 2026-08-14 for AGE-61. Re-run with node scripts/play-version-share.mjs --github.
The question
The in-app OpenCode Connect waitlist form posts to POST /api/beta-signup (Brevo list 4).
That code path first shipped in v0.4.8 (commit 0fdfb54, 2026-07-18). Every older build
has exactly one path: open a mailto: to support@agentlabs.cc, which lands in a human inbox
and nowhere near the waitlist store. 20 of 21 signups between 2026-08-03 and 2026-08-13 were
lost that way. So: how many people are still on a build with no working signup path?
Answer, by channel
| Channel | Population measured | Pre-v0.4.8 share |
|---|---|---|
| Google Play (auto-updates) | ~90–100 daily distinct users, 2026-07-31 → 2026-08-13 | 0% — Play reports a single versionCode, 142 (v0.4.10). Any older build is below Play's reporting floor (<10 users/day). |
| Sideload — GitHub release APKs (never auto-update) | 1,682 lifetime APK downloads across all releases | 25.9% (436 downloads) are pre-v0.4.8 builds. |
There is no iOS App Store listing (itunes.apple.com/lookup?bundleId=cc.agentlabs.opencode
returns 0 results) and the app is not on IzzyOnDroid, so those channels contribute nothing.
What that means
- Play is not the problem. Auto-update did its job: the measurable Android active base is effectively 100% on a build that can sign up through the API.
- The stale cohort is sideload-only and permanent. ~436 devices pulled an APK that predates
the signup API. Nothing will ever update them; they will keep emitting mailto signups until
the owner manually re-downloads. The hourly reconciler
(
VibeBrowserProductPage/.github/workflows/waitlist-mailto-reconcile.yml) is what keeps those signups from being lost, and it is not a temporary measure. - Stale builds are not the only source of mailto signups — they are, as of AGE-87, the only
remaining one. Until v0.4.12 the fallback also fired on network error, timeout (8s) and 5xx,
so a user on a current build with flaky mobile data took the same lossy path. That path is gone:
a failed signup is now persisted on-device (
opencode.waitlist.pending.v1, AsyncStorage) and retried on every app foreground (src/lib/waitlist.tsqueue section, flushed fromapp/_layout.tsx).mailto:is only ever opened by an explicit user tap after repeated retry failures. Expect the reconciler'ssynced_countto trend toward the sideload cohort only. Any plan that assumes "ship an update and the leak closes" is still wrong for those ~436 devices.
Method / reproducing
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON="$(cat play-store-key.json)" \
node scripts/play-version-share.mjs --days 14 --github
- Play numbers come from the Play Developer Reporting API,
crashRateMetricSet→distinctUsersgrouped byversionCode. That is Play's own vitals denominator: users who actually opened the app. It only covers devices with usage-and-diagnostics sharing on, and Play rounds it — which is why we quote a share, never an absolute install count. - Play versionCodes are not the ones in
android/app/build.gradle: the publish workflow overwrites them withgithub.run_number + 100. The mapping in the script was derived from the successful runs ofpublish-play-store.yml(versionCode 139= v0.4.8 = first build with the API path). - The service account is the same
PLAY_STORE_SERVICE_ACCOUNT_JSONalready used to publish; the script needs only theplaydeveloperreportingscope and mints its own token, no extra deps.