diff --git a/CHANGELOG.md b/CHANGELOG.md
index 383d49c..ebc0253 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Add `analytics` domain: Sentry span-quota guard, Sentry MCP query workflows (including longer-range/30D+ query fidelity and percentile-sample-size filtering), release-over-release performance attribution, instrumentation methodology, and supporting knowledge
+
## [0.1.0]
### Added
diff --git a/domains/analytics/knowledge/metrametrics-identity.md b/domains/analytics/knowledge/metrametrics-identity.md
new file mode 100644
index 0000000..1faed3b
--- /dev/null
+++ b/domains/analytics/knowledge/metrametrics-identity.md
@@ -0,0 +1,41 @@
+---
+name: metrametrics-identity
+domain: analytics
+description: isOptIn:true unconditionally strips user identity in MetaMetricsController — always sends as anonymous ID
+---
+
+# MetaMetrics Identity Stripping
+
+## The Mechanism
+
+In `MetaMetricsController` (`app/scripts/controllers/metametrics-controller.ts`):
+
+```typescript
+if (excludeMetaMetricsId || (isOptIn && !metaMetricsIdOverride)) {
+ idType = 'anonymousId';
+ idValue = METAMETRICS_ANONYMOUS_ID; // 0x0000000000000000
+}
+```
+
+When `isOptIn: true` with no `metaMetricsIdOverride`:
+- The user's real `metaMetricsId` is discarded
+- ALL such events share a single anonymous ID (`0x0000000000000000`) in Segment
+- User-level attribution is completely lost
+
+This is **unconditional** — it applies to fully opted-in users with valid IDs, not just anonymous users.
+
+## Intended Use
+
+The onboarding opt-in flow (`creation-successful.tsx`) — where the user hasn't committed to MetaMetrics yet and no `metaMetricsId` has been persisted. The event must fire regardless of opt-in state.
+
+## The Misuse Pattern
+
+Post-opt-in `trackEvent` calls with `{ isOptIn: true }` without `metaMetricsIdOverride`. Defeats the purpose of Segment user-level dimensions (account types, feature flags).
+
+## Detection
+
+```bash
+grep -r "isOptIn: true" app/scripts/ ui/ --include="*.ts" --include="*.tsx"
+```
+
+Any occurrence outside `creation-successful.tsx` (or the onboarding flow) is suspect.
diff --git a/domains/analytics/knowledge/segment-governance.md b/domains/analytics/knowledge/segment-governance.md
new file mode 100644
index 0000000..2ebe1e0
--- /dev/null
+++ b/domains/analytics/knowledge/segment-governance.md
@@ -0,0 +1,40 @@
+---
+name: segment-governance
+domain: analytics
+description: Segment event governance via segment-schema is advisory — no CI enforcement prevents unregistered events from shipping
+---
+
+# Segment Event Governance
+
+## Architecture
+
+| Component | Location |
+|-----------|----------|
+| Tracking plan | `Consensys/segment-schema` → `tracking-plans/metamask-extension.yaml` |
+| Event registry | `shared/constants/metametrics.ts` → `MetaMetricsEventName` enum (300+ entries) |
+| Review process | `CONTRIBUTING.md` in segment-schema; Data Council review |
+| Governance channel | `#metamask-metametrics`, `@consensys/data-council` |
+
+## The Gap
+
+There is **no CI enforcement** in the extension repo. A developer can:
+
+1. Add entry to `MetaMetricsEventName` enum
+2. Call `trackEvent` with it
+3. Merge and ship to production
+
+...without registering in segment-schema or going through Data Council review.
+
+## Implications
+
+- Schema drift between tracking plan and production events
+- No property schema validation for unregistered events
+- Billing impact goes unreviewed
+- Data Council review is bypassable by omission
+
+## Recommended Fix
+
+CI check that:
+1. Parses `MetaMetricsEventName` entries
+2. Validates each against `tracking-plans/metamask-extension.yaml`
+3. Fails build if event is missing from the plan
diff --git a/domains/analytics/knowledge/span-sub-sampling.md b/domains/analytics/knowledge/span-sub-sampling.md
new file mode 100644
index 0000000..fa90f08
--- /dev/null
+++ b/domains/analytics/knowledge/span-sub-sampling.md
@@ -0,0 +1,72 @@
+---
+name: span-sub-sampling
+domain: analytics
+description: Deterministic per-trace sub-sampling for high-frequency custom spans — global tracesSampleRate × span sub-rate, traceId-hash bucketed
+---
+
+# Span Sub-Sampling
+
+Durable fix for a custom span that fans out and eats the span budget. Layer a per-trace sub-rate **under** the global `tracesSampleRate`, keyed on the trace id so every span in a trace is kept-or-dropped together. Source: [PR #39891](https://github.com/MetaMask/metamask-extension/pull/39891) (`shared/lib/wrapper-sampling.ts`).
+
+## Rate Math
+
+```
+effective rate = global tracesSampleRate × span sub-rate
+```
+
+- Global `tracesSampleRate` is already small (extension prod: 0.75%).
+- The sub-rate cuts the custom span on top: `0.75% × 1% = 0.0075%`.
+- PR #39891 ships a sub-rate of 0.5% (`WRAPPER_SAMPLE_RATE = 0.005`) — a conservative pilot — and names 5% as the step-up once the denylist is confirmed effective in production.
+
+Pick the sub-rate from how many sampled traces the metric needs to stay useful — not from the quota alone. Too low and the metric goes dark.
+
+## Pattern
+
+```ts
+const WRAPPER_SAMPLE_RATE = 0.005;
+
+// Deterministic: same answer for the same traceId, so all spans in a trace
+// are kept or dropped together — clean waterfalls, no partial gaps.
+export function shouldSampleWrappers(traceId: string | undefined): boolean {
+ if (!traceId || traceId.length < 8) {
+ return false;
+ }
+ const hashBucket = parseInt(traceId.slice(0, 8), 16) % 10000;
+ return hashBucket < WRAPPER_SAMPLE_RATE * 10000;
+}
+```
+
+**Why deterministic, not `Math.random()` per call:** independent per-span sampling shreds a trace into partial waterfalls (some spans present, siblings missing) — useless for attribution. Hashing the trace id makes keep/drop a property of the whole trace.
+
+## Gate Order (cheapest check first)
+
+```ts
+const traceId = sentryGetActiveSpan()?.spanContext().traceId;
+if (!traceId || isReadOnlyAction(action) || !shouldSampleWrappers(traceId)) {
+ return doWorkWithoutSpan();
+}
+return trace({ name, op, data }, doWorkWithSpan);
+```
+
+1. No active trace → no span.
+2. Denylist → skip noise (below).
+3. Sub-sample miss → skip this trace's spans.
+
+## Denylist: cut before you sample
+
+Drop spans with no timing/attribution signal before sub-sampling. In PR #39891, read-only verbs are ~90% of `messenger.call` volume:
+
+```ts
+const READ_ONLY_VERB = /^(?:get|has|find|is|peek)(?:[A-Z]|$)/u;
+```
+
+Removing ~90% of volume before the sample multiplies headroom — a higher sub-rate then yields the same span budget, so kept traces are denser and more useful.
+
+## Where the Gate Goes
+
+- **Consumer (extension):** spans go through `trace()`. Gate at the call site, or for a whole span family inside the wrapper. `traceId` from `sentryGetActiveSpan()?.spanContext().traceId`.
+- **Controller package (core):** controllers call an injected `trace` callback. Gate in the package's trace util or the callback so every consumer inherits the cap. Pull the trace id from the controller's tracing context, not a fresh Sentry import.
+
+## Kill Switch
+
+Ship every always-on span family with an env disable flag (PR #39891: `SENTRY_DISTRIBUTED_TRACING_DISABLED` returns the messenger un-wrapped). It turns a future emergency cut into a config flip instead of a cherry-pick.
diff --git a/domains/analytics/skills/analytics-instrumentation/repos/metamask-extension.md b/domains/analytics/skills/analytics-instrumentation/repos/metamask-extension.md
new file mode 100644
index 0000000..5175f96
--- /dev/null
+++ b/domains/analytics/skills/analytics-instrumentation/repos/metamask-extension.md
@@ -0,0 +1,61 @@
+---
+repo: metamask-extension
+parent: analytics-instrumentation
+---
+
+## Key Files
+
+| Content | Path |
+|---------|------|
+| Sentry trace wrapper | `shared/lib/trace.ts` |
+| Trace name enum | `shared/lib/trace.ts` → `TraceName` |
+| MetaMetrics controller | `app/scripts/controllers/metametrics-controller.ts` |
+| Event enum | `shared/constants/metametrics.ts` → `MetaMetricsEventName` |
+| Sentry setup + sample rate | `app/scripts/lib/setupSentry.js` → `getTracesSampleRate()` |
+| Segment tracking plan | `Consensys/segment-schema` → `tracking-plans/metamask-extension.yaml` |
+
+## Cross-Process Context (UI → Background)
+
+The extension has two Sentry hubs — one in the UI process and one in the background service worker. A trace starting in UI and continuing in background requires explicit context propagation across the RPC boundary:
+
+```typescript
+// Serialize at UI call site
+const context: SerializedTraceContext = {
+ _name: TraceName.MyOperation,
+ _traceId: span.spanContext().traceId,
+ _spanId: span.spanContext().spanId,
+}
+
+// Background receives context, creates child span
+trace({ name: TraceName.MyOperation, parentContext: context }, async () => { ... })
+```
+
+Without propagation: Sentry shows two disconnected operations. With propagation: complete tree from user action to RPC call.
+
+## Sentry Sample Rate
+
+```bash
+grep -n "tracesSampleRate" app/scripts/lib/setupSentry.js
+# Verify current value before calculating — it has changed between releases
+```
+
+## Sentry Traces Explorer Query (Volume Estimation)
+
+```
+Environment: production | Time range: 30 days | Mode: aggregate
+Query: span.op:http.client span.description:*{endpoint}*
+Group by: span.description, transaction
+Sort: -count(span.duration)
+```
+
+## Detect `isOptIn` Misuse
+
+```bash
+grep -rn "isOptIn: true" app/scripts/ ui/ --include="*.ts" --include="*.tsx"
+# Any occurrence outside the onboarding opt-in flow is suspect
+```
+
+## Data Council Contact
+
+- Slack: `#metamask-metametrics`
+- Team: `@consensys/data-council`
diff --git a/domains/analytics/skills/analytics-instrumentation/skill.md b/domains/analytics/skills/analytics-instrumentation/skill.md
new file mode 100644
index 0000000..eab3328
--- /dev/null
+++ b/domains/analytics/skills/analytics-instrumentation/skill.md
@@ -0,0 +1,89 @@
+---
+maturity: experimental
+name: analytics-instrumentation
+description: Create and update Sentry spans, MetaMetrics events, and Segment events — methodology, policies, common pitfalls
+---
+
+# Analytics Instrumentation
+
+## When To Use
+
+- Adding or modifying a MetaMetrics (Segment) event
+- Adding or modifying a Sentry performance span
+- Estimating event or span volume from production data
+- Auditing existing instrumentation for correctness
+
+---
+
+## Do Not Use When
+
+- Adding local debug logging with no telemetry destination
+- Investigating an existing Sentry error report (use `sentry-mcp-queries`)
+- Internal feature flag evaluation not surfaced as an analytics event
+
+---
+
+## Sentry Spans
+
+### Creating a Span
+
+1. **Register a named trace entry** in the repo's trace name enum before writing any span code. Unnamed spans are invisible in Sentry filters.
+2. **Use the repo's `trace()` wrapper**, not raw `Sentry.startSpan()`. Wrappers handle cross-process context propagation, active-span inheritance, and consistent tag injection.
+3. **Inherit parent automatically** — when no `parentContext` is provided, the wrapper inherits from `Sentry.getActiveSpan()`, making the new span a child of the active parent (e.g., a `pageload` span).
+
+### Updating a Span
+
+- Adding a tag: no governance required
+- Renaming a trace name enum entry: grep all callsites; update enum and references atomically
+- Changing an `op` value: breaks saved queries and dashboards — coordinate with whoever owns them
+
+---
+
+## MetaMetrics / Segment Events
+
+### Creating an Event
+
+1. **Check the event name enum** — event may already exist under a different phrasing.
+2. **Check the segment tracking plan** — event may be registered under a different name than the enum key.
+3. **Add to the enum**, then implement the `trackEvent` call.
+4. **Do NOT use `isOptIn: true` outside the onboarding opt-in flow.** It strips user identity unconditionally for all users, not just non-opted-in ones (see Reference Knowledge: metrametrics-identity).
+5. **Open a data governance review** before merging. There is usually no CI enforcement on schema registration — this step is easy to skip (see Reference Knowledge: segment-governance).
+6. **Register in the team's segment tracking plan** before shipping.
+
+### Updating an Event
+
+- Adding a property: requires governance review and schema update
+- Renaming an event: deprecate old + add new in tracking plan; coordinate on migration window
+- Removing an event: confirm no active dashboards depend on it before removing
+
+---
+
+## Volume Estimation via Sentry
+
+When direct Segment access is unavailable, estimate from Sentry production span data:
+
+1. **Find a correlated HTTP endpoint** — one that fires 1:1 with the event.
+2. **Query Sentry Traces Explorer** (aggregate mode):
+ ```
+ span.op:http.client span.description:*{endpoint}*
+ ```
+3. **Extrapolate:**
+ ```
+ estimated_actual = sampled_count × (1 / tracesSampleRate)
+ ```
+4. **Interpret as upper bound** — endpoint may have callers outside the event path.
+
+Caveats: sample population is MetaMetrics opted-in users only; verify the current `tracesSampleRate` before calculating (it changes between releases). For longer-range (30D+) or release-over-release queries, the sampled count is **not** comparable at face value — older releases are downsampled / retention-truncated and `.0` releases are sample-thin; see `sentry-mcp-queries` (Longer-Range Queries and Percentile Fidelity) and the `performance-attribution` skill.
+
+---
+
+## Common Pitfalls
+
+| Mistake | Correct Approach |
+|---------|-----------------|
+| `isOptIn: true` on post-onboarding events | Strips user identity for all users; only valid in onboarding flow |
+| Ship event without tracking-plan registration | No CI gate — add governance review explicitly to PR checklist |
+| Raw `Sentry.startSpan()` instead of the repo's `trace()` wrapper | Use the wrapper — handles cross-process context and active-span inheritance |
+| New span with no trace name enum entry | Register enum entry first; unnamed spans are invisible in Sentry filters |
+| Multiply sampled count by `tracesSampleRate` | Multiply by inverse: `sampled × (1 / rate)` |
+| Treat Sentry estimates as exact counts | Probabilistic sample — state sample size and confidence |
diff --git a/domains/analytics/skills/performance-attribution/repos/metamask-extension.md b/domains/analytics/skills/performance-attribution/repos/metamask-extension.md
new file mode 100644
index 0000000..4d0ab9e
--- /dev/null
+++ b/domains/analytics/skills/performance-attribution/repos/metamask-extension.md
@@ -0,0 +1,96 @@
+---
+repo: metamask-extension
+parent: performance-attribution
+---
+
+## Source & Project
+
+Primary source is Sentry **Trace Explorer** (not Dashboard 219877):
+
+- Project `metamask` (ID `273505`), `environment:production`
+- Mode `Aggregates`, **Group By** `release`, **Visualize** `p75(span.duration)` and `p95(span.duration)`
+- Time `90d` (primary). Dashboard 219877 (30d) is legacy/context only.
+
+## Key Transactions
+
+| Transaction | What it measures |
+|---|---|
+| `UI Startup` | Extension click → interactive UI |
+| `/home.html` | Home page render |
+| `Asset Details` | Token/NFT detail view render |
+| `/notification.html` | dApp confirmation popup (approvals/signatures) — high-frequency for power users, compounds with usage |
+
+## Query Template
+
+```
+is_transaction:true environment:production transaction:"UI Startup" (release:metamask-extension@13.11.2 OR release:metamask-extension@13.12.2 OR release:metamask-extension@13.13.1 OR release:metamask-extension@13.14.2 OR release:metamask-extension@13.15.0)
+```
+
+Swap the `transaction:"…"` value per metric; keep `statsPeriod=90d`.
+
+## Version Selection — Highest-Sample Patch Per Minor
+
+Anchor each minor line on its highest-sample patch, never the `.0`:
+
+| Minor | Patch used | Rationale |
+|---|---|---|
+| 13.11 | 13.11.2 | Highest sample count |
+| 13.12 | 13.12.2 | Highest sample count |
+| 13.13 | 13.13.1 | Highest sample count |
+| 13.14 | 13.14.2 | Highest sample count |
+| 13.15 | 13.15.0 | Current release |
+
+`.0` releases have **10–100× fewer samples** — never anchor a percentile on a `.0` when a higher patch exists in the same minor line.
+
+## 90d vs 30d — Empirical
+
+30d baselines ran **~2× higher** than 90d for the same metric (e.g. UI Startup p75 `9.39s → 3.47s` at 30d vs `4.40s → 3.34s` at 90d). Cause unconfirmed — residual-user population and/or sampling of residual traffic; **not** confirmed "power users" (no cohort segmentation). Report 90d; cite 30d only for context. Note: Sentry share links may render 30d in the UI even when the report figure is 90d — verify `statsPeriod=90d`.
+
+## Hot-Path Files
+
+| Path | Why it matters |
+|---|---|
+| `babel.config.js` | Build-time transforms (e.g. React Compiler) — broad scope |
+| `ui/selectors/*.js` | Redux selectors — run on every state change |
+| `ui/hooks/*.ts` | Hooks — component lifecycle |
+| `ui/components/` | Virtualization / render patterns |
+| `package.json` | Dependency runtime behavior + core-package bumps |
+
+## Analysis Commands
+
+```bash
+git log v13.X.X..v13.Y.Y --oneline --no-merges | wc -l # commit count between releases
+git diff v13.X.X..v13.Y.Y --stat -- ui/selectors babel.config.js # file-level change summary
+git diff v13.X.X..v13.Y.Y -- # detailed diff for one file
+git log v13.X.X..v13.Y.Y --oneline -- # commits touching specific paths
+```
+
+## Core Packages to Monitor
+
+App-repo diffs miss work shipped as version bumps. Diff `package.json`, then read each package CHANGELOG:
+
+| Package | Performance relevance |
+|---|---|
+| `@metamask/assets-controllers` | Token detection, balance fetching, NFT metadata |
+| `@metamask/transaction-controller` | Transaction state size, history storage |
+| `@metamask/network-controller` | RPC call handling, retry logic |
+
+```bash
+git diff v13.X.X..v13.Y.Y -- package.json | grep -E "@metamask/(assets-controllers|transaction-controller|network-controller)"
+```
+
+Example findings:
+
+- `@metamask/transaction-controller` v62.8.0 — deprecated `history` / `sendFlowHistory` from `TransactionMeta` → significant state-size reduction for power users (consumed in extension [#38665](https://github.com/MetaMask/metamask-extension/pull/38665)).
+- `@metamask/assets-controllers` v94.0.0 ([core #7408](https://github.com/MetaMask/core/pull/7408)) — Account API v2 → v4 for token detection → fewer RPC calls, delegated detection.
+
+## Worked Example: v13.11 → v13.15 (90d)
+
+| Metric | p75 (typical) | p95 (tail) |
+|---|---|---|
+| UI Startup | 4.40s → 3.34s (-24%) | 15.65s → 9.11s (**-42%**, -6.5s) |
+| /home.html | 1.69s → 1.19s (-30%) | 4.96s → 3.24s (-35%) |
+| Asset Details | 100ms → 47ms (**-53%**) | 287ms → 94ms (**-67%**) |
+| /notification.html | 1.36s → 1.05s (-23%) | 4.30s → 4.71s (+9%, **high variance — inconclusive**) |
+
+Most UI Startup and /home.html gains landed in 13.12 (p95 UI Startup -40% in one release); Asset Details improved across 13.14 → 13.15. Treat the per-release header deltas as measured totals and attribute individual code changes as likely contributors only.
diff --git a/domains/analytics/skills/performance-attribution/skill.md b/domains/analytics/skills/performance-attribution/skill.md
new file mode 100644
index 0000000..379dac6
--- /dev/null
+++ b/domains/analytics/skills/performance-attribution/skill.md
@@ -0,0 +1,90 @@
+---
+maturity: experimental
+name: performance-attribution
+description: Attribute release-over-release p75/p95 performance movements to specific code changes via black-box diff analysis
+---
+
+# Performance Attribution
+
+Pair a **measured** percentile movement (from Sentry Trace Explorer) with **black-box code-diff analysis** to produce confidence-rated attributions: what changed across releases, how much it moved, and why.
+
+## When To Use
+
+- Explaining a confirmed p75/p95 latency change across releases
+- Building a per-release attribution catalogue (change → confidence → metric)
+- Auditing whether a "performance initiative" actually moved a metric
+- Attributing movement that spans the app repo **and** `@metamask/*` core-package bumps
+
+## Do Not Use When
+
+- The metric movement isn't yet confirmed reliable — run query hygiene first (see `sentry-mcp-queries`: filter superseded/low-sample releases, normalize, verify stored sample size)
+- You need proof of causation — this yields *likely contributors*, not isolated causes (see Limitations)
+- Pre-merge perf review of a single PR — there is no production metric to attribute yet
+
+## Step 1 — Get the Measurement Right First
+
+Attribution is only as good as the metric. Lock these down before touching code:
+
+- **Percentile.** p75 = typical user (more stable signal). p95 = slowest 5% — *assumed* large-wallet/power users, but **not cohort-verified** (also slow hardware / poor network). Prioritize p95 when the optimization targets data size (memoization, virtualization) that disproportionately helps the tail; trust p75 as the more reliable number.
+- **Time window.** Use the **longer (90d) window as primary** — it includes traffic from when older releases were actively used, so the population is representative and comparable across releases. A 30d window over-weights residual users still lingering on old versions → inflated baselines and bigger-looking deltas between *different* populations. Report 90d; cite 30d only as context.
+- **Version selection.** Per minor line, anchor on the **highest-sample patch**, never the `.0`. `.0` releases have 10–100× fewer samples and are rollout-biased. See `sentry-mcp-queries` → *Filtering Unreliable Releases* and *Longer-Range (30D+) Queries and Percentile Fidelity*.
+
+## Step 2 — Black-Box Code Analysis
+
+Assess impact on **code content + execution frequency alone**. Deliberately ignore commit messages, PR titles/descriptions, claimed impact, and epic/initiative goals — they bias the read. Base it on: diff content, file location (→ execution frequency), algorithmic complexity, and memoization patterns.
+
+Hot-path categories — a change here can move a render metric:
+
+| Category | Why it matters |
+|---|---|
+| Build config | Build-time transforms (e.g. React Compiler) apply broadly |
+| Selectors | Run on every state change — hottest path |
+| Hooks | Affect component lifecycle / re-render frequency |
+| Components | Virtualization & render patterns |
+| Dependencies | Version bumps change runtime behavior |
+
+Pattern catalogue — what to grep for:
+
+| Pattern | Signal | Confidence |
+|---|---|---|
+| `createDeepEqualSelector` → `createSelector` + `EMPTY_ARRAY` sentinel | Removes per-change deep compares | HIGH if foundational selector |
+| Identity-function selector `(foo => foo)` → real transform | Broken memoization fixed | HIGH if many consumers |
+| In-place mutation `.sort()/.reverse()/.splice()` → spread copy | Mutation had broken all downstream memoization | HIGH |
+| Build-plugin addition with broad scope | Build-time optimization | HIGH if scope = all `ui/` |
+| O(n) string parse → O(1) lookup | Algorithmic reduction | MEDIUM — depends on call frequency |
+
+## Step 3 — Score Confidence
+
+1. **Mechanism** — how does this reduce work? (fewer re-renders / less allocation / better caching)
+2. **Frequency** — is the path hot? (selector per state change = hot)
+3. **Scope** — how many components/files does it touch?
+4. **Match** — does it target what the metric measures?
+
+| Confidence | Criteria |
+|---|---|
+| HIGH | Clear mechanism + hot path + timing matches the metric move |
+| MEDIUM | Mechanism clear, frequency or scope uncertain |
+| LOW | Indirect or infrastructure-only |
+
+## Step 4 — Don't Forget Core Packages
+
+App-repo diffs miss work shipped as `@metamask/*` version bumps — it surfaces only as a `package.json` change. For each bump between releases, read the package CHANGELOG "Changed"/"Fixed" sections for state-size reduction, caching, fewer RPC calls, batching, or data-structure/field deprecations. (Commands and the packages to watch live in the repo file.)
+
+## Reading / Writing an Attribution Catalogue
+
+- Release-header totals = the **measured** improvement for the whole release
+- Table rows = **likely contributors**, not isolated causes
+- "High confidence" = mechanism + timing + population align
+- Always keep an **Unattributed** section for movement no change explains
+- Flag high-variance metrics (a noisy confirmation-popup p95) as inconclusive, not as wins
+
+## Limitations
+
+- **Correlation, not causation** — change + improvement in the same release does not prove the change caused it
+- **Release totals, not isolated impact** — a "-44%" reflects the entire release, not one change
+- **Production variance** — user hardware and network are uncontrolled
+- **Code analysis, not runtime profiling** — based on structure, not measured execution paths
+- **p95 cohort is assumed, not verified** — no power-user segmentation
+- **Window choice changes the baseline** — always state which window a number came from
+
+For more precise attribution: per-optimization feature flags / A-B tests, CI synthetic benchmarks, and verified user-cohort segmentation.
diff --git a/domains/analytics/skills/sentry-mcp-queries/repos/metamask-extension.md b/domains/analytics/skills/sentry-mcp-queries/repos/metamask-extension.md
new file mode 100644
index 0000000..efe70ac
--- /dev/null
+++ b/domains/analytics/skills/sentry-mcp-queries/repos/metamask-extension.md
@@ -0,0 +1,60 @@
+---
+repo: metamask-extension
+parent: sentry-mcp-queries
+---
+
+## Organization and Projects
+
+```
+mcp__sentry__find_organizations → confirm org slug
+mcp__sentry__find_projects → metamask-extension (Chrome/MV3 + Firefox/MV2)
+```
+
+## Standard Filter Set for Extension Errors
+
+```
+environment:production
+installType:normal
+```
+
+Then add `dist:mv3` or `dist:mv2` to isolate by manifest.
+
+## Sample Rate
+
+Production `tracesSampleRate` = `0.0075` (0.75%) → multiplier ≈ 133×
+
+```bash
+# Verify current value before using
+grep "tracesSampleRate" app/scripts/lib/setupSentry.js
+```
+
+## Volume Estimation — Worked Example
+
+`AssetsFirstInitFetchCompleted` correlates 1:1 with `accounts.api.cx.metamask.io/v1/supportedNetworks` (fires once per init) — **not** `/v4/multiaccount/balances` (fires per account):
+
+```
+/v1/supportedNetworks: 2.6M sampled (30d) × 133 ≈ 346M event fires / month
+/v4/multiaccount/balances: ~26M sampled × 133 ≈ 3.5B balance API calls / month (per-account — NOT the event rate)
+```
+
+Lesson: pick the once-per-event endpoint or you over-count by the fan-out factor.
+
+## Common Issue Searches
+
+| What you're looking for | Query |
+|---|---|
+| Background connection errors | `is:unresolved background connection` |
+| MV3-only errors | `is:unresolved dist:mv3` |
+| Errors spiking in recent release | `is:unresolved times_seen:>100` |
+| Performance issues | `issue.category:performance` |
+
+## Tag: `dist` Values
+
+| Value | Meaning |
+|-------|---------|
+| `mv3` | Chrome (Manifest V3 — service worker) |
+| `mv2` | Firefox (Manifest V2 — background page) |
+
+## Seer Analysis Notes
+
+Seer has access to the Sentry issue, stack traces, and recent events. It does not have access to the codebase. Validate its hypothesis against the actual handler chain in the source — especially for keepalive, lifecycle, and concurrency conclusions.
diff --git a/domains/analytics/skills/sentry-mcp-queries/skill.md b/domains/analytics/skills/sentry-mcp-queries/skill.md
new file mode 100644
index 0000000..8a13457
--- /dev/null
+++ b/domains/analytics/skills/sentry-mcp-queries/skill.md
@@ -0,0 +1,129 @@
+---
+maturity: experimental
+name: sentry-mcp-queries
+description: Query Sentry via MCP — error triage, tag distribution, volume estimation, replay retrieval
+---
+
+# Sentry MCP Queries
+
+## When To Use
+
+- Investigating a production error before attributing root cause
+- Checking dist (MV3 vs MV2) error distribution
+- Estimating event or span volume from production data
+- Comparing error rates release-over-release for regression detection
+- Retrieving session replay or profiling data
+
+## Do Not Use When
+
+- The error reproduces locally with a full stack trace
+- Reading product analytics (Segment events, not Sentry errors/spans)
+- Pre-merge investigation — Sentry data is post-merge only
+
+## Setup
+
+Run once per session:
+
+```
+mcp__sentry__whoami → confirm auth
+mcp__sentry__find_organizations → org slug
+mcp__sentry__find_projects → project slug(s)
+```
+
+All subsequent tools require `organization_slug` and usually `project_slug`. Slug mismatch causes silent empty results.
+
+## Workflow: Error Triage
+
+1. `mcp__sentry__search_issues` — find by title, fingerprint, or keyword
+2. `mcp__sentry__get_issue_tag_values` — check `dist` distribution **before** attributing root cause
+3. If 99%+ one dist → platform lifecycle root cause (see `extension-errors-debugging`)
+4. `mcp__sentry__search_issue_events` — individual events for stack trace detail
+5. `mcp__sentry__analyze_issue_with_seer` — AI-assisted hypothesis (validate against code)
+
+## Workflow: Volume Estimation
+
+Segment event volume is invisible from Sentry, but a correlated `http.client` span is not. Anchor estimation on an HTTP endpoint the event's controller calls **1:1** with the event firing.
+
+1. Identify the correlated endpoint — the one that fires **once per event**, not per sub-call (e.g. a per-init call, not a per-account call). Picking a per-sub-call endpoint over-counts.
+2. `mcp__sentry__search_events` aggregate mode, filter `span.op:http.client` + endpoint
+3. Read sampled span count
+4. Extrapolate: `estimated = sampled × (1 / tracesSampleRate)`
+5. Treat as an **upper bound** — the endpoint may have callers beyond the event path. Sample population = MetaMetrics-opted-in users only (Sentry opt-in is tied to MetaMetrics). Sample rate changes — verify the current value.
+
+## Workflow: Release Comparison
+
+Compare error rates or metrics across releases for regression detection:
+
+1. `mcp__sentry__find_releases` — list releases sorted by date
+2. **Filter out unreliable releases** (see below) before comparing
+3. `mcp__sentry__search_events` with `release:12.5.0` for baseline
+4. `mcp__sentry__search_events` with `release:12.6.0` for comparison
+5. **Normalize by sessions or users** — raw counts conflate traffic changes with error rate changes:
+ ```
+ rate = events / sessions_for_that_release
+ ```
+6. Report delta against baseline with sample-size caveat
+
+## Filtering Unreliable Releases
+
+Patch releases have uneven adoption — comparing raw counts against them produces false signal. Skip a release before comparing if:
+
+| Filter | Threshold | Reason |
+|---|---|---|
+| Age since publish | < 48–72h | Browser auto-update rollout still ramping (Chrome/Firefox/Edge) |
+| Session count | < ~50% of previous stable release | Sample too small for meaningful rates |
+| Stored span count | < ~few hundred for p75, < ~few thousand for p95+ | Tail percentiles are computed over the *stored* sample — extrapolated counts hide how few events back them |
+| Superseded patch | a higher patch in the same `X.Y.*` line exists **and** the active window (`first_seen`→`last_seen`) is short | Hotfixed-past releases collect few spans, biased to early-updaters during the rollout/migration window |
+| Release stage | `dev`, `canary`, `nightly` | Non-production build — different error profile |
+| Environment | not `production` | Development / staging noise |
+| Manifest split | compare only within same `dist` | MV3 and MV2 populations have different error distributions |
+
+**Rule of thumb:** use the newest release that has ≥ 3 days of production adoption **and** session volume comparable to the previous stable release. Everything in between is hotfix noise — skip it for regression comparisons unless investigating that specific patch.
+
+## Longer-Range (30D+) Queries and Percentile Fidelity
+
+Widening the window past ~30 days to gain sample size trades it back for **fidelity loss on older releases**. Three effects compound:
+
+- **Sample-rate drift** — `tracesSampleRate` changes between releases, so absolute span counts across a 30D+ window mix different capture rates. Normalize each release by *its own* sample rate (or by sessions/users), never a single global rate.
+- **Extrapolation hides thin samples** — span datasets report sample-rate-weighted (extrapolated) counts. A release with 40 stored spans at 0.75% extrapolates to ~5,300 — a real-looking number backed by 40 events. Always check the **stored** sample count, not the extrapolated total, before trusting a release.
+- **Retention downsampling** — spans near the retention boundary are partially evicted, so an old release's count is truncated, not representative. Treat the oldest releases in a 30D+ window as lower bounds only.
+
+**For p75+ analysis** (any tail percentile — p75/p90/p95/p99), sample size *and* quality both matter:
+
+- **Size** — percentiles are computed over stored events. p50 stabilizes in the low hundreds; p75 needs more; p95/p99 need thousands of stored spans. Below that, a handful of outliers move the number — don't report a tail percentile you can't back with stored count.
+- **Quality** — rollout-window spans (first-launch, cold cache, state migration) skew the tail high. A superseded patch release's spans are disproportionately these, so its p75+ reads worse than its steady state would.
+
+**Resolving the size-vs-fidelity tension:** when a single release lacks the sample to support p75+, **collapse the patch chain** — aggregate `release:X.Y.*` across the minor line, or compare against the last *widely-adopted* patch — rather than extending the window into aged, downsampled, sample-rate-drifted territory. Reach for sample size *across adjacent stable patches inside the retention-safe window*, not by going further back in time. Use a longer (90d) window as the **primary, comparable-across-releases** source for p75/p95 and a 30d window only as **secondary context** — 30d over-weights the users still lingering on old versions and inflates baselines.
+
+For attributing a confirmed p75/p95 movement to specific code changes, see the `performance-attribution` skill.
+
+## Workflow: Replay and Profile
+
+1. `mcp__sentry__search_issue_events` — find an event ID with replay/profile
+2. `mcp__sentry__get_replay_details` / `mcp__sentry__get_profile_details` for that event ID
+
+## Tag Filters
+
+| Tag | Values | Use |
+|-----|--------|-----|
+| `dist` | `mv3`, `mv2` | Isolate by manifest version |
+| `environment` | `production`, `staging` | Exclude non-prod noise |
+| `installType` | `normal`, `development`, `sideload`, `admin` | Exclude developer-loaded builds |
+
+**Do not conflate `environment` and `installType`** — a production build can have `installType:development` if loaded unpacked.
+
+## Common Pitfalls
+
+| Mistake | Correct Approach |
+|---------|-----------------|
+| Attribute root cause before checking `dist` distribution | Check tag values first — 99%+ MV3 → lifecycle, not app logic |
+| Use raw sampled count as event volume | Multiply by `1 / tracesSampleRate` |
+| Filter `environment:development` for dev builds | Filter `installType:normal` — environment ≠ install method |
+| Skip `whoami` and guess org slug | Slug mismatch causes silent empty results |
+| Treat Seer analysis as ground truth | Use as hypothesis to validate against code/traces |
+| Compare raw event counts across releases | Normalize by sessions — traffic changes masquerade as regressions |
+| Include a <48h-old release in a regression comparison | Wait for rollout; auto-update adoption takes 2–7 days |
+| Treat every patch release as a comparison point | Most patches have low adoption — compare to the last *widely-adopted* release |
+| Trust a release's p95 because its (extrapolated) span count looks large | Check the *stored* sample — p75+ needs hundreds-to-thousands of stored events to be stable |
+| Compare span counts across a 30D+ window at face value | Normalize per-release sample rate; older releases are downsampled / retention-truncated |
+| Anchor a percentile on a `.0` release | `.0` releases have 10–100× fewer samples — use the highest-sample patch in the minor line |
diff --git a/domains/analytics/skills/sentry-quota/repos/metamask-extension.md b/domains/analytics/skills/sentry-quota/repos/metamask-extension.md
new file mode 100644
index 0000000..d3d2b57
--- /dev/null
+++ b/domains/analytics/skills/sentry-quota/repos/metamask-extension.md
@@ -0,0 +1,50 @@
+---
+repo: metamask-extension
+parent: sentry-quota
+---
+
+## File Paths
+
+| Path | Role |
+|---|---|
+| `shared/lib/trace.ts` | `TraceName` / `TraceOperation` enums = the custom-span registry; `trace({ name, op, data }, cb)` API |
+| `shared/lib/wrapper-sampling.ts` | `shouldSampleWrappers(traceId)` — the Tier-2 deterministic sub-sample gate |
+| `shared/lib/messenger-tracing.ts` | `wrapMessengerWithTracing` + `isReadOnlyAction` read-only denylist (~90% volume cut before sampling) |
+| `app/scripts/lib/createMetaRPCHandler.ts` | `rpc.handler` span — gated behind `shouldSampleWrappers` |
+| `app/scripts/lib/setupSentry.js` | global `tracesSampleRate` (`0.0075` = 0.75%) |
+
+Core controller instrumentation lives in the **`MetaMask/core`** monorepo: per-package `TraceName` in `packages//src/**/{constants/traces,utils/trace}.ts` (e.g. `bridge-controller/src/constants/traces.ts`). Controllers don't import Sentry — they call an injected `trace` callback (`traceAsControllerCallback` in the extension).
+
+## Commands
+
+```bash
+EXT=
+CORE=
+
+# Span registries (the inventory)
+rg -n 'enum TraceName' "$EXT/shared/lib/trace.ts"
+rg -n -g '**/{traces,trace}.ts' 'enum TraceName' "$CORE/packages"
+
+# Locate a culprit's emit site
+rg -n '|TraceName.' "$EXT" "$CORE/packages"
+
+# All span creation sites — then read each enclosing scope for loop/poller (fan-out)
+rg -n 'trace\(' "$EXT/app" "$EXT/shared" "$CORE/packages//src"
+
+# Gate present before the span? (absence = always-on)
+rg -n 'shouldSample|tracesSampleRate|hashBucket|Math.random'
+
+# Kill-switch present?
+rg -n 'SENTRY_[A-Z_]*DISABLED' "$EXT" "$CORE/packages//src"
+
+# PR review — added instrumentation lines only
+gh pr diff --repo MetaMask/metamask-extension \
+ | rg '^\+' | rg 'TraceName|trace\(|shouldSampleWrappers|SENTRY_.*DISABLED|op:'
+```
+
+## Architectural Notes
+
+- **Gate location differs by repo.** Extension spans go through `trace()` — gate at the call site or in the wrapper. Core controller spans go through the injected callback — gate in the package's trace util or the callback so every consumer (extension, mobile) inherits the cap.
+- **`BackgroundRpc` / `MessengerCall`** (the `TraceName` tail) are the already-gated wrapper spans from [PR #39891](https://github.com/MetaMask/metamask-extension/pull/39891) — the reference implementation of the Tier-2 sub-sample pattern and the `SENTRY_DISTRIBUTED_TRACING_DISABLED` kill-switch.
+- **Tier-0 fix path is a core PR + a patch on the extension release branch.** Controller instrumentation originates in `MetaMask/core`; the release branch is where the cherry-pick lands. The sev-1 blocker goes on the in-flight release milestone — e.g. [issue #43211](https://github.com/MetaMask/metamask-extension/issues/43211) ("Assets Controller Sentry Instrumentation exceeding quota").
+- **Spotting the culprit first:** `sentry-mcp-queries` → Volume Estimation (`span.op` aggregate × `1 / tracesSampleRate`) ranks span contributors; this skill takes over once you have the offending span name.
diff --git a/domains/analytics/skills/sentry-quota/skill.md b/domains/analytics/skills/sentry-quota/skill.md
new file mode 100644
index 0000000..1602ca5
--- /dev/null
+++ b/domains/analytics/skills/sentry-quota/skill.md
@@ -0,0 +1,78 @@
+---
+maturity: experimental
+name: sentry-quota
+description: Catch quota-risky Sentry span instrumentation in code and PRs — fan-out × ungated × no-kill-switch — before it blows the span budget
+---
+
+# Sentry Span Quota Guard
+
+Find and fix custom Sentry span instrumentation that blows the project span budget. Operates on **code and PRs**, not Sentry dashboards — you spot the culprit in Sentry (`sentry-mcp-queries`), this skill fixes it in code.
+
+## When To Use
+
+- A PR adds custom Sentry spans (`trace()` calls / `TraceName` entries) — review it before merge.
+- A custom span/transaction dominates span volume in Sentry — locate where it's emitted and fix it.
+- Auditing controllers/UI for always-on, fan-out-prone instrumentation.
+- A custom span is the top span-count contributor and must be cut fast (release blocker).
+
+## Do Not Use When
+
+- Reading the live span counts themselves — that's `sentry-mcp-queries` (Volume Estimation).
+- Product-analytics events (Segment / `trackEvent`) — that's `analytics-instrumentation` + `segment-governance`.
+- The span is already behind a per-trace sample gate **and** a kill-switch — already mitigated.
+
+## Breach Triad
+
+A custom span is a quota risk when these stack. The first three together are the breach profile.
+
+| Signal | Static signature | Why it blows quota |
+|---|---|---|
+| **Fan-out** | span created in a loop / `.map` / `.forEach` / per-asset / per-account / per-chain / poller | N spans per trace, not 1 |
+| **Always-on** | no `tracesSampleRate` sub-rate, no hash gate before the span | every qualifying call emits |
+| **No kill-switch** | not guarded by an env flag | disabling needs a release, not a config flip |
+| Hot path | data-source / update-pipeline / network callback, not a discrete user action | high call frequency |
+
+Low fan-out + discrete user action + already gated = fine. Don't flag healthy spans.
+
+## Workflow
+
+### PR review (pre-merge gate)
+1. `gh pr diff ` — scan **added** lines for new `TraceName` entries and `trace(` call sites.
+2. Score each new span against the breach triad: is the enclosing scope a loop/poller? is there a gate? a kill-switch?
+3. Block if a new always-on span has no gate — require a sub-sample gate (`span-sub-sampling`) before merge. Cheaper than a post-ship cherry-pick.
+4. If the diff adds no `trace(` sites and no `TraceName` entries → "no new instrumentation, no quota risk", stop.
+
+### Locate (incident)
+1. Grep the span name / `TraceName.X` across the consuming repo **and** the controller package source.
+2. Open the call site; read the enclosing scope for the fan-out verdict (loop/poller?).
+3. **No grep hits ≠ safe** — the culprit may be on a release ref not checked out. Verify the package version / `gh pr checkout` the shipping ref before concluding clean.
+
+### Audit
+1. Sweep the span registries (`TraceName` enums) + `trace(` call sites.
+2. Rank by breach triad — surface ungated × hot-path × fan-out first.
+
+### Mitigate
+Pick the lowest tier that stops the bleed.
+
+## Mitigation Ladder
+
+| Tier | When | Action |
+|---|---|---|
+| **0 — Immediate** | a span fans out and is actively breaching on the live release | disable the `trace()` call at source (or env-guard it) + **cherry-pick to the release branch** + file a sev-1 release blocker on the in-flight release milestone |
+| **1 — Release containment** | spike concentrated in an old, already-patched release with lingering users | Sentry **inbound filter** dropping `release:` spans + force-update. The only dashboard action. Filters target a whole release, not one span — don't filter a release you still want data from |
+| **2 — Durable** | the span is justified long-term but ungated | deterministic `traceId`-hash sub-sample gate before the span (`span-sub-sampling`) |
+| **3 — Wrong tool** | the metric needs full fidelity; sampling loses the signal | move the metric off trace spans — they are the wrong substrate for always-on high-cardinality metrics. Segment is the usual target, but it has its own ungoverned billing gap (`segment-governance`), so it is not a free lunch |
+
+Tier 0 + 1 stop the bleed now; Tier 2 is the follow-up so the metric returns.
+
+## Common Pitfalls
+
+| Mistake | Correct approach |
+|---|---|
+| Per-call random sampling (`Math.random()` per span) | Deterministic `traceId`-hash bucket — all spans in a trace kept-or-dropped together, clean waterfalls |
+| Gate the span in Sentry config | Gate at the call site; for an injected-callback controller span, gate in the callback so every consumer inherits the cap |
+| Inbound-filter a release you still need data from | Filters drop the whole release — fix in code (Tier 0/2) instead |
+| "No grep hits, so it's safe" | The culprit may be on a release ref not checked out — verify the version/ref |
+| Disable the span on `main` only | Cherry-pick to the active release branch — `main` alone leaves the live release breaching |
+| Treat "move to Segment" as free | Segment events ship without CI governance or billing review (`segment-governance`) |
+| Ship new always-on instrumentation with no kill-switch | Add an env disable flag on day one — turns a future cut into a config flip, not a cherry-pick |