Skip to content

Daily ping: one anonymous ping per tool per day - #2543

Merged
backnotprop merged 11 commits into
mainfrom
feat/daily-ping
Nov 3, 2026
Merged

backnotprop merged 11 commits into
mainfrom
feat/daily-ping

Conversation

@backnotprop

@backnotprop backnotprop commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Owner decisions in this PR

  • On by default. There is no in-app notice and no wait: the first ping can go on the first use after upgrading. People are told by the release notes, the privacy page, and the Inbox's Settings > About "Privacy" part.
  • NO_ANON_PING is the only off switch, and the only variable that user-facing text names. Any non-empty value other than 0 turns it off. There is no Settings switch and no config key.
  • The do-not-track convention and CI also turn it off, quietly. Dev and test builds never send.
  • No README change.

The Worker is deployed at ping.plannotator.ai (see below), and the owner has approved the privacy page and release-note wording.

What it sends

Once a UTC day, each tool a computer uses sends one ping. The tools are inbox, plan, review, annotate and last.

POST https://ping.plannotator.ai/ping
content-type: application/json
user-agent: plannotator

{"version":"0.28.46","age_week":2,"tool":"plan","first":1}
  • version: the release (X.Y.Z only).
  • age_week: the install-age bucket, Fedora countme style: 1 the first week, 2 weeks 2-4, 3 weeks 5-24, 4 older.
  • tool: inbox, plan, review, annotate or last.
  • first: 1 only on the computer's first ping of the UTC day from any tool, else 0. The server counts daily users from it, with no ID.

A computer sends at most 5 pings a day. Pings carry no ID, no OS and nothing else.

When it checks

  • CLI sessions: plan review, code review, annotate, and every annotate-last command (as last). Archive and goal setup send nothing.
  • The Inbox: at start, then at most once an hour on the existing update timer.
  • Pi's plan, review and annotate servers. Pi's annotate-last counts as last.
  • OpenCode's in-process plan, review, annotate and last.

Every check runs in the background and is never awaited.

Only a release CI marks can send

  • Release CI compiles the binaries with --define __PLANNOTATOR_RELEASE__=true, in release.yml only.
  • Before packing, npm-package writes a release.json ({"version":"X.Y.Z"}) into each package that pings: dist/release.json for OpenCode, and release.json beside Pi's package.json (Pi's files ships it).
  • OpenCode and Pi count as a release only when that mark names the package's own version. A run from this checkout, a local bun pm pack, or a copied folder has no mark and is a dev build.

Everything else has no live endpoint: a source run, a local build, or a test that spawns the CLI with any __CLI_VERSION__ and a replaced env. On top of that:

  • The test preload and the e2e global setup set NO_ANON_PING=1.
  • Each test that spawns with a replaced env sets it too.
  • tests/daily-ping-live-guard.test.ts guards all of it.

How a check works

  1. Read ping.json in the data dir. Stop if this tool already sent today.
  2. Take ping.lock (O_EXCL, holds a random token, waits up to 5 s, stale after 60 s), then read the file again. A new file's firstWeek is the week of the oldest time in the data folder, so an existing user's first ping isn't counted as a new install.
  3. POST with a 3 s timeout.
  4. Record the tool only on a 2xx, and only while the lock still holds this process's token. A process whose lock was taken over writes nothing and removes nothing. A failed or offline send tries again at the tool's next check.

Nothing is written while the ping is off.

Worker (apps/telemetry, deployed)

  • POST /ping validates the exact four keys and upserts daily(day, version, age_week, tool, n). When first is 1 it also upserts daily_total(day, version, age_week, n). Both upserts use n = n + 1. Returns 204.
  • A body over 160 bytes is refused unread, by its Content-Length, or once a capped read passes the limit.
  • GET /totals returns public JSON of the last 90 days' sums: users, per age bucket, per version, per tool. It is served from caches.default for 5 minutes, so repeat GETs don't touch D1.
  • There is no per-isolate rate limit, because it would bound nothing. If counts are ever inflated, add a zone rate-limiting rule.
  • It stores no IP, no header and no request.cf data.
  • Logs, observability and Logpush are off. It answers only on the custom domain ping.plannotator.ai (workers_dev = false).
  • database_id is the deployed D1 database plannotator-ping (not a secret). The deploy steps are in apps/telemetry/README.md.

Inbox Settings > About

A plain "Privacy" part below the version and updates, with the owner-approved text. It shows on the computer's own page and on a tailnet guest's page.

Tests

All tests use a temp data dir and a loopback receiver only.

  • packages/shared/daily-ping.test.ts:
    • the exact bytes;
    • once a day per tool;
    • first only on the day's first ping;
    • the CLI tool mapping;
    • age buckets;
    • NO_ANON_PING, DO_NOT_TRACK, CI and dev builds;
    • sandbox loopback only;
    • a failed or offline send retries;
    • the stale lock;
    • two processes with the same tool give one request;
    • two processes with different tools give one first.
  • tests/daily-ping-live-guard.test.ts: a child with a release version and a bare env has no live endpoint; no test makes a marked build; every test that spawns with a release version turns the ping off or sandboxes it; every script or workflow that runs under env -i sets NO_ANON_PING=1; the preload sets the off switch. It also parses release.yml: every CLI compile defines the mark, each release.json is written after the build and before its pack, no other workflow marks a build, and Pi's files ships release.json.
  • apps/hook/server/daily-ping-cli.test.ts: plannotator annotate as a real process sends its ping to a loopback receiver.
  • apps/opencode-plugin/daily-ping.test.ts: plan, review, annotate and last each send their ping; a local build is dev.
  • packages/server/inbox-daily-ping.test.ts: an Inbox start sends tool: "inbox", and NO_ANON_PING and DO_NOT_TRACK stop it.
  • apps/pi-extension/daily-ping.test.ts: Pi parity for all four tools; a checkout run and an installed package without a matching release.json count as dev builds; the off switches work.
  • apps/telemetry/test/handler.test.ts: exact keys, both upserts, no IP, header or cf data reaching D1 (fake D1), and the /totals shape.
  • packages/inbox/aboutPrivacy.test.tsx (DOM): Privacy shows on both pages, with the env var as code and the link opening in a new tab.

46 guarded behaviors were each reverted on their own, and the matching test failed every time.

Sends {"version":"X.Y.Z","age":N} (Fedora countme age bucket) to
ping.plannotator.ai once a day, after a one-time notice (CLI stderr line,
Inbox toast) and never on the notice day. Off with NO_ANON_PING, the shared
do-not-track convention, CI, dev and test builds, config.json telemetry, or
the synced Settings switch. Checks run in the background from the CLI's
sessions, the Inbox (start, hourly), Pi's servers and OpenCode's in-process
servers. Restores apps/telemetry as a cut-down Worker (POST /ping, GET
/totals, D1 daily(day, version, age, n), no IP, logs off). Rewrites the
privacy page and README, and fixes the stale update-check opt-out text.
Owner decisions applied:
- Each tool (inbox, plan, review, annotate, last) sends its own
  once-a-day ping: {"version","age_week","tool","first"}. `first` is 1
  only on the computer's first ping of the UTC day, so the server
  counts daily users without an ID. At most five pings a day.
- On by default with no in-app notice and no wait.
- No Settings switch and no config key: NO_ANON_PING is the only off
  switch (the do-not-track convention and CI still turn it off,
  quietly). The README is back to main.
- Inbox Settings > About gets a plain "Privacy" part with the
  approved text.
- The Worker stores daily(day, version, age_week, tool, n) and
  daily_total(day, version, age_week, n).
- Privacy page, env var docs and agent guides updated.
@backnotprop backnotprop changed the title Daily ping: one private "ran today" per computer per day (default waits on the owner) Daily ping: one anonymous ping per tool per day Nov 3, 2026
- P1: only a build release CI marks may reach the live host. The
  binaries are compiled with --define __PLANNOTATOR_RELEASE__=true
  (release.yml only). OpenCode needs the dist/release.json release CI
  writes. Pi counts only outside this checkout. A test that spawns the
  CLI with a release version and a replaced env is a dev build.
  The test preload and the e2e global setup set NO_ANON_PING=1. Each
  replaced-env spawn sets it too. tests/daily-ping-live-guard.test.ts
  guards all three.
- P2: GET /totals is served from caches.default for 5 minutes and
  reads only the last 90 days.
- P3: Pi's archive browser sends nothing. age_week counts from the
  data folder's oldest time. The lock holds a random token: a
  process whose lock was taken over writes and removes nothing. The
  Worker refuses a large body unread (Content-Length, capped read).
  Doc fix. bun.lock is main's plus only the telemetry workspace. The
  CLI and OpenCode ping calls have loopback tests.
- Pi counts only with the release.json release CI writes beside its
  package.json (shipped by files) naming the package's own version; a
  local pack or copied folder is a dev build.
- release.yml writes the Pi mark before "Pack @plannotator/pi-extension".
- The live guard parses release.yml: every CLI compile defines
  __PLANNOTATOR_RELEASE__=true, each release.json comes after the build
  and before its pack, no other workflow marks a build, Pi files ships
  release.json; scripts and workflows that run under env -i set
  NO_ANON_PING=1.
- smoke-update-prepare.sh smoke_env sets NO_ANON_PING=1.
- firstUseWeek uses lstatSync.
@backnotprop backnotprop added the ci:full Run the heavy CI tier on this PR (added by the merge script) label Nov 3, 2026
…ng off

#2547's spec spawns the Inbox with a release version and a replaced env,
which drops global-setup's NO_ANON_PING. Set it in the spawned env, as
inbox-list-intro.spec.ts does, so daily-ping-live-guard passes.
@backnotprop
backnotprop merged commit 303b672 into main Nov 3, 2026
69 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full Run the heavy CI tier on this PR (added by the merge script)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant