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/exploring-apm-traces@PostHog? Sign in with GitHub to claim this listing.Comprehensive APM trace investigation guide with clear workflows, parsing scripts, and tool descriptions
1---2name: exploring-apm-traces3description: >4 Investigates distributed application performance using PostHog APM (OpenTelemetry span) data via MCP.5 Use when the user asks about service traces, slow HTTP/database spans, error spans, trace IDs, or span6 attributes — not AI observability traces or product logs. Uses posthog:query-apm-spans, posthog:apm-trace-get,7 posthog:apm-services-list, posthog:apm-attributes-list, and posthog:apm-attribute-values-list.8---910# Exploring APM traces (OpenTelemetry spans)1112PostHog captures distributed traces from OpenTelemetry. Each trace is a tree of spans representing a request's path through services.1314**Disambiguation:** This skill is for **APM / OpenTelemetry traces**. Do not confuse with **AI observability traces** (agent/model `$ai_*` events) or **logs** (`posthog:query-logs`, `posthog:logs-*`).1516## Available tools1718| Tool | Purpose |19| ----------------------------------- | ------------------------------------------------- |20| `posthog:query-apm-spans` | Search and filter spans (compact list view) |21| `posthog:apm-trace-get` | Get the full span list for one hex `trace_id` |22| `posthog:apm-spans-aggregate` | Per-operation aggregates (count, p50/p95, errors) |23| `posthog:apm-spans-tree` | Call-tree aggregates per `(parent, child)` edge |24| `posthog:apm-services-list` | List distinct service names |25| `posthog:apm-attributes-list` | List span or resource attribute keys |26| `posthog:apm-attribute-values-list` | List values for a specific attribute key |2728See [references/spans-and-fields.md](./references/spans-and-fields.md) for the response schema and the `kind`/`status_code` enums.2930## Workflow: debug a trace from a URL3132### Step 1 — Fetch the trace3334```json35posthog:apm-trace-get36{37 "trace_id": "<hex_trace_id>"38}39```4041The response is `{ results: [span, span, …] }` — a flat list of every span in the trace.42The list can be very large for fan-out request flows; when it exceeds the inline limit, Claude Code auto-persists it to a file.4344From the result you get:4546- Every span with `name`, `service_name`, `kind`, `status_code`, `parent_span_id`, `duration_nano`, `is_root_span`47- The `_posthogUrl` — **always include this in your response** so the user can click through to the UI4849### Step 2 — Parse large results with scripts5051When the result is persisted to a file (traces with hundreds of spans across services), use the [parsing scripts](./scripts/) to explore it.5253**Start with the summary** to get the full picture, then drill into specifics:5455```bash56# 1. Overview: services, span count, slowest spans, errors57python3 scripts/print_summary.py /path/to/persisted-file.json5859# 2. Indented chronological tree (DFS by parent_span_id)60python3 scripts/print_timeline.py /path/to/persisted-file.json6162# 3. Drill into a specific span by name63SPAN="HTTP GET /api/users" python3 scripts/extract_span.py /path/to/persisted-file.json6465# 4. Search for a keyword across span names, services, IDs66SEARCH="keyword" python3 scripts/search_spans.py /path/to/persisted-file.json6768# 5. When the JSON shape looks unfamiliar69python3 scripts/show_structure.py /path/to/persisted-file.json70```7172All scripts support `MAX_LEN=N` env var to control truncation (`0` = unlimited).7374## Tree reconstruction (parent_span_id → span_id)7576The flat span list is a tree. Each span carries:7778- `trace_id` — same on every span in the trace79- `span_id` — this span's unique hex ID80- `parent_span_id` — points to the parent's `span_id` (zero-padded hex `000…000` for the root)81- `is_root_span` — convenience flag for the trace entry8283To rebuild the tree:84851. Spans where `is_root_span` is true (or `parent_span_id == "00000000…"`) are **root spans**.862. Every other span is a child of the span whose `span_id` matches its `parent_span_id`.873. Group by `parent_span_id`, walk from each root downward.8889`scripts/print_timeline.py` does this for you and prints a DFS-indented tree.9091## Investigation patterns9293### "Where is time going?"94951. Run `print_summary.py` — it surfaces the top-5 slowest spans by `duration_nano`.962. For a noisy trace, run `print_timeline.py` and scan the indented durations — you can see whether time is dominated by one child span or fan-out across many.973. To dig into one slow span, `SPAN="<name>" python3 scripts/extract_span.py FILE`.9899### "Where did the error happen?"1001011. `print_summary.py` lists every span with `status_code == 2` (Error). Each entry shows service, span name, and parent context.1022. Walk up the tree from an error span via `parent_span_id` to see what request path led there.1033. Error detail lives in each span's `attributes` map (e.g. `exception.message`, `exception.type`), which **is** returned in the trace payload — read it directly off the error span. `apm-attribute-values-list` is for discovering values across spans, not a prerequisite for reading one span's attributes.104105### "Did the request hit service X?"1061071. Run `print_summary.py` — it prints the set of services involved in the trace.1082. If service X is missing, the request never reached it (or instrumentation is missing — check `apm-services-list` to confirm X has emitted spans recently at all).109110### "Did the fan-out look right?"1111121. `print_timeline.py` shows the indentation — wide trees mean parallel calls, deep trees mean sequential dependencies.1132. Look for spans of kind `Client` (3) followed by matching `Server` (2) spans on the called service — that's a synchronous downstream call.114115### Searching by attribute (e.g. `http.method=POST`)116117Each span carries an `attributes` map (span-level OTel attributes like `http.method`, `db.statement`) **in the payload** — so for a span you already have, just read it. **Resource** attributes (k8s labels, `service.version`) are not in the payload. To filter the whole dataset by an attribute:1181191. Use `apm-attributes-list` / `apm-attribute-values-list` to discover keys and values (resource attributes especially).1202. Re-issue `query-apm-spans` with a `filterGroup` entry of type `span_attribute` or `span_resource_attribute`.121122## Constructing UI links123124`apm-trace-get` and `query-apm-spans` return `_posthogUrl` — **always surface this to the user** so they can verify in the PostHog UI.125126When presenting findings, include the relevant PostHog URL.127128## Finding traces129130Use `posthog:query-apm-spans` to search and filter spans. Note this returns spans, not a tree — pass `query.traceId` or grab a `trace_id` from the results and feed it to `apm-trace-get` for the tree.131132### Discover before filtering133134Before constructing filters, discover what's actually in the project:1351361. **Confirm services exist** — call `apm-services-list` to see which services have emitted spans.1372. **Find filterable attributes** — call `apm-attributes-list` with `attribute_type: "span"` or `"resource"`.1383. **Get actual values** — call `apm-attribute-values-list` with a key to see the real values in use.139140Only then construct `query-apm-spans` filters. Custom attributes vary per project and cannot be guessed.141142### By filters143144```json145posthog:query-apm-spans146{147 "query": {148 "serviceNames": ["api-gateway"],149 "dateRange": {"date_from": "-1h"},150 "filterGroup": [151 {"key": "http.status_code", "operator": "gt", "type": "span_attribute", "value": "499"}152 ]153 }154}155```156157### By trace ID (when known)158159```json160posthog:apm-trace-get161{162 "trace_id": "0123456789abcdef0123456789abcdef"163}164```165166### Common gotchas167168- **Durations are nanoseconds.** 1 second = `1_000_000_000`. Filter values in `query-apm-spans` for `duration` are also nanoseconds.169- **`status_code == 2` is Error.** `0` is Unset, `1` is OK. Use `OK` to match `{0, 1}` in the UI filter.170- **`kind`** is an integer 0–5: 0 Unspecified, 1 Internal, 2 Server, 3 Client, 4 Producer, 5 Consumer.171- **`parent_span_id` of a root span** is `"0000000000000000"` (16 zero hex chars, matching the 8-byte span ID width — _not_ the 16-byte trace ID width), not null.172173## Parsing large trace results174175Trace tool results are JSON. When too large to read inline, Claude Code persists them to a file.176177### Persisted file format178179```json180[{ "type": "text", "text": "{\"results\": [...], \"_posthogUrl\": \"...\"}" }]181```182183Every script in `scripts/` unwraps this envelope before parsing.184185### Trace JSON structure186187```text188results (array of span dicts)189 └── each span:190 ├── uuid, trace_id, span_id, parent_span_id (hex strings)191 ├── name, kind (int 0–5), service_name192 ├── status_code (int 0–2), is_root_span (bool)193 ├── timestamp, end_time (ISO 8601)194 ├── duration_nano (int, nanoseconds)195 ├── attributes (map of span-level OTel attributes, e.g. db.statement, http.url)196 └── matched_filter (0/1 — 1 if this span matched the query-apm-spans filter, 0 if it197 only shares a trace with a match; always present, only meaningful from query-apm-spans)198```199200### Available scripts201202| Script | Purpose | Usage |203| -------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------- |204| [`print_summary.py`](./scripts/print_summary.py) | Trace metadata, services, slowest spans, errors | `python3 scripts/print_summary.py FILE` |205| [`print_timeline.py`](./scripts/print_timeline.py) | DFS-indented tree from `parent_span_id` walk | `python3 scripts/print_timeline.py FILE` |206| [`extract_span.py`](./scripts/extract_span.py) | Full row + parent/children for spans matching a name | `SPAN="name" python3 scripts/extract_span.py FILE` |207| [`search_spans.py`](./scripts/search_spans.py) | Find a keyword across name, service_name, IDs | `SEARCH="kw" python3 scripts/search_spans.py FILE` |208| [`show_structure.py`](./scripts/show_structure.py) | Show JSON keys and types without values | `python3 scripts/show_structure.py FILE` |209210## Tips211212- Always set `dateRange` on `query-apm-spans` — queries without a time range are slow. Default is `-1h`; widen only when needed.213- Always include the `_posthogUrl` in your response so the user can click through.214- Span-level attributes **are** in the `apm-trace-get` / `query-apm-spans` payload (each span's `attributes` map). Resource attributes are not — use `apm-attributes-list` (type `resource`) and `apm-attribute-values-list` for those.215- `is_root_span` is the cheap way to find the trace entry — don't string-match `00000000…`.216- For aggregates (p95 by operation, slowest children of a span), use `apm-spans-aggregate` for a flat view or `apm-spans-tree` for parent→child edges — don't reach for SQL.217
Full transparency — inspect the skill content before installing.