analysis: Phase 1 cohort query the shipped helper cannot express - #431
Merged
Conversation
…ress it `anonymous_activation_summary(p_days)` groups by `day, channel` and selects no `app_version` at all, and applies no maturity window -- it counts an install that first reported ten minutes ago as a failed activation. Phase 1 needs a direct query. Verified against the live function body (pg_get_functiondef, 2026-08-12): production matches the migration byte for byte, so this is a limitation of the helper rather than drift. Read-only. No migration number consumed, no function created, no schema touched. Cohorts by `first_seen_at`, never by `app_version` -- the upsert does `app_version = excluded.app_version`, so a 1.45 install that later updates reports as 1.47. Version columns are kept as labelled diagnostics only. Records four things that are easy to get wrong and expensive to get wrong late: - The metric is menu-open -> provider, not install -> value. The disclosure card lives in the lazily-built MenuBarExtra and gates both events via `hasSeenDisclosure`, so an install that never opens the menu is invisible rather than counted as a failure. - iOS contributes nothing. `AnonymousTelemetryCoordinator` is constructed only in `CLIPulseBarApp`; the iOS target has no reference to it. The iOS 1.47 approval date is irrelevant to this table. - Deleting a row does not reset the client. `installReported` and `activationReported` are UserDefaults latches set only on a 2xx, so a deleted install is absent from every future read. pg_stat_user_tables currently reads 7 inserts / 5 deletes / 2 live rows. - `providers.isEmpty == false` can be satisfied by cloud-synced rows from another Mac, so it means "had a number to show", not "detected a CLI here". Section 4 carries negative controls, and 4e proves they fire on synthetic input rather than merely passing on real input -- which caught a real bug in 4b: the naive `string_to_array(v,'.')::int[] < array[1,47,0]` reports "1.47" as pre-1.47, because a shorter array sorts before its own prefix. The committed version pads to three components. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a read-only Supabase analysis SQL script to compute and sanity-check the Phase 1 “menu-open → provider detected” cohort funnel from public.anonymous_installs, including maturity gating, channel cutover dates, and negative controls intended to catch silent undercounting.
Changes:
- Introduces a staged attrition readout that explains why the eligible cohort can be empty (channel recognition, cutover, maturity, activation).
- Adds the main cohort rollup query with an explicit minimum-N decision gate for interpreting percentages.
- Adds an “escape-hatch” deadline calculation plus negative controls (including synthetic proof that controls can fire).
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+196
to
+203
| -- 4a. Rows whose channel is not in the availability list. Expected: 0 rows. | ||
| -- Non-zero means section 2 is silently under-counting. | ||
| select 'unlisted channel' as control, | ||
| channel, count(*) as rows, | ||
| 'expected 0 -- non-zero means section 2 silently drops these' as expectation | ||
| from public.anonymous_installs | ||
| where channel not in ('devid', 'brew', 'mas') | ||
| group by channel; |
Comment on lines
+239
to
+247
| select 'write history' as control, | ||
| n_tup_ins as inserts, n_tup_upd as updates, n_tup_del as deletes, | ||
| n_live_tup as live_rows, | ||
| n_tup_ins - n_tup_del as expected_live, | ||
| case when n_tup_ins - n_tup_del = n_live_tup | ||
| then 'reconciles' | ||
| else 'MISMATCH -- investigate before reading any percentage' end as verdict | ||
| from pg_stat_user_tables | ||
| where relname = 'anonymous_installs'; |
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.
Why
anonymous_activation_summary(p_days)groups byday, channeland selects noapp_versionat all, and applies no maturity window — it counts an install that first reported ten minutes ago as a failed activation. Phase 1 cannot be read through it.Checked against the live function body (
pg_get_functiondef, 2026-08-12): production matches the migration byte for byte. This is a limitation of the helper, not drift.Read-only. No migration number consumed, no function created, no schema change.
What it encodes
Cohorts by
first_seen_at, never byapp_version— the upsert doesapp_version = excluded.app_version, so a 1.45 install that later updates reports as 1.47. Version columns survive as labelled diagnostics only.Four things that are cheap to record now and expensive to rediscover later:
MenuBarExtraand gates both events viahasSeenDisclosure. An install that never opens the menu is invisible, not a counted failure. Never call it "activation rate" unqualified.AnonymousTelemetryCoordinatoris constructed only inCLIPulseBarApp;CLI Pulse Bar iOShas zero references. The iOS 1.47 approval date is irrelevant to this table.installReported/activationReportedare UserDefaults latches set only on a 2xx. A deleted install is absent from every future read.pg_stat_user_tablescurrently reads 7 inserts / 5 deletes / 2 live rows.providers.isEmpty == falsecan be satisfied by cloud-synced rows from another Mac. It means "had a number to show", not "detected a CLI here".Guard verification
Per the repo rule that a new guard must be shown to fire, not merely to pass — §4e feeds synthetic rows through the same predicates and asserts each control returns ≥ 1.
That caught a real bug in §4b: the naive
reports
"1.47"as pre-1.47, because a shorter array sorts before its own prefix. Confirmed against a probe of1.45.0 / 1.47.0 / 1.47 / 1.46.9 / 1.48.0 / 2.0 / 1.47.1. The committed version pads to three components.Verification run (production, read-only, 2026-08-12)
Every section executed against production. Backend CI gates run locally, exit codes captured to file rather than piped:
ci_check_search_path.py·ci_check_rpc_contract.py·ci_check_date_windows.py(80 SQL files scanned, includes this one) ·ci_check_alert_types.py·ci_check_user_id_cascade.py— allexit=0scripts/check_migration_numbers.sh—exit=0, 73 migrations, all uniqueCurrent output — §1 attributes the empty cohort rather than just reporting zero:
first_seen_at≥ channel 1.47 availability§3 computes the escape-hatch deadline as 2026-09-09, 27 days remaining.
🤖 Generated with Claude Code