A collection of PostHog skills for enhancing AI-assisted workflows. Add this repo as a Claude Code plugin marketplace to get access to all PostHog skills: Then install individual plugins: Or browse available plugins: Copy any skill directory to .claude/skills/ in your project: Any directory under skills/ that contains a .claude-plugin/plugin.json is automatically discovered and added to the market
Add this skill
npx mdskills install PostHog/investigating-error-issue@PostHog? Sign in with GitHub to claim this listing.Comprehensive error investigation workflow with detailed SQL patterns and property nuances
1---2name: investigating-error-issue3description: >4 Investigates a single PostHog error tracking issue end-to-end. Use when5 the user provides an issue ID or pastes an issue URL6 (`/error_tracking/<id>`) and wants to understand the error — who it7 affects, what triggers it, when it started, whether it correlates with8 a release, browser, OS, or feature flag, and what the next step should9 be. Pulls aggregated metrics, sample exception events, segment10 breakdowns, linked replays, and synthesizes a hypothesis-grade summary11 in one pass.12---1314# Investigating an error tracking issue1516When a user asks "what's going on with this error?" or pastes an issue URL, gather17the context they would otherwise have to assemble manually: who is hitting it, what18changed, where it happens, and whether a replay shows the cause.1920## Available tools2122| Tool | Purpose |23| ------------------------------------------- | ------------------------------------------------------------------------------------------- |24| `posthog:query-error-tracking-issue` | Compact issue details (status, assignee, top frame, release, aggregates) |25| `posthog:query-error-tracking-issue-events` | Sampled `$exception` events with stack, URL, browser, `$session_id` |26| `posthog:execute-sql` | Breakdowns, release / flag correlations, surrounding events + console logs around the error |27| `posthog:query-logs` | OTEL log entries around the error timestamp for server-side issues |28| `posthog:query-session-recordings-list` | Linked replays (delegate ranking to `finding-replay-for-issue`) |29| `posthog:read-data-schema` | Confirm property keys before filtering on them |3031## Workflow3233### Step 1 — Establish the issue baseline3435Fetch the issue record with its compact aggregates and a sparkline:3637```json38posthog:query-error-tracking-issue39{40 "issueId": "<issue_id>",41 "dateRange": { "date_from": "-30d" },42 "includeSparkline": true,43 "volumeResolution": 1244}45```4647Capture: `name`, `description`, `status`, `first_seen`, `last_seen`, `assignee`,48total `occurrences` / `users` / `sessions`, top in-app frame, latest release49metadata, and the volume buckets.5051The sparkline tells you the shape — flat, spike, ramp, or recurring — and that52shape drives the rest of the investigation. If the user only asked a status53question, skip `includeSparkline` to save tokens.5455### Step 2 — Pull a sample exception event5657A captured event has the stack frames, URL, browser, and properties needed to58reason about cause. Pull a recent sample first, then an early one to compare.5960```json61posthog:query-error-tracking-issue-events62{63 "issueId": "<issue_id>",64 "limit": 1,65 "verbosity": "stack"66}67```6869Use `verbosity: "raw"` only if the truncated stack hides the answer. The tool70defaults to `onlyAppFrames: true`, which strips vendor frames; flip to `false`71when the bug appears to live in a third-party library — or when the response72comes back with `stacktrace.type: "resolved"` but no frames at all (common for73minified bundles where every frame looks vendor-y to the resolver, e.g. React74production builds).7576For the earliest sample, narrow `dateRange` to a tight window around the77issue's `first_seen` (e.g. set `date_from` slightly before and `date_to`78slightly after) and pass `orderDirection: "ASC"` so you get the earliest79event in the window rather than the latest — the tool defaults to `DESC`,80which would return a recent event and silently duplicate the first call.81If recent and earliest events look materially different — different stack82root, different URL pattern — the issue may be a grouping mistake. Flag for83`grouping-noisy-errors` instead of continuing as if it were one bug.8485### Step 3 — Run breakdowns to isolate the cause8687Breakdowns aren't a typed tool — drop into `execute-sql`. Run only the88breakdowns the issue's shape suggests; each one costs a query and clutters the89synthesis.9091| Sparkline shape | First breakdown to try |92| ----------------- | ------------------------------------------------------------------------ |93| Spike from zero | By app version / release — almost always a deploy regression (see below) |94| Steady-state high | By browser / OS — rendering or platform-specific bug |95| Ramp | By geography or feature flag — gradual rollout exposure |96| Bursts then quiet | By time of day or `$current_url` — scheduled job or specific page |9798#### Picking the right version property99100PostHog emits three version-shaped fields. They mean different things and only101one of them answers "what version of the user's app introduced this?":102103| Property | What it is | Auto-captured by | Use for |104| --------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |105| `$exception_releases` | Cymbal-managed release map, keyed by release ID | Only when SDK publishes release metadata (e.g. sourcemap upload tied to a release) | Most precise release attribution **when present** |106| `$app_version` | The user's deployed app version | iOS (`CFBundleShortVersionString`), React Native (Expo / react-native-device-info) | "What deploy of my app introduced this?" — the question users care about |107| `$lib_version` | The PostHog SDK library version (e.g. posthog-js 1.298.0) | Every SDK on every event | The narrow "did upgrading the PostHog SDK introduce this?" question |108109`$lib_version` is on virtually every event, which makes it tempting — but it's110the PostHog library version, not the user's app version. A constant111`$lib_version` paired with a spike means the user shipped a regression in112their own code with the SDK unchanged, which is the common case. Reach for113`$lib_version` only when nothing else is populated and you're explicitly114asking "did upgrading PostHog cause this?".115116Web / server / Node / Java / Python projects do **not** auto-capture117`$app_version` — the customer has to set it (via `register`, a context118provider, or `before_send`). If the breakdown comes back with one119`$app_version` row of all-NULL, say so explicitly in the synthesis and120suggest the customer wire it up; falling back to `$exception_releases` or to121a per-day timeline by `first_seen` keeps the investigation moving.122123Example (`$app_version` — populated automatically on mobile, manually on124web / server):125126```sql127posthog:execute-sql128SELECT129 properties.$app_version AS app_version,130 count() AS occurrences,131 uniq(person_id) AS users,132 min(timestamp) AS first_seen,133 max(timestamp) AS last_seen134FROM events135WHERE event = '$exception'136 AND (issue_id = '<issue_id>' OR properties.$exception_issue_id = '<issue_id>')137 AND timestamp > now() - INTERVAL 30 DAY138GROUP BY app_version139ORDER BY occurrences DESC140LIMIT 20141```142143The `(issue_id = ... OR properties.$exception_issue_id = ...)` pattern144mirrors the canonical `build_issue_where` clause from145`products/error_tracking/backend/api/query_utils.py`. `issue_id` is the146resolved virtual field on `events` (it follows fingerprint overrides so147merged/split issues route correctly); `properties.$exception_issue_id` is148the raw event property captured at ingestion. Filtering on only the property149silently undercounts events for issues that have been merged or split.150151If `first_seen` for one `app_version` is much later than the issue's overall152`first_seen`, that release introduced or worsened the bug — strong root-cause153signal. If every row is `NULL`, the SDK isn't reporting an app version on154this project (common on web / server) — switch to `$exception_releases` if155the customer ships releases, or fall back to a `toDate(timestamp)` timeline.156157When `$exception_releases` is populated, it's a JSON dict keyed by release158ID. There is no top-level `$release` property; query `$exception_releases`159directly when you need release attribution and the customer has it wired up.160161Repeat with `properties.$browser`, `properties.$os`, `properties.$current_url`,162or any feature flag the project tags errors with.163164### Step 4 — Check feature flag exposure165166If the user suspects an experiment or rollout, check whether affected users had167a flag enabled when the error fired.168169To enumerate which flags were evaluated on affected users, parse the170`$active_feature_flags` property — it is materialized as a JSON-encoded string in171ClickHouse, so `arrayJoin(properties.$active_feature_flags)` directly will fail;172`JSONExtract` is the working pattern:173174```sql175posthog:execute-sql176SELECT177 arrayJoin(JSONExtract(toString(properties.$active_feature_flags), 'Array(String)')) AS flag,178 count() AS occurrences,179 uniq(person_id) AS users180FROM events181WHERE event = '$exception'182 AND (issue_id = '<issue_id>' OR properties.$exception_issue_id = '<issue_id>')183 AND timestamp > now() - INTERVAL 14 DAY184 AND notEmpty(toString(properties.$active_feature_flags))185GROUP BY flag186ORDER BY occurrences DESC187LIMIT 20188```189190Caveat: every event captures every evaluated flag key, so this enumeration often191returns identical counts across flags and **doesn't tell you which flag192correlates with the error** — only which were on the user. To actually test a193hypothesis, query the per-flag value column `properties.$feature/<flag-key>`,194which carries the evaluated value (`true`/`false`/variant name):195196```sql197posthog:execute-sql198SELECT199 properties.`$feature/my-flag-key` AS variant,200 count() AS occurrences,201 uniq(person_id) AS users202FROM events203WHERE event = '$exception'204 AND (issue_id = '<issue_id>' OR properties.$exception_issue_id = '<issue_id>')205 AND timestamp > now() - INTERVAL 14 DAY206GROUP BY variant207ORDER BY occurrences DESC208```209210Compare the variant split here to the project's overall exposure on the same211flag in the same window. Disproportionate representation of one variant212suggests the flag is involved in the cause — not a guarantee, but a strong213hypothesis.214215### Step 5 — Reconstruct what happened around the error216217Use the `$session_id` from the sample event in step 2 to pull the activity218surrounding the exception. Three sources stack on each other; run the ones219that make sense for the SDK that captured the error.220221#### 5a. Surrounding events (client SDKs by `$session_id`)222223Mirrors the ET frontend session timeline. Pulls custom events, page views,224and other exceptions captured under the same session within a ±1h window:225226```sql227posthog:execute-sql228SELECT229 uuid,230 event,231 timestamp,232 properties.$lib AS lib,233 properties.$current_url AS url234FROM events235WHERE $session_id = '<session_id_from_step_2>'236 AND (event = '$exception' OR event = '$pageview' OR left(event, 1) != '$')237 AND timestamp >= toDateTime('<error_timestamp>', 'UTC') - INTERVAL 1 HOUR238 AND timestamp <= toDateTime('<error_timestamp>', 'UTC') + INTERVAL 1 HOUR239ORDER BY timestamp ASC240LIMIT 100241```242243The `left(event, 1) != '$'` clause drops PostHog autocapture / system events244while keeping every custom event. The `OR event = '$pageview'`/`'$exception'`245exceptions re-add the two system events worth seeing on the timeline. This is246the same filter the ET UI uses.247248Mixed `$lib` values in the output are a feature, not noise. When a server SDK249propagates `$session_id` from the client request (PostHog's own backend does250this), the timeline shows server-side activity inline with the browser side —251"both SDKs when available" for free. Skim the lib column to see how each row252was produced.253254The skill defaults to a ±1h window because that's what the UI uses; widen it255when an issue's actions are slow (long batch jobs, background workers) or256tighten it when only the seconds right before the throw matter.257258#### 5b. Console logs (web / React Native session replay)259260When session replay is enabled, the replay pipeline emits `console.*` calls261into the `log_entries` table tagged with the same session id. Pull them with262the matching window:263264```sql265posthog:execute-sql266SELECT timestamp, level, message267FROM log_entries268WHERE log_source = 'session_replay'269 AND log_source_id = '<session_id_from_step_2>'270 AND timestamp >= toDateTime('<error_timestamp>', 'UTC') - INTERVAL 1 HOUR271 AND timestamp <= toDateTime('<error_timestamp>', 'UTC') + INTERVAL 1 HOUR272ORDER BY timestamp ASC273LIMIT 200274```275276`log_source = 'session_replay'` is the discriminator — `log_entries` is shared277with other sources. Empty results are common: either replay isn't enabled, or278this specific session wasn't recorded. Mention that in the synthesis rather279than treating it as a failure.280281#### 5c. Server logs around the error (OTEL via `query-logs`)282283For server-side exceptions, correlate the exception timestamp with OTEL log284entries the customer ingests. Many projects don't ingest logs at all — if285`query-logs` returns nothing or errors, say so and move on. Discover available286services first with `logs-attribute-values-list` when you don't know which287service produced the error.288289```json290posthog:query-logs291{292 "query": {293 "dateRange": {294 "date_from": "<error_timestamp minus 5 minutes>",295 "date_to": "<error_timestamp plus 5 minutes>"296 },297 "severityLevels": ["error", "warn"],298 "serviceNames": ["<service.name if known>"],299 "limit": 50,300 "orderBy": "earliest"301 }302}303```304305Caveats worth knowing before relying on this output:306307- Logs are ingested separately from events and typically have shorter retention.308 Old exceptions may return empty even though the issue is still active.309- `trace_id` / `span_id` come back zero-padded (`"00000000..."`) when not set.310 Trace-based correlation only works for explicitly instrumented requests, not311 for every event.312- `service.name` is a resource attribute. Narrow with `serviceNames` rather313 than a free-text `searchTerm` when you know the producer.314315#### 5d. Find a representative replay316317Hand off to `finding-replay-for-issue` when picking the _best_ session matters —318popular issues link hundreds of recordings, mostly short crash fragments or319idle-tab sessions, and that skill applies the duration / active-time / recency320ranking that finds the one most likely to show the cause. Hand off too when the321user asks for "a replay" without specifying which.322323Skip the hand-off and pull a recording inline via `query-session-recordings-list`324with `session_ids` from the sample exception events you already fetched in step 2325when only a handful of sessions are linked, the user already named a specific326session, or any working example will do (e.g. proving the error reproduces).327328If neither path returns a recording, mention that session replay may not be329enabled for the affected users — useful context, not a failure.330331### Step 6 — Synthesize332333Present in this order:3343351. **What it is** — type, message, where in the stack3362. **Who it affects** — total users, sessions, and any segment breakdown that337 stood out3383. **When it started** — `first_seen`, plus the release / version that339 introduced it if a breakdown found one3404. **Likely cause** — one or two hypotheses backed by the breakdowns above3415. **Next step** — a concrete action: investigate the suspected release, watch342 the linked replay, ping the assignee, or escalate343344Keep the synthesis tight. The user wants the answer, not a tour of the data.345346## Tips347348- The canonical join key from events to an issue is the resolved `issue_id`349 virtual field, with `properties.$exception_issue_id` as fallback — see Step 3350 for the reason and the `build_issue_where` pattern.351- For a "what version introduced this?" breakdown, prefer `$app_version` (the352 user's deployed app version, auto-captured on iOS / React Native and353 manually set on web / server) or `$exception_releases` when populated. Avoid354 `$lib_version` for this question — it's the PostHog SDK library version, not355 the user's app. See the "Picking the right version property" subsection in356 Step 3.357- If the issue spans more than 30 days, widen the date range explicitly.358 Defaults often truncate the original `first_seen` event off the breakdown.359- Don't propose a fix in the synthesis unless the cause is obvious from the360 sample stack. Hypotheses backed by data are more useful than confident361 guesses.362- If `query-error-tracking-issue` returns an `external_issues` array, the issue363 is already linked to a Linear / Jira / GitHub ticket. Mention the link in the364 synthesis so the user doesn't open a duplicate.365
Full transparency — inspect the skill content before installing.