Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/implementation/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,57 @@ The MCP server starts without a synchronous auth gate. Tool calls resolve tokens
| `src/services/filesystem-service.ts` | File system abstraction for testable storage |
| `src/auth/pkce.ts` | PKCE cryptographic primitives |
| `src/services/config.ts` | URL and API token configuration |

## Campaign attribution

The authorization URL is tagged with UTM parameters before it is opened or
printed (`src/commands/login.ts`, via `withUtm` in `src/shared/utm.ts`), so
PostHog can attribute web signups to the CLI.

| Parameter | Value |
| --- | --- |
| `utm_source` | `githits-cli` |
| `utm_medium` | `cli` |
| `utm_campaign` | `cli-login` or `cli-init` |
| `utm_content` | CLI version |

Only the five standard `utm_*` parameters are used, because PostHog
auto-captures exactly that set (`CAMPAIGN_PARAMS` in posthog-js) under the
default configuration. A custom parameter name would need the frontend to extend
the list via `custom_campaign_params`, and would capture nothing until that
config change ships — so the standard slots keep the CLI independent of a web
deploy. No analytics SDK is bundled and the CLI sends no events of its own.

The URL is tagged once, immediately after `buildAuthUrl`, and the tagged value
is used in all three places it is consumed: the browser open, the `--no-browser`
printout, and the "browser did not open" fallback. Tagging only the browser path
would drop attribution for SSH users and for anyone whose browser failed to
launch — the segments most likely to abandon, which would bias the funnel
upward.

Tagging deliberately sits above `buildAuthUrl` rather than inside it. OAuth
parameters are correctness-critical while attribution is best-effort, and the
two should not share a failure mode. `withUtm` never throws and returns the URL
unchanged if it does not parse.

### What this measures

**Auth-page arrival → OAuth completion.** Not command execution → completion.

A UTM parameter only exists once a browser requests the URL, so anyone who runs
the command and never reaches the auth page produces no signal and is absent
from the denominator. Numbers from this funnel are a lower bound on real
command-to-signup conversion, not a conversion rate.

**Signup vs sign-in does not come from the UTM.** The URL is built before the
user authenticates, so at tagging time it is not yet known whether an account
will be created. The UTM carries origin only; the outcome comes from the
frontend's own signup event, and the two are joined at query time on the person.

The web app runs `posthog-js` with `cookieless_mode: "on_reject"`. Users who
reject cookies have no `$anon_distinct_id` and cannot be linked to their signup;
users who never answer the banner have their events dropped entirely. Treat the
result as directional and comparative — `cli-init` vs `cli-login`, or movement
across releases — rather than absolute.

Full analysis: `docs/superpowers/specs/2026-07-27-cli-utm-attribution-design.md`.
Loading