Skip to content

analysis: Phase 1 cohort query the shipped helper cannot express - #431

Merged
JasonYeYuhe merged 1 commit into
mainfrom
telemetry-phase1-cohort-query
Aug 12, 2026
Merged

analysis: Phase 1 cohort query the shipped helper cannot express#431
JasonYeYuhe merged 1 commit into
mainfrom
telemetry-phase1-cohort-query

Conversation

@JasonYeYuhe

Copy link
Copy Markdown
Collaborator

Why

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 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 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 survive as labelled diagnostics only.

Four things that are cheap to record now and expensive to rediscover later:

Metric is menu-open → provider The disclosure card lives in the lazily-built MenuBarExtra and gates both events via hasSeenDisclosure. An install that never opens the menu is invisible, not a counted failure. Never call it "activation rate" unqualified.
iOS contributes nothing AnonymousTelemetryCoordinator is constructed only in CLIPulseBarApp; CLI Pulse Bar iOS has zero references. The iOS 1.47 approval date is irrelevant to this table.
Deleting a row does not reset the client installReported / activationReported are UserDefaults latches set only on a 2xx. A deleted install is absent from every future read. pg_stat_user_tables currently reads 7 inserts / 5 deletes / 2 live rows.
Construct validity providers.isEmpty == false can 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

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. Confirmed against a probe of 1.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 — all exit=0
  • scripts/check_migration_numbers.shexit=0, 73 migrations, all unique

Current output — §1 attributes the empty cohort rather than just reporting zero:

stage rows
all rows in table 2
channel recognised 2
first_seen_at ≥ channel 1.47 availability 0
AND matured ≥ 7 days 0

§3 computes the escape-hatch deadline as 2026-09-09, 27 days remaining.

🤖 Generated with Claude Code

…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>
Copilot AI lite review requested due to automatic review settings August 12, 2026 12:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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';
@JasonYeYuhe
JasonYeYuhe merged commit 197e939 into main Aug 12, 2026
22 checks passed
@JasonYeYuhe
JasonYeYuhe deleted the telemetry-phase1-cohort-query branch August 12, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants