Stop CI counting as plugin downloads, and see who abandons sign-in #191

Merged
cruelacid merged 1 commit from worktree-downloads-truth into main 2026-09-27 08:14:02 +01:00
Owner

Task: NEC-133

The KPI dashboard showed 212 plugin downloads against five accounts, all ours. This fixes why, and adds the one measurement that would show whether outsiders reach the sign-in page and give up.

Where the downloads came from

The e2e job's compat step ("The previous plugin release against this server") downloaded main.js, manifest.json and styles.css from the latest public GitHub release on every run. GitHub counts each fetch as a download, and the community store shows GitHub's manifest.json count. From the step's own logs (testing this server against plugin X):

Release CI GitHub total
0.1.0–0.1.4 12 26
0.1.5 156 165
0.2.0 6 7
0.2.1 25 (step executions since its release) 36

No outside address has ever requested a sign-in code, so nothing suggests sign-up is broken; there has been almost no outside traffic.

Changes

  • ci.yml
    • The latest tag comes from git ls-remote, and the three files are cached per tag with actions/cache. A release is fetched once, not once per run.
    • The REST API it replaces also rate-limited the shared runner IP: 23 runs on 23–24 Sep got 403 and passed as "compatibility NOT exercised".
    • A release that exists but can't be fetched now fails the job. The only allowed skip is "nothing published yet".
    • wire-structure.test.ts asserts all three properties.
  • KPI store and dashboard (clconsole)
    • New downloads_ci table with CI's count per release, and a view v_downloads_excluding_ci.
    • A "Excluding our CI" tile beside the store figure.
    • Downloads are read hourly instead of every 15 minutes, since Obsidian refreshes them about daily.
  • /api/kpi on identity
    • Now reports every sign-in flow, finished or not, with its stage: started → page_opened → provider / email_code → completed.
    • Every stage comes from columns the routes already write. email_code comes from mail_log, not the plugin's pre-filled address.
    • page_opened is a lower bound: the page asks for passkey options on load for autofill, so an unfinished passkey attempt can't be told apart from a page that was only seen.
    • The collector's upsert only ever moves a flow forward.
  • Docs: docs/security-model.md now says unfinished sign-ins are counted.

Verified locally

  • pnpm test, pnpm -r typecheck, pnpm lint and build-dashboard --check all pass.
  • Against a real identity service and shard with abandoned flows at each stage, running over a store created by main's schema:
    • The existing row was backfilled as completed.
    • Every stage was reported correctly.
    • A stale reading could not regress a completed sign-in.
    • All 38 panel queries ran through Grafana's read-only role.
  • The ls-remote lookup returns 0.2.1 for the real repo, and fails with no output for a missing one.
  • Mutation checks, each confirmed to fail: reading the stage from auth_flows.email, ranking autofill above a provider choice, and dropping the forward-only guard (in both the unit test and the database).
  • Not verifiable locally: the cache hit/miss behaviour, which needs this PR's run and a re-run.

Changelog

NONE

🤖 Generated with Claude Code

https://claude.ai/code/session_01MY63BZ4UVtMHFcADNr1jFg

Task: NEC-133 The KPI dashboard showed 212 plugin downloads against five accounts, all ours. This fixes why, and adds the one measurement that would show whether outsiders reach the sign-in page and give up. ## Where the downloads came from The e2e job's compat step ("The previous plugin release against this server") downloaded `main.js`, `manifest.json` and `styles.css` from the latest public GitHub release on every run. GitHub counts each fetch as a download, and the community store shows GitHub's `manifest.json` count. From the step's own logs (`testing this server against plugin X`): | Release | CI | GitHub total | |---|---|---| | 0.1.0–0.1.4 | 12 | 26 | | 0.1.5 | 156 | 165 | | 0.2.0 | 6 | 7 | | 0.2.1 | 25 (step executions since its release) | 36 | No outside address has ever requested a sign-in code, so nothing suggests sign-up is broken; there has been almost no outside traffic. ## Changes - **ci.yml** - The latest tag comes from `git ls-remote`, and the three files are cached per tag with `actions/cache`. A release is fetched once, not once per run. - The REST API it replaces also rate-limited the shared runner IP: 23 runs on 23–24 Sep got 403 and passed as "compatibility NOT exercised". - A release that exists but can't be fetched now fails the job. The only allowed skip is "nothing published yet". - `wire-structure.test.ts` asserts all three properties. - **KPI store and dashboard (clconsole)** - New `downloads_ci` table with CI's count per release, and a view `v_downloads_excluding_ci`. - A "Excluding our CI" tile beside the store figure. - Downloads are read hourly instead of every 15 minutes, since Obsidian refreshes them about daily. - **`/api/kpi` on identity** - Now reports every sign-in flow, finished or not, with its stage: `started` → `page_opened` → `provider` / `email_code` → `completed`. - Every stage comes from columns the routes already write. `email_code` comes from `mail_log`, not the plugin's pre-filled address. - `page_opened` is a lower bound: the page asks for passkey options on load for autofill, so an unfinished passkey attempt can't be told apart from a page that was only seen. - The collector's upsert only ever moves a flow forward. - **Docs:** `docs/security-model.md` now says unfinished sign-ins are counted. ## Verified locally - `pnpm test`, `pnpm -r typecheck`, `pnpm lint` and `build-dashboard --check` all pass. - Against a real identity service and shard with abandoned flows at each stage, running over a store created by `main`'s schema: - The existing row was backfilled as completed. - Every stage was reported correctly. - A stale reading could not regress a completed sign-in. - All 38 panel queries ran through Grafana's read-only role. - The `ls-remote` lookup returns `0.2.1` for the real repo, and fails with no output for a missing one. - Mutation checks, each confirmed to fail: reading the stage from `auth_flows.email`, ranking autofill above a provider choice, and dropping the forward-only guard (in both the unit test and the database). - Not verifiable locally: the cache hit/miss behaviour, which needs this PR's run and a re-run. ## Changelog NONE 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MY63BZ4UVtMHFcADNr1jFg
Stop CI counting as plugin downloads, and see who abandons sign-in
All checks were successful
Release note / release-note (pull_request) Successful in 13s
CI / build (pull_request) Successful in 4m39s
CI / e2e (pull_request) Successful in 5m27s
CI / promote (pull_request) Has been skipped
Deploy site / deploy (push) Successful in 48s
CI / build (push) Successful in 4m54s
CI / e2e (push) Successful in 5m23s
CI / promote (push) Successful in 30s
79a10b4cd0
The store showed 212 downloads against five accounts, all ours. About 200
of the first 230 were CI: the compat step fetched the latest public release
from GitHub on every run, and GitHub counts each fetch as a download, which
is the number the store shows. Counted from the step's own logs.

- ci.yml finds the release with git ls-remote and caches its three files
  per tag, so a release is fetched once, not per run. The REST API it used
  also rate-limited the shared runner IP: 23 runs on 23-24 Sep got 403 and
  passed as "compatibility NOT exercised". A release that exists but cannot
  be fetched now fails the job; only "nothing published yet" skips.
- The KPI store records CI's share per release (downloads_ci), and the
  dashboard shows the store figure beside the figure without it. Downloads
  are read hourly, since Obsidian refreshes them about daily.
- /api/kpi now reports every sign-in flow, finished or not, with how far it
  got: started, page seen (passkey autofill, so a lower bound), provider
  chosen, email code sent, completed. All from columns the routes already
  write; the email-code stage comes from mail_log, not the plugin's
  pre-filled address. The collector's upsert only moves a flow forward.

Task: NEC-133

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MY63BZ4UVtMHFcADNr1jFg
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Nectenda/nectenda!191
No description provided.