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
5 changes: 4 additions & 1 deletion context/agents/integration-v2/capture.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ user the way the identify docs describe, not the event.
## How you know you succeeded

The meaningful user actions across the app have capture calls that fire on the
real action, not on page load, each one attributable to the user who took it, and
real action, not on page load — and where the action waits on a server, in the
branch that runs once the response confirms success, never before it. Each one is
attributable to the user who took it, server-side business events that are not
updating the person pass `$process_person_profile: false`, and
`.posthog-wizard-cache/.posthog-events.json` lists the events you instrumented.

You do not run builds, linters, or tests — the review task verifies the whole integration after you; your edits just need to be right by reading.
4 changes: 3 additions & 1 deletion context/commandments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ commandments:
# PostHog-specific
- For feature flags, use useFeatureFlagEnabled() or useFeatureFlagPayload() hooks - they handle loading states and external sync automatically
- Add analytics capture in event handlers where user actions occur, NOT in useEffect reacting to state changes
- When the handler awaits a server call, capture in the success branch after it resolves, never at the top of the handler - a request the server rejects must not count as a success. When the outcome only ever arrives as state (useActionState, a mutation hook's isSuccess), capture inside the server action instead, or off that success state - the one legitimate exception to the useEffect rule above.
# General React patterns (react.dev/learn/you-might-not-need-an-effect)
- Do NOT use useEffect for data transformation - calculate derived values during render instead
- Do NOT use useEffect to respond to user events - put that logic in the event handler itself
Expand Down Expand Up @@ -60,7 +61,8 @@ commandments:
javascript_node:
- posthog-node is the Node.js server-side SDK package name; posthog-js is browser-only, so use posthog-node on the server instead
- 'Include enableExceptionAutocapture: true in the PostHog constructor options'
- Add posthog.capture() calls in route handlers for meaningful user actions – every route that creates, updates, or deletes data should track an event with contextual properties
- Add posthog.capture() calls in route handlers for meaningful user actions – every route that creates, updates, or deletes data should track an event with contextual properties, captured after the write succeeds rather than on entry to the handler
- 'Backend SDKs process a person profile on every capture carrying a distinct id, and the runtime metadata they attach ($os, $lib) overwrites what the browser set on that person. Pass `$process_person_profile: false` on server events that only record that something happened - a row written, a job run, a webhook received. NOT on signup, subscription, plan-change or churn events: those must reach the person, paired with identify()/$set. Never put the flag on every capture - that leaves the project with no person profiles at all.'
- Add posthog.captureException(err, distinctId) in the application's error handler (e.g., Express error middleware, Fastify setErrorHandler, Koa app.on('error'))
- The SDK batches events and flushes asynchronously. await flush() or await shutdown() before letting that process exit. If unsure, set flushAt 1 and flushInterval 0.
- '`posthog.capture()` enqueues synchronously and returns; the batched HTTP send happens afterwards. Treat every per-request handler as short-lived even when the framework feels like a server: Next.js / Nuxt / SvelteKit / Remix route handlers, serverless and edge functions, and Lambda are torn down per invocation before the send runs. Create the client with flushAt 1 and flushInterval 0, then await the send before returning. Always use `await posthog.flush()` for a shared/singleton client, `await posthog.shutdown()` for a per-request client. Never skip the awaited flush or risk the enqueued event being silently dropped.'
Expand Down
3 changes: 2 additions & 1 deletion context/skills/audit-events/description.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The audit covers two lenses:

The audit runs as a step chain. **The exact step list lives in the reference files themselves, not in this overview.** Step 1 lives at `references/1-presence.md`; each step file ends with a `next_step:` frontmatter pointer to the next, and the final step has `next_step: null`. Follow them in the order they point. You must resolve each step in order before any source-tree exploration.

The audit ledger is seeded by the wizard with one pending check per event check. **Each step gracefully handles a missing check id**: if a step's expected id is not in the ledger, it skips its `audit_resolve_checks` call for that id and continues. Use `mcp__wizard-tools__audit_resolve_checks` to patch each check as you finish it.
The audit ledger is seeded by Step 1, not by the wizard — `wizard audit events` runs through the generic skill program, which pre-seeds nothing. Step 1 calls `mcp__wizard-tools__audit_seed_checks` with one pending entry per check; skip it and every later `audit_resolve_checks` call is rejected as an unknown id. **Each later step still handles a missing check id gracefully**: if a step's expected id is not in the ledger, it skips its `audit_resolve_checks` call for that id and continues. Use `mcp__wizard-tools__audit_resolve_checks` to patch each check as you finish it.

**Start by reading the path relative to this file at `references/1-presence.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.

Expand All @@ -33,6 +33,7 @@ The wizard intercepts these and updates the spinner. Use them freely — they ar

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**:

- `mcp__wizard-tools__audit_seed_checks({ checks })` — create the ledger. Called once, by Step 1, with the exact payload that step lists.
- `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.

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 `Agent` subagents when a step says so.
Expand Down
29 changes: 27 additions & 2 deletions context/skills/audit-events/references/1-presence.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ next_step: 2-events-fix.md

# Step 1 — Presence detector

This step decides whether the rest of the audit has anything to look at. Run it **before** any other work. Resolve zero ledger checks here — this step is gating only.
This step decides whether the rest of the audit has anything to look at, then seeds the ledger the later steps resolve against. Run it **before** any other work. Resolve zero ledger checks here — this step gates and seeds only.

## Status

Expand All @@ -14,6 +14,10 @@ Emit:
[STATUS] Detecting PostHog event capture usage
```

## Load the tools

Load via `ToolSearch select:Grep,mcp__wizard-tools__audit_seed_checks,mcp__wizard-tools__audit_resolve_checks` once at the start of this step. Later steps reuse `audit_resolve_checks` to patch each check as it resolves, so it stays loaded.

## Action

Run **two `Grep` calls in parallel**, both with `output_mode: "files_with_matches"`:
Expand All @@ -24,9 +28,30 @@ Run **two `Grep` calls in parallel**, both with `output_mode: "files_with_matche
## Decision

- **Both greps return zero hits anywhere in the project:** emit `[ABORT] PostHog SDK initialization not found` and stop. The wizard catches `[ABORT]` and terminates the run.
- **Init found, capture not found:** continue. Step 2 (fix) will detect this and resolve its four ledger checks with skip details. Step 3 (optimize) still has work to do because pageview defaults and downstream usage may still matter.
- **Init found, capture not found:** continue. Step 2 (fix) will detect this and resolve its five ledger checks with skip details. Step 3 (optimize) still has work to do because pageview defaults and downstream usage may still matter.
- **Both found:** continue normally.

## Seed the audit ledger

Do this only on the continue paths above — never after an `[ABORT]`.

The ledger lives at `.posthog-audit-checks.json` and renders live in the wizard sidebar / "Audit plan" tab. **The runtime does not pre-seed this skill's ledger** — `wizard audit events` runs through the generic skill program, which seeds nothing, so `audit_resolve_checks` rejects every id until the ledger exists. Call `mcp__wizard-tools__audit_seed_checks` directly here with the exact payload below. The tool replaces the file atomically, so calling it once at the start of every run is safe.

```json
{
"checks": [
{ "id": "capture-event-names-static", "area": "Event Capture", "label": "Event names are static string literals", "status": "pending" },
{ "id": "event-naming-standardization", "area": "Event Capture", "label": "Event names follow one consistent convention", "status": "pending" },
{ "id": "event-duplicates-and-bloat", "area": "Event Capture", "label": "No duplicate or kitchen-sink events", "status": "pending" },
{ "id": "event-quality-context-review", "area": "Event Capture", "label": "Capture calls are free of PII and hot-path issues", "status": "pending" },
{ "id": "capture-fires-on-success", "area": "Event Capture", "label": "Completion events fire on success, not on intent", "status": "pending" },
{ "id": "event-usage-coverage", "area": "Event Capture — Optimize", "label": "Captured events are used downstream", "status": "pending" },
{ "id": "events-pageview-defaults", "area": "Event Capture — Optimize", "label": "Pageview / pageleave defaults are sized right", "status": "pending" },
{ "id": "events-env-pollution", "area": "Event Capture — Optimize", "label": "Dev / staging events are not leaking into production", "status": "pending" }
]
}
```

Do not read any files in this step. Do not call `audit_resolve_checks`. Do not preload future steps.

Continue to **`2-events-fix.md`**.
55 changes: 50 additions & 5 deletions context/skills/audit-events/references/2-events-fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ next_step: 3-events-optimize.md

# Step 2 — Event capture (fix)

This step resolves four event-capture quality checks **in parallel**, one subagent per check. These are the same ids the broader PostHog audit seeds — `audit-events` reuses them so a fix once made here is observable from either entry point.
This step resolves five event-capture quality checks **in parallel**, one subagent per check. These are the same ids the broader PostHog audit seeds — `audit-events` reuses them so a fix once made here is observable from either entry point.

- `capture-event-names-static`
- `event-naming-standardization`
- `event-duplicates-and-bloat`
- `event-quality-context-review`
- `capture-fires-on-success`

## Skip case — no `posthog.capture` calls found

If Step 1's capture grep returned **zero** hits, resolve all four checks in a single `audit_resolve_checks` call with `status: "pass"` and `details: "skip: no posthog.capture call sites detected"`. Then continue to **`3-events-optimize.md`**. Do not dispatch subagents.
If Step 1's capture grep returned **zero** hits, resolve all five checks in a single `audit_resolve_checks` call with `status: "pass"` and `details: "skip: no posthog.capture call sites detected"`. Then continue to **`3-events-optimize.md`**. Do not dispatch subagents.

## Status

Expand All @@ -23,9 +24,9 @@ Emit before dispatching:
[STATUS] Auditing event capture quality
```

## Action — dispatch four subagents in one message
## Action — dispatch five subagents in one message

Make **four `Agent` tool calls in a single message** so they run concurrently. Wait for all four to return, then continue to `3-events-optimize.md`. Do not run any other tools between dispatch and the next step.
Make **five `Agent` tool calls in a single message** so they run concurrently. Wait for all five to return, then continue to `3-events-optimize.md`. Do not run any other tools between dispatch and the next step.

The bundled `best-practices.md` reference holds PostHog's authoritative guidance on event-name shape, naming consistency, duplication, and event-quality patterns. It's typically at `.claude/skills/audit-events/references/best-practices.md`; if that path doesn't exist, discover it with `Glob` `**/skills/audit-events/references/best-practices.md`. Each subagent reads it once before judging.

Expand Down Expand Up @@ -175,6 +176,50 @@ Emit one `mcp__wizard-tools__audit_resolve_checks` call with a single update for
Return when the call completes. Do not write the audit report.
```

## After all four return
### Task E — `capture-fires-on-success`

`description`: `Audit capture-fires-on-success`

`prompt`:
```
You are an audit subagent. Resolve exactly one rule and return: capture-fires-on-success.

Background: a capture call placed at the top of a submit handler fires on the user's *intent*, before the server has accepted anything. A form that fails server-side validation then counts as a success, so the event over-reports the thing it is supposed to measure and inflates the event bill. The correct placement for a completion event is the branch that runs after the awaited call resolves successfully — after `if (response.ok)`, inside the `.then()`, after the `await` that would have thrown.

Run **one** Grep: `posthog\.capture\(`. Read each file that contains a hit, once.

Consider only captures whose event name reads as a **completed outcome** — names ending in or containing `_saved`, `_created`, `_updated`, `_deleted`, `_completed`, `_purchased`, `_submitted`, `_signed_up`, `_subscribed`, `_upgraded`, `_paid`, `_sent`, `_published`, or an equivalent past-tense completion in this project's own naming convention. Ignore intent-stage names (`_clicked`, `_started`, `_viewed`, `_opened`, `_attempted`) — those are *supposed* to fire on the action itself, and flagging them is a false positive.

For each completion-named capture, determine whether it sits inside an `async` function, a promise chain, or a callback that receives a server result. If it does not (pure client-side state change, no network call), it passes — there is no outcome to wait for.

Watch for the case that has no awaited response at all: a component whose submission runs through a Next.js Server Action, `useActionState`, a form `action` prop, or a mutation hook, with the capture sitting in the submit path rather than gated on the returned success state. That is the same defect and it is easy to miss, because there is no `await` next to the capture to notice. The fix to report is to move the capture into the action itself, on the server, after the mutation succeeds — or, where it must stay in the browser, to fire it off the returned success state.

Where it does, decide whether the capture executes:
- AFTER the awaited call resolved AND inside the branch taken on success (after an `if (response.ok)` / `if (!error)` guard, inside `.then()`, or after an `await` that would throw on failure and is not wrapped in a `try` that swallows the error) — correct.
- BEFORE the `await`, or after it but OUTSIDE any success guard so it also runs on a rejected/failed response (for example after a `try/catch` that swallows, or before `if (!response.ok) return`) — a violation.

Rule:
- pass: no completion-named captures wait on a server, OR every one of them fires only on the success path.
- suggestion: 1–2 completion-named captures fire before the awaited response resolves or outside the success branch.
- warning: 3+ such captures, OR any one of them on a payment, checkout, subscription, or billing path — those inflate the metrics the business is steered by.

Report the fix concretely: name the guard or branch the call should move inside, at that `path:line`. Do not rewrite any code — this is an audit.

Emit one `mcp__wizard-tools__audit_resolve_checks` call with a single update for id `capture-fires-on-success`, with `file` set to the most material violation's path:line if any, and `details` as compact JSON:

```
{
"completion_event_count": <N>,
"fires_on_intent_count": <N>,
"examples": [
{"event": "<name>", "file": "<path:line>", "issue": "before-await | outside-success-branch", "fix": "<one line: which branch it belongs in>"}
]
}
```

Return when the call completes. Do not write the audit report.
```

## After all five return

Continue to **`3-events-optimize.md`**. Do not write the report yet — that's Step 4's job after Step 3 finishes.
13 changes: 8 additions & 5 deletions context/skills/audit-identify/references/5-server-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,18 @@ Run **two** Greps in parallel:
- `\$process_person_profile|process_person_profile|processPersonProfile` — explicit usage of the property anywhere.

Read each file that contains a server-side `capture(` hit, once. For each server-side capture, determine whether:
- The capture is intended to update person properties (it should follow a corresponding `identify()` and pass `$set` / `$set_once`), OR
- The capture is a transactional/business event that shouldn't touch person properties (most cases — `subscription_upgraded`, `webhook_received`, `cron_job_completed`).
- The capture defines the person or their account state — signup/registration, subscription started, plan changed, churn, or anything that should follow a corresponding `identify()` and pass `$set` / `$set_once`. These are **supposed** to reach the person, OR
- The capture only records that something happened — a row written, a job run, a webhook received (`webhook_received`, `cron_job_completed`).

For the second category, check whether `$process_person_profile: false` is set in the properties object.

This check reads in **both** directions. A missing flag corrupts person properties; a flag on everything is worse, because the project then has no person profiles at all and loses cohorts, person-property filters, lifecycle insights, and its acquisition funnel. Judge each capture on which category it falls in, not on a global preference.

Rule:
- pass: every server-side capture either passes `$process_person_profile: false` OR is paired with an explicit identify()/$set in the same flow (intentional person-property update).
- suggestion: 1–3 server-side captures lack `$process_person_profile: false` and don't appear to update person properties intentionally — recommend adding the flag to prevent silent property corruption.
- warning: 4+ server-side captures or any high-frequency server-side capture (cron, webhook, polling loop) without `$process_person_profile: false` — high blast radius for property corruption.
- pass: every server-side capture that only records an activity passes `$process_person_profile: false`, AND every person-defining capture (signup, subscription, plan change, churn) does **not** — each paired with an explicit identify()/$set in the same flow.
- suggestion: 1–3 activity-only captures lack `$process_person_profile: false` and don't appear to update person properties intentionally — recommend adding the flag to prevent silent property corruption.
- warning: 4+ activity-only captures or any high-frequency server-side capture (cron, webhook, polling loop) without `$process_person_profile: false` — high blast radius for property corruption.
- warning: `$process_person_profile: false` is set on a person-defining event (signup, subscription started, plan changed, churn), or on **every** server capture in the project with no `identify()` anywhere — the project creates no person profiles at all. Name the events that should be let through.

Emit one `mcp__wizard-tools__audit_resolve_checks` call with a single update for id `server-process-person-profile`, including `file` (path:line of the most representative offending capture) and `details` as compact JSON:

Expand Down
Loading
Loading