Skip to content

Commit 5eb0851

Browse files
authored
Merge pull request #118 from PostHog/audit
Audit skill
2 parents b2fe1e5 + 2a22af3 commit 5eb0851

9 files changed

Lines changed: 539 additions & 1 deletion

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ routeTree.gen.ts
7373
# Build artifacts
7474
dist/
7575

76+
# Audit skill runtime ledger
77+
.posthog-audit-checks.json
78+
posthog-audit-report.md
79+
7680
# Misc
7781
*.pem
7882
*.key

scripts/lib/skill-generator.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ function expandSkillGroups(config, configDir) {
146146
_template: template,
147147
_sharedDocs: sharedDocs,
148148
_examplePaths: [...baseExamplePaths, ...normalizeExamplePaths(variation.example_paths)],
149+
_references: group.references || null,
149150
_group: key,
150151
});
151152
}
@@ -485,16 +486,29 @@ async function generateSkill({
485486
}
486487

487488
// Copy local markdown references from a source references/ directory, if present.
489+
// Group config injects a shared `preamble`; per-file `next_step` frontmatter drives continuation links.
488490
const sourceReferencesDir = path.join(configDir, 'skills', ...skill._group.split('/'), 'references');
489491
if (fs.existsSync(sourceReferencesDir)) {
490492
const localReferences = fs.readdirSync(sourceReferencesDir, { withFileTypes: true })
491493
.filter(entry => entry.isFile() && entry.name.endsWith('.md'));
492494

495+
const refsConfig = skill._references || {};
496+
493497
for (const reference of localReferences) {
494498
const sourcePath = path.join(sourceReferencesDir, reference.name);
495-
const content = fs.readFileSync(sourcePath, 'utf8');
499+
const parsed = matter(fs.readFileSync(sourcePath, 'utf8'));
500+
const nextFile = parsed.data.next_step;
501+
let content = parsed.content.replace(/^\n+/, '');
496502
const headingMatch = content.match(/^#\s+(.+)$/m);
497503

504+
if (nextFile) {
505+
if (refsConfig.preamble && headingMatch) {
506+
const headingEnd = content.indexOf(headingMatch[0]) + headingMatch[0].length;
507+
content = content.slice(0, headingEnd) + '\n\n' + refsConfig.preamble + content.slice(headingEnd);
508+
}
509+
content += `\n\n---\n\n**Upon completion, continue with:** [${nextFile}](${nextFile})`;
510+
}
511+
498512
fs.writeFileSync(
499513
path.join(referencesDir, reference.name),
500514
content,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
type: docs-only
2+
template: description.md
3+
description: Audit an existing PostHog integration for correctness and best practices
4+
tags: [best-practices]
5+
references:
6+
preamble: "**Read ONLY this file.** Do not read any other reference file until this one tells you to."
7+
shared_docs:
8+
- https://posthog.com/docs/getting-started/identify-users.md
9+
- https://posthog.com/docs/product-analytics/best-practices.md
10+
variants:
11+
- id: all
12+
display_name: PostHog audit
13+
tags: [best-practices]
14+
docs_urls: []
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PostHog Audit
2+
3+
This skill audits an existing PostHog integration for **data integrity** in event capture and identification. **Read-only** — the only file you create is the final audit report.
4+
5+
Perform the checks described in the referenced skills and only the events referenced in the skills.
6+
7+
## Workflow
8+
9+
The audit runs as a 5-step chain: Installation (SDK + version) → init correctness → identification → event capture → report. Each step file ends with a pointer to the next. Follow them in the order they are written. You must resolve them in order before any source-tree exploration.
10+
11+
The audit ledger is already seeded with the 10 pending checks. Use `mcp__wizard-tools__audit_resolve_checks` to patch each one as you finish it.
12+
13+
**Start by reading the path relative to this file at `references/1-version.md`.** Do not Glob, ls, or find the skill directory. Do not preload future steps. Do not re-read a step file once you've moved past it. Do not re-read SKILL.md.
14+
15+
`ToolSearch` is only for loading a tool by exact name when the SDK has it deferred (e.g. `select:Grep`). Do **not** use it to browse for other tools — every tool the audit needs (`Glob`, `Grep`, `Read`, `Write`, `Bash`, and the named `mcp__wizard-tools__audit_*` tools) is already named in this skill.
16+
17+
**Do not call `TodoWrite`.** The audit doesn't track its own task list — progress comes from the audit ledger plus `[STATUS]` lines.
18+
19+
## Live activity — `[STATUS]`
20+
21+
The "Working on …" banner reads from `[STATUS]` lines you emit in plain text. Whenever you start a new sub-step, write a line like:
22+
23+
```
24+
[STATUS] Scanning manifests
25+
```
26+
27+
The wizard intercepts these and updates the spinner. Use them freely — they are cheap. Each step file lists the exact `[STATUS]` strings to emit at each sub-step.
28+
29+
## Audit checks ledger
30+
31+
The ledger lives at `.posthog-audit-checks.json` and is rendered live in the "Audit plan" tab. It is owned by MCP tools — **never `Write` this file directly**:
32+
33+
- `mcp__wizard-tools__audit_resolve_checks({ updates })` — patch one or more checks by `id`. Each `update` is `{ id, status, file?, details? }`. Batch updates from the same step into a single call.
34+
35+
All audit ledger calls are atomic and serialize internally — **concurrent calls from parallel subagents cannot lose updates**, so feel free to fan out runtime checks across `Task` subagents when a step says so.
36+
37+
### Check entry shape
38+
39+
- `id` — stable kebab-case slug. Reuse the existing seeded ids exactly when calling `audit_resolve_checks`.
40+
- `area` — short group name. The current core workflow uses `Installation`, `Identification`, and `Event Capture`.
41+
- `label` — short human name.
42+
- `status``pending` | `pass` | `error` | `warning` | `suggestion`.
43+
- `file` — optional `path:line` for findings tied to a location.
44+
- `details` — optional one-line explanation.
45+
46+
After the report is written (Step 5), delete `.posthog-audit-checks.json`.
47+
48+
## Severity levels
49+
50+
- `error`: Must fix. Broken functionality, data corruption, or security issue.
51+
- `warning`: Should fix. Pattern that causes subtle bugs or data-quality problems.
52+
- `suggestion`: Nice to have. Best-practice improvement.
53+
54+
## Key principles
55+
56+
- **Read-only**: Do not edit project source files. The only file you create is the audit report.
57+
- **Evidence-based**: Reference specific `file:line` for every non-pass finding.
58+
- **Actionable**: Every finding states what to fix and how.
59+
60+
## Abort statuses
61+
62+
Report abort states with `[ABORT]` prefixed messages. The wizard catches these and terminates the run — do not halt yourself.
63+
- No PostHog SDK found
64+
65+
## Framework guidelines
66+
67+
{commandments}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
next_step: 2-init.md
3+
---
4+
5+
# Step 1 — SDK installed + SDK up-to-date
6+
7+
This step is intentionally narrow. It runs **before any other project work**. Resolve exactly two checks: `sdk-installed` and `sdk-up-to-date`. **Do not** read source code, locate init sites, look at `.env*` files, or scan for identify/capture call sites in this step — that all belongs to later steps.
8+
9+
## Status
10+
11+
Emit:
12+
13+
```
14+
[STATUS] Scanning manifests
15+
[STATUS] Checking SDK version
16+
```
17+
18+
## Action
19+
20+
### a. Find the PostHog SDK
21+
22+
`Glob` for the project's dependency manifests across every language PostHog ships an SDK for. The full list:
23+
24+
- `package.json` — npm / pnpm / yarn (Node, web, React, Next.js, Nuxt, Vue, Svelte, Angular, React Native, Expo)
25+
- `requirements.txt`, `pyproject.toml`, `Pipfile`, `setup.py` — Python (Django, Flask, FastAPI, etc.)
26+
- `Gemfile` — Ruby / Ruby on Rails
27+
- `composer.json` — PHP / Laravel
28+
- `go.mod` — Go
29+
- `build.gradle`, `build.gradle.kts`, `pom.xml` — Java / Android
30+
- `Podfile`, `Package.swift` — iOS / Swift
31+
- `pubspec.yaml` — Flutter / Dart
32+
- `*.csproj` — .NET
33+
- `mix.exs` — Elixir
34+
35+
Read enough of them to identify which PostHog SDK the project uses, what version, and what framework it sits on top of.
36+
37+
If no PostHog SDK is anywhere in the project, emit `[ABORT] No PostHog SDK found` and stop. The wizard catches `[ABORT]` and terminates the run.
38+
39+
### b. Install the matching integration skill
40+
41+
Once you know the SDK + framework, install the matching integration skill so the rest of the audit has framework-specific install docs to reference instead of guessing:
42+
43+
1. Call `mcp__wizard-tools__load_skill_menu({ category: "integration" })` once to list available integration skill IDs.
44+
2. Call `mcp__wizard-tools__install_skill({ skillId: "<id>" })` with the **single** ID that matches the framework you detected. Pick one — do not install multiple.
45+
46+
If no integration skill matches the framework, skip this step. Step 2 will fall back to general framework knowledge.
47+
48+
### c. Check latest published version
49+
50+
For each detected SDK, run `Bash` once to look up the latest published version. Use the command that matches the SDK's registry:
51+
52+
- **npm** (JS/TS, Node, React, Next.js, Nuxt, Vue, Svelte, Angular, React Native, Expo): `npm view <pkg> version`
53+
- **PyPI** (Python): `pip index versions <pkg>` (or `pip show <pkg>` if `index` is unavailable)
54+
- **RubyGems** (Ruby / Rails): `gem search ^<pkg>$ -r`
55+
- **Packagist** (PHP / Laravel): `composer show <pkg> --latest --available --format=json`
56+
- **Go modules** (Go): `curl -s https://proxy.golang.org/<module>/@latest` (returns JSON with the latest `Version`)
57+
- **Maven Central** (Java / Android): `curl -s "https://search.maven.org/solrsearch/select?q=g:<group>+AND+a:<artifact>&rows=1&wt=json"` and read `.response.docs[0].latestVersion`
58+
- **CocoaPods** (iOS / Swift): `pod search <pkg>` (or check `https://cdn.cocoapods.org/all_pods_versions_<x>_<y>_<z>.txt` for the spec mirror)
59+
- **Swift Package Manager** (Swift): `gh release list --repo posthog/posthog-ios --limit 1` (SwiftPM resolves from GitHub tags)
60+
- **pub.dev** (Flutter / Dart): `curl -s https://pub.dev/api/packages/<pkg> | jq -r .latest.version`
61+
- **NuGet** (.NET): `curl -s https://api.nuget.org/v3-flatcontainer/<pkg>/index.json | jq -r '.versions[-1]'`
62+
- **Hex** (Elixir): `mix hex.info <pkg>`
63+
64+
## Resolution rules
65+
66+
`sdk-installed`:
67+
- `pass`: at least one PostHog SDK in a manifest. Record SDK + version in `details`.
68+
69+
`sdk-up-to-date`:
70+
- `pass`: at the latest minor.
71+
- `suggestion`: patch-only behind.
72+
- `warning`: more than one minor behind.
73+
- `error`: one or more major versions behind.
74+
75+
## Resolve
76+
77+
Single call to `mcp__wizard-tools__audit_resolve_checks` with two updates and **nothing else**:
78+
79+
```
80+
{
81+
"updates": [
82+
{ "id": "sdk-installed", "status": "pass", "details": "<sdk>@<version>" },
83+
{ "id": "sdk-up-to-date", "status": "pass|suggestion|warning|error", "details": "installed <v>, latest <v>" }
84+
]
85+
}
86+
```
87+
88+
Do not include `init-correct` in this call — it's resolved in Step 2.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
next_step: 3-identification.md
3+
---
4+
5+
# Step 2 — Init correctness
6+
7+
This step resolves exactly one check: `init-correct`. Manifests and SDK versions are already resolved (Step 1). Identification call sites belong to Step 3 and event-capture call sites to Step 4 — do not scan for them here.
8+
9+
## Status
10+
11+
Emit:
12+
13+
```
14+
[STATUS] Locating PostHog initialization
15+
```
16+
17+
## Action
18+
19+
Locate the project's PostHog init by issuing whatever `Grep` and `Read` calls are needed in parallel. Confirm the init exists, runs in the right runtime for the detected SDK + framework, and sources its token from an env variable (not hardcoded). Also check `.env*` files to confirm the token env var is actually set. Reverse-proxy / `api_host` configuration belongs to Step 4 — don't evaluate it here.
20+
21+
Use the detected SDK + framework from Step 1 to know what to look for: the canonical init filename, runtime, and shape vary by framework. If the host project already ships a PostHog integration skill, use that as the source of truth. Skills are typically under `.claude/skills/`; if that directory doesn't exist (some projects keep skills under `agents/skills/`, plain `skills/`, etc.), discover any candidates with one `Glob` pattern: `**/skills/**/SKILL.md`. Read the matching skill before judging.
22+
23+
When no integration skill is available, rely on general framework knowledge — and stay conservative on `init-correct` (prefer `warning` over `error` when the convention is unclear).
24+
25+
## Resolution rules
26+
27+
`init-correct`:
28+
- `pass`: init present, env-sourced token, runtime-appropriate location.
29+
- `error`: init missing, hardcoded token, or wrong runtime (e.g. server-only init for a browser-side framework).
30+
- `warning`: init present but in a non-canonical location for the framework.
31+
32+
## Resolve
33+
34+
Single call to `mcp__wizard-tools__audit_resolve_checks` with one update:
35+
36+
```
37+
{
38+
"updates": [
39+
{ "id": "init-correct", "status": "pass|error|warning", "file": "<path:line>", "details": "..." }
40+
]
41+
}
42+
```
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
next_step: 4-event-capture.md
3+
---
4+
5+
# Step 3 — Identification
6+
7+
This step resolves four identification checks **in parallel**, one subagent per check:
8+
9+
- `identify-stable-distinct-id`
10+
- `identify-not-late`
11+
- `cross-runtime-distinct-id`
12+
- `identify-reset-on-logout`
13+
14+
Each subagent owns its own grep, reads, evaluates its single rule, and emits one `audit_resolve_checks` call with one update. The ledger's mutex serializes concurrent writes — there's no race.
15+
16+
## Status
17+
18+
Emit before dispatching:
19+
20+
```
21+
[STATUS] Auditing identification
22+
```
23+
24+
## Action — dispatch four subagents in one message
25+
26+
Make **four `Task` tool calls in a single message** so they run concurrently. Wait for all four to return, then continue to `4-event-capture.md`. Do not run any other tools between dispatch and the next step.
27+
28+
The bundled `identify-users.md` reference holds PostHog's authoritative guidance on `distinct_id`, `identify()` ordering, and cross-runtime identity. It's typically at `.claude/skills/audit/references/identify-users.md`; if that path doesn't exist, discover it with `Glob` `**/skills/audit/references/identify-users.md`. Each subagent reads it once before judging.
29+
30+
### Task A — `identify-stable-distinct-id`
31+
32+
`description`: `Audit identify-stable-distinct-id`
33+
34+
`prompt`:
35+
```
36+
You are an audit subagent. Resolve exactly one rule and return: identify-stable-distinct-id.
37+
38+
Read this skill's bundled `identify-users.md` reference once (typically `.claude/skills/audit/references/identify-users.md`; otherwise discover with `Glob` `**/skills/audit/references/identify-users.md`).
39+
40+
Run **one** Grep: `posthog\.identify\(`. Read each file that contains a hit, once. Inspect the first argument passed to identify().
41+
42+
Rule:
43+
- distinct_id must be a stable identifier (auth user id, account id), not a session UUID, ephemeral cookie, or device-only id.
44+
- pass: sources from authenticated user (session.user.id, auth.uid(), etc.)
45+
- error: sources from a session, request, or device id that resets
46+
- warning: source unclear — flag for human review
47+
48+
Emit one `mcp__wizard-tools__audit_resolve_checks` call with a single update for id `identify-stable-distinct-id`, including `file` (path:line) and `details` (one-line explanation). Return when the call completes. Do not write the audit report.
49+
```
50+
51+
### Task B — `identify-not-late`
52+
53+
`description`: `Audit identify-not-late`
54+
55+
`prompt`:
56+
```
57+
You are an audit subagent. Resolve exactly one rule and return: identify-not-late.
58+
59+
Read this skill's bundled `identify-users.md` reference once (typically `.claude/skills/audit/references/identify-users.md`; otherwise discover with `Glob` `**/skills/audit/references/identify-users.md`).
60+
61+
Run **two** Greps in parallel:
62+
- `posthog\.identify\(` — where identity is established
63+
- `posthog\.capture\(|getFeatureFlag\(|isFeatureEnabled\(` — where captures and flag evals happen
64+
65+
Read each file that contains a hit, once. Compare the timing/ordering of identify() against the surrounding capture / flag-eval calls.
66+
67+
Rule:
68+
- identify() must be called before any posthog.capture for that user, and before any feature-flag eval depending on user identity.
69+
- pass: identify runs at session start / right after login. Captures and flag evals come after.
70+
- warning: identify runs lazily (e.g. settings-page mount), so early captures and flag evals are anonymous.
71+
72+
Emit one `mcp__wizard-tools__audit_resolve_checks` call with a single update for id `identify-not-late`, including `file` (path:line of the identify call) and `details` (one-line explanation). Return when the call completes. Do not write the audit report.
73+
```
74+
75+
### Task C — `cross-runtime-distinct-id`
76+
77+
`description`: `Audit cross-runtime-distinct-id`
78+
79+
`prompt`:
80+
```
81+
You are an audit subagent. Resolve exactly one rule and return: cross-runtime-distinct-id.
82+
83+
Read this skill's bundled `identify-users.md` reference once (typically `.claude/skills/audit/references/identify-users.md`; otherwise discover with `Glob` `**/skills/audit/references/identify-users.md`).
84+
85+
Run **one** Grep: `posthog\.init\(|new PostHog\(|posthog\.Posthog\(|Posthog\(` — locate every PostHog initialization across runtimes. Read each file that contains a hit, once. Determine whether both client and server runtimes initialize PostHog, and if so, how distinct_id flows between them.
86+
87+
Rule:
88+
- If both client and server runtimes call PostHog, the same distinct_id must be used on both sides for the same user.
89+
- pass: server-side captures source the client's distinct_id (cookie, session token, or explicit hand-off).
90+
- error: server-side captures use a different identifier scheme.
91+
- Skip (`pass` with details: "single runtime"): only one runtime initializes PostHog.
92+
93+
Emit one `mcp__wizard-tools__audit_resolve_checks` call with a single update for id `cross-runtime-distinct-id`, including `file` (path:line of the most relevant init or capture site) and `details` (one-line explanation). Return when the call completes. Do not write the audit report.
94+
```
95+
96+
### Task D — `identify-reset-on-logout`
97+
98+
`description`: `Audit identify-reset-on-logout`
99+
100+
`prompt`:
101+
```
102+
You are an audit subagent. Resolve exactly one rule and return: identify-reset-on-logout.
103+
104+
Read this skill's bundled `identify-users.md` reference once (typically `.claude/skills/audit/references/identify-users.md`; otherwise discover with `Glob` `**/skills/audit/references/identify-users.md`).
105+
106+
Locate logout, sign-out, and account-switching flows by issuing whatever `Grep` and `Read` calls are needed in parallel. Determine whether those flows clear PostHog state with `posthog.reset()`.
107+
108+
Rule:
109+
- Logout or account-switching flows should call `posthog.reset()`. Without a reset, when user B logs in on the same device after user A, PostHog's anonymous ID is shared and the next `identify()` can merge both accounts into one person.
110+
- pass: every detected logout/account-switch flow calls `posthog.reset()`.
111+
- error: a logout/account-switch flow is missing `posthog.reset()`.
112+
- Skip (`pass` with details: "no logout/account-switch flow found"): no detectable logout/account-switch flow exists.
113+
- note: `posthog.reset(true)` is valid when a completely clean device ID reset is required.
114+
115+
Emit one `mcp__wizard-tools__audit_resolve_checks` call with a single update for id `identify-reset-on-logout`, including `file` (path:line of the most relevant logout or reset site) and `details` (one-line explanation). Return when the call completes. Do not write the audit report.
116+
```

0 commit comments

Comments
 (0)