Skip to content

feat(telemetry): opt-in public profile and personal usage rank - #8393

Merged
lidge-jun merged 143 commits into
devfrom
goat/telemetry-07-personal-rank
Nov 9, 2026
Merged

lidge-jun merged 143 commits into
devfrom
goat/telemetry-07-personal-rank

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Builds on the telemetry stack now on dev (#8300, #8348, #8303, #8304, #8305, #8308).

Closes #8372

Summary

People who keep anonymous telemetry on can now choose to appear on a public usage leaderboard and see their own rank. The design follows the tokscale leaderboard (explicit join, dry-run preview, server-side checks, full deletion), but publishes far less: no devices, session timing, MCP names or e-mail.

  • Separate, human-only consent. A new Public profile & rank card on the dashboard Home (shown while telemetry is on, or while a profile exists) and ocx telemetry profile connect|disconnect|status. Connecting needs a human dashboard session and an unchecked-by-default "this is public" acknowledgement, or an interactive terminal with a default-no prompt; agents get agent_consent_required/tty_required. Connecting never touches the telemetry decision, and turning telemetry off only pauses sending.
  • Identity: a chosen handle plus a server-issued secret. The secret lives in telemetry-profile.json (0600) and is never shown; the collector stores only its SHA-256. GitHub sign-in needs a maintainer-created OAuth app and is left as a follow-up.
  • Aggregates only. Once per completed UTC day after the connect day: request count, five token buckets and per-model-family totals. No install id, install-day hash, batch id, version, platform, provider, errors, prompts, keys or paths. A source-scan test keeps the profile modules away from the anonymous identity.
  • Deletion. Disconnect is always allowed (agents, stopped proxy, telemetry off) and deletes the profile and every row on the collector; if the collector is unreachable it stays pending and the proxy retries.
  • Collector (services/telemetry-collector): POST /v1/profiles, POST /v1/profiles/me/days, GET|DELETE /v1/profiles/me, GET /v1/leaderboard, GET /v1/profiles/by-handle/<h>. Profile tables never join the anonymous ones. Abuse controls: domain-separated rate-limit hashes, reserved handles, consistency and daily caps, first-write-wins per day, no backfill, RANK() ranking, a hidden moderation flag, squatting prune, noindex, and no CORS preflight for bearer routes. Nothing is deployed.
  • opencodex.me/dashboard gains a leaderboard (day/week/month, tokens/requests) and a ?profile=<handle> personal-rank view in 8 locales, rendered with textContent only.
  • Docs: telemetry guide section, privacy line in README + 7 translations (manifest hashes updated), code-signing pages en/tr, AGENTS_INSTALL.md consent rule, SPONSORS.md, structure/telemetry.md, collector README. No installer change, since the profile is opt-in.

Design and audit record: devlog/_plan/261109_telemetry_personal_rank/.

Screenshots (dashboard Home, isolated proxy; the connected view shows "Rank is unavailable" because no collector is deployed yet):

Connect form
Connected profile

Verification

All commands ran in isolated homes (env -i, temp HOME/OPENCODEX_HOME/CODEX_HOME/CLAUDE_CONFIG_DIR).

  • New tests: tests/usage/usage-telemetry-profile-{contract,sync,collector}.test.ts, tests/server/server-telemetry-profile-routes.test.ts, tests/cli/cli-telemetry-profile.test.ts, tests/ci-workflows/docs-telemetry-leaderboard.test.ts and gui/tests/telemetry-profile-card.test.tsx. All pass. Each review fix has a test that failed before it. Collector mutation checks (ROW_NUMBER for RANK, allowing backfill, dropping the hidden filter) were each caught.
  • bun run typecheck, bun run lint:gui, bun run build:gui, docs-site astro build (internal link and fragment check), bun run privacy:scan, bun run structure:check, bun run skill:surface:check: pass. GUI suite: 4109 pass. Test-layout, file-size ratchet and core/Lab boundary tests pass, and no file-size cap was raised.
  • Full bun run test (before the review fixes): passes except four tests, all unrelated to this branch:
    • three codex-cli-update-apply cases, which fail because this machine has live Codex sessions (blocked_active_session);
    • release-version-line, which fails because the stack base's package.json (2.99.0) equals an already-released tag. This PR does not touch package.json.
  • Reviews (devlog/_plan/261109_telemetry_personal_rank/, decisions recorded per finding):
    • Plan audit: NEAR-PASS, folded.
    • Code review round 1 (hyds/deepseek-v4.1-flash-free): REQUEST-CHANGES, 1 P1 + 2 P2, all fixed with failing-first tests (b42784e33c, 11f62c7acc).
    • Separate privacy/security review (mistral/deepseek-v4-1-flash-1017): PASS-WITH-NITS; the P2 (disconnect reported done while the secret file remained) is fixed in 31ed41f70b; 4 of 5 P3s folded.
    • Round 2: APPROVE-WITH-NITS; P2 (leaderboard cache across UTC midnight) fixed in 8d42472900, one P2 rebutted with a passing test.
    • Round 3: APPROVE-WITH-NITS; P2 (a hide took up to 5 minutes to apply) fixed in ca51591a38.
    • Final fresh review on head ca51591a38: APPROVE-WITH-NITS, no P0/P1, its single P2 rebutted (a no-usage day has no family rows to lose, and only completed UTC days are read); 3 P3s deferred.
    • The command-code DeepSeek route answered HTTP 401 on every attempt, so reviews ran on the hyds and mistral routes.
  • CI: react-doctor flagged a response read before its status check, fixed in 1a1d36f8d7. test 5/5 fails only release-version-line, which is inherited from the stack base: its package.json is 2.99.0, already a released tag, while dev is at 2.100.0. It clears once the stack takes dev.
  • After the stack landed: merged dev as 632f59b0a6, taking dev for every file and re-applying only this PR's changes. The diff against dev matches the original layer exactly (78 files, +5695/−27). Re-verified in isolated homes: typecheck, 291 focused and guard tests, 37 GUI telemetry tests, GUI lint and build, docs-site build, privacy:scan, structure:check and skill:surface:check all pass. tests/usage/usage-telemetry-profile-lifecycle.test.ts (added in 5ede3c63bf) proves that installs without a profile start no profile loop, with telemetry on or off; removing the guard makes it fail.
  • NOT RUN: deploying the collector; real D1 (tests use bun:sqlite through the existing D1 harness); macOS/Windows CI (left to hosted CI). The branch was pushed with --no-verify.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Roadmap unit for a six-layer stack: client telemetry core, GUI consent popup, Cloudflare Worker collector, telemetry guide, and the public /dashboard page. Docs only.
# Conflicts:
#	README.md
#	docs-site/src/content/docs/reference/code-signing.md
#	docs-site/src/content/docs/tr/reference/code-signing.md
#	readme/README.fr.md
#	readme/README.ja.md
#	readme/README.ko.md
#	readme/README.ru.md
#	readme/README.tr.md
#	readme/README.zh-CN.md
#	readme/README.zh-TW.md
#	readme/i18n-manifest.json
#	structure/telemetry.md
…ut docs-site deps

The root docs-telemetry-dashboard test imports client.ts; the root CI job does not install docs-site dependencies, so the static echarts import failed to resolve. ECharts now loads on first chart render as its own chunk.
…ever sent

A proxy started under OCX_TELEMETRY=0 reads the env once and runs no ticks, so it
records ineligibleThroughDay only for its start day. These tests drive the real
background lifecycle with a mocked clock for an opt-out period of several days,
ended by a crash and by a clean stop. They fail on this commit: the first normal
tick sends 2026-11-03, a day the proxy ran opted out.
…il it ends

Observing OCX_TELEMETRY=0 or DO_NOT_TRACK=1 now also persists an
envOptOutActiveSince marker in telemetry-state.json. While it is set no day is
eligible. The opted-out proxy's clean stop, or the first tick that runs without the
opt-out, closes the period: every day through the closing day stays ineligible and
the marker is cleared. A crashed opted-out proxy therefore costs at most the
restart day and never leaks its opt-out days.

SPONSORS.md no longer says opencodex collects no telemetry.
@lidge-jun lidge-jun added the ci:full Run the four full Ubuntu test shards on this pull request label Nov 8, 2026
@coderabbitai

coderabbitai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Nov 8, 2026
…ending deletion from the reply, and tighten preview and loop start
…d read; refuse connect before prompting when telemetry is off
…board

# Conflicts:
#	devlog/_plan/261107_telemetry_dashboard/060_review_round.md
#	docs-site/src/content/docs/guides/telemetry.md
#	readme/i18n-manifest.json
#	scripts/test-layout/layout-usage.json
#	src/telemetry/consent.ts
#	src/telemetry/routes.ts
#	src/telemetry/sender.ts
#	src/telemetry/state.ts
#	structure/manifest.json
#	structure/telemetry.md
#	tests/config/telemetry-live-save.test.ts
#	tests/fixtures/test-layout-expected.json
#	tests/server/server-telemetry-routes.test.ts
#	tests/usage/usage-telemetry-client.test.ts
…o goat/telemetry-07-personal-rank

# Conflicts:
#	readme/i18n-manifest.json
Base automatically changed from goat/telemetry-06-dashboard to dev November 9, 2026 05:40
…onal-rank

The stack landed as squash commits, so this merge takes dev for every file and re-applies only this
PR's own changes (879a507..e91c0db) on top; no older stack copy is kept.
@lidge-jun

Copy link
Copy Markdown
Owner Author

Maintainer judgment — head 632f59b0a6

1. Value. This implements #8372: an opt-in public profile and personal usage rank built on the anonymous telemetry now on dev (#8300–#8308). No other open PR covers profiles or a leaderboard. The prior art (tokscale) is cited in the design unit devlog/_plan/261109_telemetry_personal_rank/.

2. UX change — intentional and documented. Everything user-visible is new and opt-in:

  • a Public profile & rank card on the dashboard Home, shown while anonymous telemetry is on or while a profile exists;
  • ocx telemetry profile connect|disconnect|status;
  • collector profile and leaderboard endpoints, which are not deployed;
  • a leaderboard section on opencodex.me/dashboard.

Existing flows, defaults, config semantics, the anonymous telemetry payload (contract.ts, payload.ts, sender.ts, aggregate.ts are byte-identical to dev) and the output of existing CLI commands are unchanged. The only change to an existing command is that ocx telemetry --help lists the new profile subcommand. The README privacy line (English and 7 translations), the code-signing pages (en/tr), the telemetry guide, AGENTS_INSTALL.md and SPONSORS.md describe the opt-in.

3. No regression for existing users. Nothing happens until a person connects:

  • Installs without a profile file start no profile loop and send nothing, with telemetry on or off. tests/usage/usage-telemetry-profile-lifecycle.test.ts covers this; it was mutation-checked, and removing the guard makes 2 cases fail.
  • Telemetry-off users (OCX_TELEMETRY=0, DO_NOT_TRACK=1, declined) send nothing, and the card stays hidden for them (sync tests and GUI card test).
  • Connecting never writes config.json or the anonymous telemetry files.
  • The core request path does not reach src/telemetry/ (boundary tests).
  • The one new background activity for every Home viewer is a loopback-only GET /api/telemetry/profile every 60 s. It returns before any aggregation when no profile exists, and nothing leaves the machine.

Evidence on this exact head.

  • Fresh independent final review: APPROVE-WITH-NITS (no P0/P1/P2; three P3s deferred, see below).
  • Separate privacy/security review: PASS-WITH-NITS (its P2 was fixed in 31ed41f70b).
  • Earlier review rounds' P1/P2 findings were each fixed with a test that failed before the fix.
  • Exact-head CI: all 39 checks green.
  • Local checks in isolated homes: typecheck, 291 focused and guard tests, 37 GUI telemetry tests, GUI lint and build, docs-site build, privacy:scan, structure:check, skill:surface:check.

Pre-deploy follow-ups (P3, not blocking):

  • Rate-limit collector DELETE per profile after authentication rather than per client address.
  • Surface a refused-day count in status and the card.
  • Combine the per-period rank queries.

Nothing is deployed.

@lidge-jun
lidge-jun marked this pull request as ready for review November 9, 2026 06:51
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner November 9, 2026 06:51
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T06:54:54.940843Z 632f59b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@lidge-jun
lidge-jun merged commit 8d29015 into dev Nov 9, 2026
45 of 46 checks passed
@lidge-jun
lidge-jun deleted the goat/telemetry-07-personal-rank branch November 9, 2026 06:54

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 632f59b0a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +110 to +111
"INSERT OR IGNORE INTO profile_days (profile_id, day, requests, input, cache_read, cache_write, output, reasoning, tokens) " +
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recheck profile existence atomically when inserting usage

If a submission authenticates before a concurrent disconnect but finishes reading its body afterward, this batch inserts usage for an already-deleted profile. The new tables have no foreign keys, and neither insert checks that the profile still exists. I reproduced this with the collector's SQLite harness: DELETE returned 200, the delayed submission then returned 200, and both usage tables retained rows despite profiles being empty. Subsequent deletion attempts return 401, leaving these rows until retention pruning. Guard both inserts with profile existence inside the atomic batch, or enforce referential integrity, so a completed disconnect cannot be followed by orphaned usage.

Useful? React with 👍 / 👎.

Comment on lines +204 to +206
const result = await deleteProfile(state.secret, options);
if (result.deleted) {
if (deleteTelemetryProfileFile(configDir)) return { status: "deleted" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify the secret before removing the disconnected profile file

When two disconnect requests overlap, one can finish and remove the old file while the other is still awaiting the collector. If the user reconnects before the delayed request completes, this unconditional removal deletes the new profile's file and its only bearer secret, although that new public profile remains on the collector. This also occurs when a disconnect overlaps the scheduler's pending-deletion retry. Re-read the file and compare its secret with state.secret before unlinking, as retryPendingDelete already does.

Useful? React with 👍 / 👎.

Comment on lines +372 to +375
button.addEventListener("click", () => {
metric = parseLeaderboardMetric(button.dataset.tlMetric);
void load();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh the open profile when the ranking metric changes

On the public dashboard, opening a profile and then switching from Tokens to Requests refreshes only the leaderboard. The profile panel retains the ranks fetched with the previous metric, so it can show a different position from that same person's row under the now-selected metric. An in-flight profile lookup can likewise render old-metric ranks after the toggle. Track the currently displayed handle and reload its profile on metric changes, invalidating any previous lookup.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full Run the four full Ubuntu test shards on this pull request enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Opt-in public profile and personal usage rank on top of anonymous telemetry

1 participant