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/suppressing-noisy-errors@PostHog? Sign in with GitHub to claim this listing.Comprehensive, safety-first error suppression workflow with clear tool selection and dry-run validation
1---2name: suppressing-noisy-errors3description: >4 Create PostHog error tracking suppression rules to drop high-volume,5 low-value errors at ingestion. Use when the user asks "stop capturing6 this error", "drop browser extension errors", "ignore ResizeObserver7 loops", "suppress bot-driven errors", or wants to reduce ingestion8 cost from noisy unactionable errors. Identifies suppression9 candidates, scopes the filter tightly, decides between full10 suppression and sampling, and confirms the rule before creating it.11 Suppressed errors are dropped permanently — this skill defaults to12 caution.13---1415# Suppressing noisy errors1617Suppression is destructive in spirit: matching events are dropped at ingestion and18never become issues. The wrong rule silently throws away real bugs. This skill19exists to make sure suppression is applied only to patterns that are genuinely20unactionable, with filters narrow enough to avoid swallowing unrelated errors.2122## When suppression is the right tool2324Suppression is the right tool when an error is:2526- **Unactionable from your code** — browser extensions, third-party scripts, ad27 blockers, network beacons firing after navigation. You can't fix it because you28 didn't write it.29- **Browser engine quirks** — `ResizeObserver loop limit exceeded`,30 `Script error.`, `Non-Error promise rejection captured` with empty payloads.31- **Bot or crawler traffic** — errors firing only from headless browsers or known32 crawler user agents.33- **Sampling already enough** — for high-volume but real errors, dampen with34 `sampling_rate` instead of full suppression so you keep visibility without35 paying full cost.3637Suppression is **not** the right tool when:3839- The error is unactionable _today_ but might become actionable after a fix —40 use issue status `archived` or `resolved` instead so it surfaces if it returns.41- You only want to mute notifications — assign the issue to a user, change its42 status, or use notification rules.43- The error is a duplicate of another — merge or create a grouping rule44 (`grouping-noisy-errors`).4546## Available tools4748| Tool | Purpose |49| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |50| `posthog:query-error-tracking-issues-list` | Find suppression candidates by volume and impact; dry-run a candidate filter via `filterGroup` |51| `posthog:query-error-tracking-issue-events` | Inspect sampled `$exception` events to confirm the pattern |52| `posthog:execute-sql` | Fallback dry-run for filters that need OR groups or operators outside the `filterGroup` allowed list |53| `posthog:error-tracking-suppression-rules-list` | Check existing suppression rules |54| `posthog:error-tracking-suppression-rules-create` | Create the suppression rule |55| `posthog:error-tracking-issues-partial-update` | Hide past data via issue status without dropping events at ingestion |5657## Workflow5859### Step 1 — Identify candidates6061High occurrences with low distinct users is the strongest noise signal — one62user (or one bot) producing many events.6364```json65posthog:query-error-tracking-issues-list66{67 "status": "active",68 "orderBy": "occurrences",69 "orderDirection": "DESC",70 "dateRange": { "date_from": "-7d" },71 "limit": 30,72 "volumeResolution": 073}74```7576Look for:7778- High `occurrences`, low `users` ratio (e.g., 50,000 occurrences, 3 users → likely79 bot or extension loop)80- Exception messages matching known noise patterns: `ResizeObserver loop`,81 `Script error.`, extension namespaces (`chrome-extension://`,82 `moz-extension://`, `safari-extension://`)83- Stack traces dominated by third-party domains the user doesn't control8485### Step 2 — Confirm the pattern8687For each candidate, pull a sample of `$exception` events and check that the88pattern matches what you intend to suppress:8990```json91posthog:query-error-tracking-issue-events92{93 "issueId": "<candidate_issue_id>",94 "limit": 10,95 "verbosity": "stack"96}97```9899`onlyAppFrames` defaults to `true`, but for noise investigation you usually100want the third-party frames visible — pass `onlyAppFrames: false` so extension101URLs and vendor domains show up in the stack.102103Confirm:104105- The exception type or message text is consistent across the sample106- The URLs / user agents / browsers don't include real user traffic mixed in with107 the noise108- Suppressing this pattern won't hide a future real bug that happens to share109 the type110111If any sample doesn't match, narrow the filter or skip the candidate.112113### Step 3 — Scope the filter tightly114115Suppression rules are configured with the same filter shape as grouping rules.116The `error-tracking-suppression-rules-create` tool description warns explicitly:117do **not** create match-all rules and do **not** create overly broad rules.118Match on the most specific property combination you can:119120| Noise pattern | Recommended filter |121| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |122| Chrome extension errors | `$exception_sources icontains "chrome-extension://"` |123| Firefox extension errors | `$exception_sources icontains "moz-extension://"` |124| Safari extension errors | `$exception_sources icontains "safari-extension://"` |125| ResizeObserver loop | `$exception_values icontains "ResizeObserver loop"` (the message is specific; a type filter is optional) |126| Cross-origin "Script error." | `$exception_values icontains "Script error."` AND `$exception_types exact "Error"` |127| Bot user agents | `$raw_user_agent regex "(?i)bot"` for a single term; see the alternation pattern below for matching several bot/crawler markers in one rule |128| Third-party network beacon failures | `$exception_sources icontains "<vendor-domain>"` AND a type filter (e.g. `$exception_types exact "TypeError"`) |129130The canonical exception properties (`$exception_types`, `$exception_values`,131`$exception_sources`, `$exception_functions`) are arrays at capture time. The132property filter compiler [special-cases them](https://github.com/PostHog/posthog/blob/master/posthog/hogql/property.py#L904) — it parses the133JSON-materialized column and wraps the filter in134`arrayExists(v -> ..., JSONExtract(...))`, so all the standard operators135(`exact`, `is_not`, `icontains`, `not_icontains`, `regex`, `not_regex`) work136against individual elements with the bare value: `exact "TypeError"`, not137`exact '["TypeError"]'` or `regex '"TypeError"'`.138139The singular forms (`$exception_type`, `$exception_message`) and140`$exception_stack_trace_raw` are emitted on a fraction of a percent of events;141filtering on them produces a rule that silently never matches.142143Note that the `regex` operator on suppression and grouping rules compiles to144the HogVM `Operation::Regex`, which is **case-sensitive**. Use the `(?i)`145inline flag for case-insensitive matching (e.g. `(?i)headlesschrome`).146147For matching multiple bot or crawler terms, use bare pipes for alternation.148Pass this as the `value` field of the regex filter when calling the API149(`$raw_user_agent` is more reliable than the parsed `$user_agent`, which some150parsers normalize away from crawler markers):151152```text153(?i)(HeadlessChrome|bot|crawler|spider)154```155156Whenever possible, AND together two or more conditions — type plus message, or157message plus URL pattern — so the rule is specific to the real noise.158159### Step 4 — Decide: suppress or sample160161If you want to keep some visibility, use `sampling_rate` between 0 and 1:162163- `sampling_rate: 1` — drop everything matching (full suppression)164- `sampling_rate: 0.95` — drop 95% of matching events, keep 5% as sentinel data165- `sampling_rate: 0.5` — half-rate, useful for high-volume but real errors166167Default to a non-1.0 sampling rate when there's any doubt that the pattern is168purely noise. You can tighten to 1.0 later once the data shows the rule isn't169catching real issues.170171### Step 5 — Dry-run the filter against live data172173Before asking for confirmation, run the candidate filter against the issues174list so you (and the user) can see exactly which issues the rule would have175caught over the last 7 days. `query-error-tracking-issues-list` accepts the176same property-filter shape suppression rules use via its `filterGroup`177parameter, so for a typical AND-only rule you can pass the rule's leaf178filters directly — no HogQL translation needed:179180```json181posthog:query-error-tracking-issues-list182{183 "filterGroup": [184 { "type": "event", "key": "$exception_types", "operator": "exact", "value": "Error" },185 { "type": "event", "key": "$exception_values", "operator": "icontains", "value": "ResizeObserver loop" }186 ],187 "dateRange": { "date_from": "-7d" },188 "status": "all",189 "filterTestAccounts": false,190 "orderBy": "occurrences",191 "limit": 25192}193```194195Important defaults to override for suppression preview:196197- `status: "all"` — suppression applies regardless of issue status, so don't198 let the default `active` filter hide already-archived noise.199- `filterTestAccounts: false` — the rule will not respect the test-account200 toggle at ingestion. The preview should match production reality.201202Each row is one issue the rule would catch: `name` (exception type),203`description` (sample message), `source`, `library`, plus204`aggregations.occurrences` and `aggregations.users`. The issue list **is**205the per-issue breakdown — read every row.206207**The single most important safety check**: scan the result for any issue208whose `name` / `description` / `source` looks like a real bug the team209would want to fix, not noise. A filter that looks tight by message text210will routinely match unrelated issues that happen to share a phrase, and211this is the failure mode that silently destroys real data once the rule is212live. If you see anything suspicious, narrow the filter (step 3) and rerun213this step until only the genuine noise pattern is in the list.214215Add up `aggregations.occurrences` and `aggregations.users` across rows for216the blast-radius totals you'll surface to the user in step 6. If you need217exact totals across more than `limit` issues, paginate with `offset` or218fall back to the HogQL aggregate at the end of this step.219220For one or two concrete sample events with full stack traces, follow up on221the most suspicious-looking issue with `query-error-tracking-issue-events`:222223```json224posthog:query-error-tracking-issue-events225{226 "issueId": "<id from the list>",227 "limit": 3,228 "verbosity": "stack",229 "onlyAppFrames": false230}231```232233#### When you must fall back to execute-sql234235`filterGroup` is **flat AND only**. Drop into HogQL when:236237- The rule uses `type: "OR"` at the outer group or any nested OR.238- The rule uses operators not supported by `filterGroup` (e.g. `between`,239 `in`, `semver_*`).240- You want a precise event-level count rather than per-issue aggregates.241242The HogQL shape mirrors what the suppression rule bytecode compiles to.243The materialized property column is nullable, so the `coalesce(..., '[]')`244wrapper is required — without it ClickHouse rejects the query with245"Nested type Array(String) cannot be inside Nullable type":246247```sql248SELECT249 count() AS matched,250 count(DISTINCT distinct_id) AS users,251 count(DISTINCT properties.$exception_issue_id) AS issues252FROM events253WHERE event = '$exception'254 AND timestamp > now() - INTERVAL 7 DAY255 AND arrayExists(256 v -> ifNull(ilike(v, '<pattern>'), 0),257 JSONExtract(coalesce(properties.$exception_values, '[]'), 'Array(String)')258 )259```260261Use `ilike` for `icontains`, plain equality for `exact`, `match(v,262'<pattern>')` for `regex`. The rule's `regex` is case-sensitive — add263`(?i)` inline if needed.264265### Step 6 — Confirm with the user before creating266267Suppression is destructive in spirit even though the API marks it268`destructive: false`. Show the user before creating:2692701. The exact filter you plan to send2712. The list of issues from step 5 with their `occurrences` and `users`,272 plus the aggregate totals — call out any rows that look like real bugs2733. Whether it overlaps any existing suppression rules274 (`posthog:error-tracking-suppression-rules-list` first)275276Wait for explicit confirmation. Then create:277278```json279posthog:error-tracking-suppression-rules-create280{281 "filters": {282 "type": "AND",283 "values": [284 {285 "type": "event",286 "key": "$exception_types",287 "operator": "exact",288 "value": "Error"289 },290 {291 "type": "event",292 "key": "$exception_values",293 "operator": "icontains",294 "value": "ResizeObserver loop"295 }296 ]297 },298 "sampling_rate": 0.95299}300```301302Start at `0.95` (drop 95%, keep 5% as sentinel data) so you can confirm the303rule isn't catching real errors before tightening to `1.0`.304305### Step 7 — Watch the rule for 24-48h306307After creating the rule:308309- Confirm matching events are no longer being captured by running the same310 filter against a short window scoped to **after** the rule was created311 (e.g. `WHERE timestamp > now() - INTERVAL 1 HOUR` once an hour has passed).312 Don't re-run the 7-day estimate from step 5 — suppression only applies to313 new events, so historical events in the window will still be there and the314 count won't drop.315- Watch related active issues over the post-creation window — if their volume316 drops while non-related issues hold steady, the rule was scoped correctly317- If a related real issue's volume drops too (false-positive), ask the user to318 disable the rule via **Project settings → Error tracking → Suppression rules**319 immediately and tighten the filter before re-creating it. The MCP tools to320 edit or delete a rule (`error-tracking-suppression-rules-partial-update`,321 `-destroy`) are not enabled — the agent has no way to recover programmatically.322323If you see signs of false positives (a real issue going quiet at the same time324the rule was created), prefer disabling the rule over deleting it — that325preserves the rule's configuration for forensic review.326327## Tips328329- Project settings → Error tracking → Suppression rules shows the same data;330 mention this when the user asks where rules live in the UI.331- Suppression applies at ingestion. Existing issues from past events keep their332 data; only new events are dropped.333- For a status-only change (don't drop the data, just hide it from the active334 list), prefer `error-tracking-issues-partial-update` with `status: "suppressed"`335 over a suppression rule.336- The schema explicitly warns the model not to create match-all rules. If the337 user asks "suppress everything from extensions", still scope by stack trace or338 URL — never leave `filters` empty.339- A suppression rule that turns out to be too narrow is harmless (some noise340 leaks through). A rule that's too broad silently destroys real data — bias341 toward narrow.342
Full transparency — inspect the skill content before installing.