Skip to content

Commit ecbea98

Browse files
committed
docs(claude): document telemetry conventions for new integrations
Add a Telemetry section to CLAUDE.md so future Claude sessions adding new commands or integrations don't miss PostHog event wiring or accidentally break the redaction / identity / Person-property invariants. Covers: - Most new commands need ZERO telemetry code (Action wrapper handles it). - When to add a domain Capture call (lifecycle events only). - Hard rules: no posthog-go imports outside internal/telemetry, no Flush method, no PII in event payloads, no per-loop Capture calls. - How to add a new sensitive flag (denyKeywords vs canonical name). - How project_id auto-attaches and where to update resolveProjectID. - How to smoke-test against a staging PostHog key.f
1 parent b3f561f commit ecbea98

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ When adding a new command:
2424
1. Create the file under `cmd/<group>/`
2525
2. Register it in the group's `NewXxxCommand()` subcommands slice
2626
3. Add it to the manual list in `root.go` Action (the home screen) in alphabetical order
27+
4. Telemetry is automatic — @docs/telemetry.md
28+
2729

2830
## API Client
2931

docs/telemetry.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## Telemetry
2+
3+
The CLI ships PostHog telemetry via `internal/telemetry`. **Most new commands need ZERO telemetry code**`command_invoked` / `command_completed` / `command_failed` are emitted automatically by the Action wrapper in `cmd/root/telemetry.go` and the `main.go` finalizer. Just write your command's `Action` and it will be tracked.
4+
5+
### When you DO need to touch telemetry
6+
7+
Add a `telemetry.Default.Capture(...)` call ONLY when a command represents a discrete domain event distinct from "command invoked/completed". Examples already in the codebase:
8+
9+
- `auth_event{action: login|logout|refresh, method, success}` — login/logout/refresh in `cmd/auth/`, `cmd/root/root.go` Before hook.
10+
- `upgrade_event{from_version, to_version, success, failure_reason}``cmd/upgrade/`.
11+
12+
If you're adding a similar high-value lifecycle event (e.g. `deployment_event`, `vm_event`), follow the same pattern:
13+
```go
14+
if telemetry.Default != nil {
15+
telemetry.Default.Capture("<event_name>", map[string]any{
16+
"action": "<verb>",
17+
"success": true,
18+
// domain-specific props (NO secrets, NO file paths, NO emails)
19+
})
20+
}
21+
```
22+
23+
### Hard rules (do NOT break)
24+
25+
-**Never** import `github.com/posthog/posthog-go` outside `internal/telemetry/`.
26+
-**Never** add a `Flush(timeout)` method to `internal/telemetry/Client`. posthog-go's `Close()` is terminal and there is no non-terminal flush primitive.
27+
-**Never** include user email, file paths, command output, tokens, or any flag value matching the redact denylist (token/password/secret/key/credential/bearer/auth) in event Properties. The Action wrapper auto-redacts flag values via `internal/telemetry/redact.go`; preserve that behavior — if you add a new sensitive flag alias, ensure `internal/telemetry/redact.go::denyKeywords` covers it (canonical name OR alias).
28+
-**Never** call `apiClient.GetUser()` outside of `cmd/auth/login.go`'s `bindIdentityAndCapture`. Identity binding happens once at login, not per-command.
29+
-**Never** persist user_id/email/anything PII to `~/.createos/.identity` beyond `{user_id, aliased_for_user_id}`. The file is intentionally minimal; PostHog Person properties (email, name, signup_date) are sent in-memory via `Client.SetPersonProperties` and never touch disk.
30+
-**Never** emit telemetry from `App.Before` (subcommand name not yet resolved) or `App.After` (cannot see Action error). Use the Action wrapper or the `main.go` finalizer.
31+
-**Never** call `telemetry.Default.Capture` from a hot loop or per-iteration code path. Events are coarse-grained — one per CLI invocation, plus a handful of domain lifecycle events. The free monthly quota is 1M events.
32+
33+
### When adding a new sensitive flag
34+
35+
If you add a flag whose value should be redacted from telemetry (any new auth/secret-bearing flag):
36+
- Pick a name where the canonical OR any alias contains a denylist keyword (`token`, `secret`, etc.) — e.g. `--api-token`, `--ssh-key`. The redact path canonicalizes via `c.Lineage()` so any alias matching the denylist redacts the whole flag.
37+
- If the flag name doesn't naturally contain a denylist keyword (e.g. a credential called `--cookie`), add the new keyword to `internal/telemetry/redact.go::denyKeywords`.
38+
39+
### When adding a new project-scoped command
40+
41+
The Action wrapper auto-attaches `project_id` to events when:
42+
- the command has a `--project` or `--project-id` flag, OR
43+
- a `.createos.json` exists in cwd / parent dirs (`config.FindProjectConfig`).
44+
45+
If your command resolves project ID via a different mechanism (e.g. positional arg only, or a custom env var), update `cmd/root/telemetry.go::resolveProjectID` so the project_id property is set correctly.
46+
47+
### Verifying your changes
48+
49+
After wiring telemetry, smoke test against a staging key:
50+
```bash
51+
go build -ldflags="-X github.com/NodeOps-app/createos-cli/internal/telemetry.PostHogAPIKey=<STAGING_KEY> \
52+
-X github.com/NodeOps-app/createos-cli/internal/telemetry.PostHogHost=https://us.i.posthog.com" \
53+
-o /tmp/createos-test .
54+
/tmp/createos-test <your-command>
55+
# wait ~10s for posthog-go batch flush + 3s Shutdown
56+
# then query PostHog HogQL: SELECT event, properties FROM events WHERE timestamp > now() - INTERVAL 5 MINUTE
57+
```
58+
59+
Run the anti-pattern grep audit from the plan (`docs/superpowers/plans/2026-05-01-posthog-telemetry-plan.md` §Phase 7) before merging.

0 commit comments

Comments
 (0)