Skip to content

Correlate CLI, server and web PostHog events with one join key #574

Closed

Description

@stktung

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: validate j/p, consent gate, capture cli_auth_return, 302 onward
  • referrer-policy: no-referrer, cache-control: no-store, x-robots-tag: noindex, nofollow on both hops
  • src/server/signup/provision.ts: accept optional joinId, stamp join_id on signup_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 / UserCliSetupCompletedEvent gain optional JoinId
  • PosthogEventMapper.CliSetup stamps join_id
  • README analytics section

kcap-cli (ships last)

  • New SetupJoin in Capacitor.Cli.Core/Telemetry/: mint at setup/login entry, single-use Accept with per-field validation
  • LoopbackBrowser: redirect script on the closing page, listener-ownership contract, background drain, response headers
  • joinId on 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, never distinct_id, is what travels back to the CLI. After identify(email) the distinct_id is an email address; the ^[A-Za-z0-9_-]{1,64}$ check is a fail-closed guarantee against importing one.
  • cli_auth_return is 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 no error=". Anything measuring "signed in" must pair the key with cli_setup_signin_completed or signup_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 setup happens after rollout (RecordCliSetup is 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions