Count a person's devices once, not once per organisation #263

Merged
cruelacid merged 1 commit from worktree-kpi-devices into main 2026-10-06 02:27:55 +01:00
Owner

Task: NEC-223

The KPI leaderboard showed a paying customer with 6 active devices when they have 2, a Windows desktop and an Android phone, because they're in three organisations.

Why

  • Each organisation a person belongs to is a separate seat (users row), and each seat has its own devices rows.
  • The KPI views added device counts up over seats. That happened in three places: the leaderboard (where it also feeds the score), the funnel's "Two or more devices" step, and the "Devices per person" panel.

Fix

  • Shard /api/kpi: each seat now carries deviceIds and activeDeviceIds30d, as sha256 truncated to 16 hex characters. The plugin's device id is per install (getOrCreateDeviceId), so the same install gives the same value in every organisation.
  • KPI store: a new view v_person_devices counts distinct ids per person, and all three places use it.
  • Fallback: snapshots from before this change carry no ids, so the view uses the largest single seat's count for them. That can undercount, but never doubles.

Verified

  • pnpm test, typecheck, lint and build-dashboard --check all pass.
  • Shard test: one install signed in to two organisations reports the same hashed id in both seats, and the raw device id never appears in the payload. Returning raw ids makes that test fail.
  • Postgres, upgrading from main's schema, with a person who has 2 devices in 3 organisations:
    • The old views read 6.
    • After the upgrade, the old snapshot reads 2 via the fallback.
    • A snapshot with ids reads 2 devices, 1 of them active.
    • Putting sum back into the view reads 6 again.

Changelog

NONE

🤖 Generated with Claude Code

https://claude.ai/code/session_01MY63BZ4UVtMHFcADNr1jFg

Task: NEC-223 The KPI leaderboard showed a paying customer with **6 active devices** when they have 2, a Windows desktop and an Android phone, because they're in three organisations. ## Why - Each organisation a person belongs to is a separate seat (`users` row), and each seat has its own `devices` rows. - The KPI views added device counts up over seats. That happened in three places: the leaderboard (where it also feeds the score), the funnel's "Two or more devices" step, and the "Devices per person" panel. ## Fix - **Shard `/api/kpi`:** each seat now carries `deviceIds` and `activeDeviceIds30d`, as sha256 truncated to 16 hex characters. The plugin's device id is per install (`getOrCreateDeviceId`), so the same install gives the same value in every organisation. - **KPI store:** a new view `v_person_devices` counts distinct ids per person, and all three places use it. - **Fallback:** snapshots from before this change carry no ids, so the view uses the largest single seat's count for them. That can undercount, but never doubles. ## Verified - `pnpm test`, typecheck, lint and `build-dashboard --check` all pass. - **Shard test:** one install signed in to two organisations reports the same hashed id in both seats, and the raw device id never appears in the payload. Returning raw ids makes that test fail. - **Postgres, upgrading from `main`'s schema**, with a person who has 2 devices in 3 organisations: - The old views read 6. - After the upgrade, the old snapshot reads 2 via the fallback. - A snapshot with ids reads 2 devices, 1 of them active. - Putting `sum` back into the view reads 6 again. ## Changelog NONE 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MY63BZ4UVtMHFcADNr1jFg
Count a person's devices once, not once per organisation
Some checks failed
Release note / release-note (pull_request) Successful in 18s
CI / build (pull_request) Failing after 1m51s
CI / promote (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
558502a542
The leaderboard showed a paying customer with 6 active devices: a desktop
and a phone, signed in to three organisations. Each organisation is its own
seat with its own device rows, and the KPI views added seats up. The same
sum was in the funnel's "two or more devices" step and the devices-per-
person panel.

The shard's /api/kpi now reports each seat's device ids, hashed. The
plugin's device id is per install and the same in every organisation, so
the new v_person_devices view counts distinct ids per person. Snapshots
from before this carry no ids and use the largest single seat's count,
which can undercount but never doubles.

Task: NEC-223

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MY63BZ4UVtMHFcADNr1jFg
cruelacid force-pushed worktree-kpi-devices from 558502a542
Some checks failed
Release note / release-note (pull_request) Successful in 18s
CI / build (pull_request) Failing after 1m51s
CI / promote (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
to cba1d8bd98
Some checks failed
Release note / release-note (pull_request) Successful in 18s
CI / build (pull_request) Failing after 1m39s
CI / promote (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
2026-10-06 02:04:30 +01:00
Compare
cruelacid force-pushed worktree-kpi-devices from cba1d8bd98
Some checks failed
Release note / release-note (pull_request) Successful in 18s
CI / build (pull_request) Failing after 1m39s
CI / promote (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
to e50db43048
All checks were successful
Release note / release-note (pull_request) Successful in 16s
CI / build (pull_request) Successful in 6m4s
CI / e2e (pull_request) Successful in 9m21s
CI / promote (pull_request) Has been skipped
2026-10-06 02:07:13 +01:00
Compare
cruelacid force-pushed worktree-kpi-devices from e50db43048
All checks were successful
Release note / release-note (pull_request) Successful in 16s
CI / build (pull_request) Successful in 6m4s
CI / e2e (pull_request) Successful in 9m21s
CI / promote (pull_request) Has been skipped
to c9fce3a073
All checks were successful
Release note / release-note (pull_request) Successful in 16s
CI / build (pull_request) Successful in 6m27s
CI / e2e (pull_request) Successful in 9m20s
CI / promote (pull_request) Has been skipped
CI / build (push) Successful in 6m24s
CI / e2e (push) Successful in 9m31s
CI / promote (push) Successful in 35s
2026-10-06 02:17:37 +01:00
Compare
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!263
No description provided.