Repository navigation
Correlate CLI, server and web PostHog events with one join key #574
Description
Problem
kcap-web (browser), kcap-cli, kcap-web's Worker and kcap-server all publish to the same PostHog project, but each uses a different distinct_id:
| Producer | distinct_id |
|---|---|
| Browser | posthog-js anonymous id, then identify(email) on the org form |
| CLI | random GUID in telemetry-device.json |
| kcap-web Worker | the tenant slug |
| kcap-server | HMAC-SHA256(PostHog:IdSalt, userId) |
So one person moving from website → kcap setup → workspace → recorded sessions is stored as four unrelated people, and "which acquisition channel produces activated workspaces" is unanswerable. www.kurrent.io and capacitor.kurrent.io additionally mint separate web persons, because posthog-js is initialised with no cookie_domain.
Approach
One opaque per-auth-run key (join_id, 32 hex chars, grants nothing) published as a small number of bridge facts, each pairing the key with one identity. No storage and no read-model change: the join is resolved at query time, because every producer already stamps its own distinct_id.
The browser is reached by replacing the CLI's dead-end loopback closing page with a redirect through two /api/cli/return hops and back to the same local page. /api/* is excluded from A/B arm assignment, so a CLI user observes the experiment without joining its sample.
Full design: docs/superpowers/specs/2026-08-16-cli-web-event-correlation-design.md (ships with the kcap-cli PR).
Scope
kcap-web (ships first — the CLI redirects to this route, so it must exist before the CLI ships)
- New
src/pages/api/cli/return.ts+src/server/cli/return.ts: validatej/p, consent gate, capturecli_auth_return, 302 onward -
referrer-policy: no-referrer,cache-control: no-store,x-robots-tag: noindex, nofollowon both hops -
src/server/signup/provision.ts: accept optionaljoinId, stampjoin_idonsignup_reserved/signup_blocked/signup_flagged - Test that
isApiPath("/api/cli/return")is true, so the path can never enter arm assignment - Privacy-policy update
kcap-server
-
CliSetupPayload/RecordCliSetup/UserCliSetupCompletedEventgain optionalJoinId -
PosthogEventMapper.CliSetupstampsjoin_id - README analytics section
kcap-cli (ships last)
- New
SetupJoininCapacitor.Cli.Core/Telemetry/: mint atsetup/loginentry, single-useAcceptwith per-field validation -
LoopbackBrowser: redirect script on the closing page, listener-ownership contract, background drain, response headers -
joinIdon the provisioning request and the cli-setup ping (camelCase on the wire) - README Telemetry section
Requirements worth calling out
- Consent-gated. No web-side bridge is created for a visitor who did not accept cookies: nothing read, nothing captured, nothing passed on. The redirect still happens either way — it is how the person gets back to their terminal. Gate the capture, not the hop.
$device_id, neverdistinct_id, is what travels back to the CLI. Afteridentify(email)thedistinct_idis an email address; the^[A-Za-z0-9_-]{1,64}$check is a fail-closed guarantee against importing one.cli_auth_returnis not a sign-in event. The loopback closing page is written before state validation and before the token exchange, so it means only "a callback arrived with noerror=". Anything measuring "signed in" must pair the key withcli_setup_signin_completedorsignup_reserved.- Opt-out suppresses everything.
KCAP_TELEMETRY=0/DO_NOT_TRACK/ persisted opt-out → no key, no redirect, closing page identical to today. - Query contract. The server-side bridge only covers users whose first
kcap setuphappens after rollout (RecordCliSetupis once-only). The spec lists every population that must be excluded from an activation metric. - Not solved: cross-device. Someone browsing on a phone and installing on a laptop stays two people.
Blocking prerequisite
A ~15-minute browser check before any implementation: posthog-js runs persistence: "localStorage+cookie", and whether the cookie carries $device_id cannot be determined from source. Read it in three states — anonymous, after identify(email), and after withdrawing consent (the gate uses cookie presence as its consent signal). If $device_id is absent from the cookie, the browser half of the design must change shape.
Docs
Per the standing same-PR rule, and because the current wording stops being accurate: the CLI first-run notice and README.md Telemetry section change from anonymous to pseudonymous and state that CLI usage can be associated with a workspace and its creator. Retention for CLI telemetry needs deciding in the same pass.
Activity
linear-code commentedon Aug 17, 2026
AI-1999
Don't let a junk return hop end the wait for the real one
Arm the return-hop wait only after the closing page is written
Guard the shared property bag, now that a background thread writes it
WIP: port the join-key production code onto fresh main (tests still t…
WIP: port the join-key production code onto fresh main (tests still t…
WIP: port the join-key production code onto fresh main (tests still t…
WIP: port the join-key production code onto fresh main (tests still t…
[AI-1999] Correlate CLI, web and server analytics: the CLI half (#592)