From 88a5e005c137269468651d4708d15e5bc3be3318 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Tue, 1 Sep 2026 09:51:55 -0400 Subject: [PATCH 1/8] Add a `data` domain for product-analytics knowledge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `metrametrics-identity` and `segment-governance`, split out of the domain that was called `analytics` and is becoming `observability` in #76. Traces, errors and span cost are one subject; product analytics is another, and with MetaMetrics migrating to `AnalyticsController` in both clients this half needs a name that outlives the tool. Knowledge only, no skill yet. `tools/install` copies domain knowledge beside each skill in its domain, so nothing here installs until the domain gains one — stated in the body rather than discovered at install time. --- .../data/knowledge/metrametrics-identity.md | 41 +++++++++++++++++++ domains/data/knowledge/segment-governance.md | 40 ++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 domains/data/knowledge/metrametrics-identity.md create mode 100644 domains/data/knowledge/segment-governance.md diff --git a/domains/data/knowledge/metrametrics-identity.md b/domains/data/knowledge/metrametrics-identity.md new file mode 100644 index 00000000..4c88b558 --- /dev/null +++ b/domains/data/knowledge/metrametrics-identity.md @@ -0,0 +1,41 @@ +--- +name: metrametrics-identity +domain: data +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/data/knowledge/segment-governance.md b/domains/data/knowledge/segment-governance.md new file mode 100644 index 00000000..bd58d6b5 --- /dev/null +++ b/domains/data/knowledge/segment-governance.md @@ -0,0 +1,40 @@ +--- +name: segment-governance +domain: data +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 From 5b35a809840f67d3bcced83a3c250e657e48dabc Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Mon, 14 Sep 2026 07:49:51 -0400 Subject: [PATCH 2/8] Describe anonymous events by `excludeMetaMetricsId`, the mechanism on `main` The file quoted an `isOptIn` branch in `MetaMetricsController` that metamask-extension#42885 (integrate analytics controller) removed. Anonymity is now set per event by `excludeMetaMetricsId`, including a default for event names starting with `Send` or `Confirm`, and read by the platform adapter. Citations are pinned to extension `c31416a`. --- .../data/knowledge/metrametrics-identity.md | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/domains/data/knowledge/metrametrics-identity.md b/domains/data/knowledge/metrametrics-identity.md index 4c88b558..d5d3f0d2 100644 --- a/domains/data/knowledge/metrametrics-identity.md +++ b/domains/data/knowledge/metrametrics-identity.md @@ -1,41 +1,38 @@ --- name: metrametrics-identity domain: data -description: isOptIn:true unconditionally strips user identity in MetaMetricsController — always sends as anonymous ID +description: Which extension MetaMetrics events are sent without the user's analytics ID, set by the excludeMetaMetricsId option or by an event name starting with Send or Confirm --- -# MetaMetrics Identity Stripping +# MetaMetrics Identity on Anonymous Events + +Read at `metamask-extension` [`c31416a`](https://github.com/MetaMask/metamask-extension/commit/c31416a47811bc4355a904925021a30f4c5564bb). ## The Mechanism -In `MetaMetricsController` (`app/scripts/controllers/metametrics-controller.ts`): +Each event is sent under the user's analytics ID or under one shared anonymous ID, decided per event: -```typescript -if (excludeMetaMetricsId || (isOptIn && !metaMetricsIdOverride)) { - idType = 'anonymousId'; - idValue = METAMETRICS_ANONYMOUS_ID; // 0x0000000000000000 -} -``` +1. The background `trackEvent` reads `excludeMetaMetricsId` from the event's build options. When it is true, [`applyAnonymousEventOptions`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L303-L333) sets an `anonymous: true` marker on the event's properties. +2. The platform adapter reads the marker. A marked event is sent with `anonymousId` set to `METAMETRICS_ANONYMOUS_ID` ([`0x0000000000000000`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/shared/constants/metametrics.ts#L777)), and an unmarked one with `userId` set to the analytics ID ([`platform-adapter.ts:345-355`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L345-L355)). +3. [`enrichEventProperties`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L137-L172) adds the profile identity properties to unmarked events only, and deletes `profile_id`, `canonical_profile_id` and the marker from marked ones. +4. Some events are renamed on the anonymous path through [`anonymousEventNameOverrides`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L246), for example the transaction lifecycle events and `SignatureRequested`. -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 +No event is sent while basic functionality is off ([`analytics.ts:351`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L351)). -This is **unconditional** — it applies to fully opted-in users with valid IDs, not just anonymous users. +## The Name Default -## Intended Use +`excludeMetaMetricsId` is not only the caller's choice. An event whose name matches `/^send|^confirm/iu` is marked anonymous unless the caller passes `excludeMetaMetricsId: false` ([`analytics.ts:321-324`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L321-L324)). The comment above it calls the match a carry-over from the previous implementation. -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. +So a new event whose name begins with `Send` or `Confirm` loses user-level attribution by default. Every such event shares one ID in Segment, and user-level dimensions such as account type or feature flags cannot be joined to it. -## The Misuse Pattern +## Sensitive Properties -Post-opt-in `trackEvent` calls with `{ isOptIn: true }` without `metaMetricsIdOverride`. Defeats the purpose of Segment user-level dimensions (account types, feature flags). +An event carrying `sensitiveProperties` cannot also set `excludeMetaMetricsId: true`. `applyAnonymousEventOptions` throws ([`analytics.ts:307-315`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L307-L315)), and `trackEvent` catches the error and reports it to Sentry ([`analytics.ts:391-392`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L391-L392)), so the event is not sent. ## Detection ```bash -grep -r "isOptIn: true" app/scripts/ ui/ --include="*.ts" --include="*.tsx" +git grep -n 'excludeMetaMetricsId: true' -- app shared ui ':!*.test.*' ``` -Any occurrence outside `creation-successful.tsx` (or the onboarding flow) is suspect. +At `c31416a` this returns the phishing-detection, `eth_requestAccounts` and MetaMetrics data-deletion call sites, among others. A new hit, or a new event name beginning with `Send` or `Confirm`, drops the analytics ID from that event. Check that this is intended. From 798aff35b288066bdd4549df7d05ccfc4f8b7d16 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Mon, 14 Sep 2026 08:53:23 -0400 Subject: [PATCH 3/8] Say that an event with sensitive properties goes out under both IDs `@metamask/analytics-controller` sends it once identified without them and once anonymous with them, so a count over both rows counts it twice. The analytics ID is a hex string rather than a UUIDv4, and Sentry carries the same value as `user.id`. --- domains/data/knowledge/metrametrics-identity.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/domains/data/knowledge/metrametrics-identity.md b/domains/data/knowledge/metrametrics-identity.md index d5d3f0d2..846b5f7b 100644 --- a/domains/data/knowledge/metrametrics-identity.md +++ b/domains/data/knowledge/metrametrics-identity.md @@ -10,10 +10,10 @@ Read at `metamask-extension` [`c31416a`](https://github.com/MetaMask/metamask-ex ## The Mechanism -Each event is sent under the user's analytics ID or under one shared anonymous ID, decided per event: +Each event is sent under the user's analytics ID or under one shared anonymous ID, decided per event, except that an event with sensitive properties is sent under both (see Sensitive Properties): 1. The background `trackEvent` reads `excludeMetaMetricsId` from the event's build options. When it is true, [`applyAnonymousEventOptions`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L303-L333) sets an `anonymous: true` marker on the event's properties. -2. The platform adapter reads the marker. A marked event is sent with `anonymousId` set to `METAMETRICS_ANONYMOUS_ID` ([`0x0000000000000000`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/shared/constants/metametrics.ts#L777)), and an unmarked one with `userId` set to the analytics ID ([`platform-adapter.ts:345-355`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L345-L355)). +2. The platform adapter reads the marker. A marked event is sent with `anonymousId` set to `METAMETRICS_ANONYMOUS_ID` ([`0x0000000000000000`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/shared/constants/metametrics.ts#L777)), and an unmarked one with `userId` set to the analytics ID ([`platform-adapter.ts:345-355`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L345-L355)). The analytics ID is a hex string rather than a UUIDv4, so the adapter sets `skipUUIDv4Check: true` ([`platform-adapter.ts:308-321`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L308-L321)). Sentry attaches the same value to its events as `user.id` ([`sentry-metametrics.ts:47-51`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/lib/sentry-metametrics.ts#L47-L51)), so an identified Segment row and a Sentry event from one install carry one ID. 3. [`enrichEventProperties`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L137-L172) adds the profile identity properties to unmarked events only, and deletes `profile_id`, `canonical_profile_id` and the marker from marked ones. 4. Some events are renamed on the anonymous path through [`anonymousEventNameOverrides`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L246), for example the transaction lifecycle events and `SignatureRequested`. @@ -29,6 +29,8 @@ So a new event whose name begins with `Send` or `Confirm` loses user-level attri An event carrying `sensitiveProperties` cannot also set `excludeMetaMetricsId: true`. `applyAnonymousEventOptions` throws ([`analytics.ts:307-315`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L307-L315)), and `trackEvent` catches the error and reports it to Sentry ([`analytics.ts:391-392`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L391-L392)), so the event is not sent. +Otherwise an event with sensitive properties is sent twice: once under the analytics ID without them, and once with them and the anonymous marker, under the anonymous name where `anonymousEventNameOverrides` has one ([`AnalyticsController.ts:1452-1483`](https://github.com/MetaMask/core/blob/d0487f6f68af1bb9be22f5aa4eb72e036b0f4452/packages/analytics-controller/src/AnalyticsController.ts#L1452-L1483) in `@metamask/analytics-controller` 2.1.0, with the feature enabled at [`analytics-controller-init.ts:59`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/messenger-client-init/analytics-controller-init.ts#L59)). A count over both rows counts the event twice. + ## Detection ```bash From a5183cb2c5b15a037666916046b03d5b3fe3c40c Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Tue, 15 Sep 2026 10:07:50 -0400 Subject: [PATCH 4/8] Say which ID the non-sensitive row of a sensitive-properties event goes out under --- domains/data/knowledge/metrametrics-identity.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domains/data/knowledge/metrametrics-identity.md b/domains/data/knowledge/metrametrics-identity.md index 846b5f7b..da04845f 100644 --- a/domains/data/knowledge/metrametrics-identity.md +++ b/domains/data/knowledge/metrametrics-identity.md @@ -10,7 +10,7 @@ Read at `metamask-extension` [`c31416a`](https://github.com/MetaMask/metamask-ex ## The Mechanism -Each event is sent under the user's analytics ID or under one shared anonymous ID, decided per event, except that an event with sensitive properties is sent under both (see Sensitive Properties): +Each event is sent under the user's analytics ID or under one shared anonymous ID, decided per event, except that an event with sensitive properties is sent twice (see Sensitive Properties): 1. The background `trackEvent` reads `excludeMetaMetricsId` from the event's build options. When it is true, [`applyAnonymousEventOptions`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L303-L333) sets an `anonymous: true` marker on the event's properties. 2. The platform adapter reads the marker. A marked event is sent with `anonymousId` set to `METAMETRICS_ANONYMOUS_ID` ([`0x0000000000000000`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/shared/constants/metametrics.ts#L777)), and an unmarked one with `userId` set to the analytics ID ([`platform-adapter.ts:345-355`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L345-L355)). The analytics ID is a hex string rather than a UUIDv4, so the adapter sets `skipUUIDv4Check: true` ([`platform-adapter.ts:308-321`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/platform-adapter.ts#L308-L321)). Sentry attaches the same value to its events as `user.id` ([`sentry-metametrics.ts:47-51`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/lib/sentry-metametrics.ts#L47-L51)), so an identified Segment row and a Sentry event from one install carry one ID. @@ -29,7 +29,7 @@ So a new event whose name begins with `Send` or `Confirm` loses user-level attri An event carrying `sensitiveProperties` cannot also set `excludeMetaMetricsId: true`. `applyAnonymousEventOptions` throws ([`analytics.ts:307-315`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L307-L315)), and `trackEvent` catches the error and reports it to Sentry ([`analytics.ts:391-392`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/controllers/analytics/analytics.ts#L391-L392)), so the event is not sent. -Otherwise an event with sensitive properties is sent twice: once under the analytics ID without them, and once with them and the anonymous marker, under the anonymous name where `anonymousEventNameOverrides` has one ([`AnalyticsController.ts:1452-1483`](https://github.com/MetaMask/core/blob/d0487f6f68af1bb9be22f5aa4eb72e036b0f4452/packages/analytics-controller/src/AnalyticsController.ts#L1452-L1483) in `@metamask/analytics-controller` 2.1.0, with the feature enabled at [`analytics-controller-init.ts:59`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/messenger-client-init/analytics-controller-init.ts#L59)). A count over both rows counts the event twice. +Otherwise an event with sensitive properties is sent twice: once without them, under whichever ID the event would otherwise get (the anonymous ID when the name default has marked it), and once with them and the anonymous marker, under the anonymous name where `anonymousEventNameOverrides` has one ([`AnalyticsController.ts:1452-1483`](https://github.com/MetaMask/core/blob/d0487f6f68af1bb9be22f5aa4eb72e036b0f4452/packages/analytics-controller/src/AnalyticsController.ts#L1452-L1483) in `@metamask/analytics-controller` 2.1.0, with the feature enabled at [`analytics-controller-init.ts:59`](https://github.com/MetaMask/metamask-extension/blob/c31416a47811bc4355a904925021a30f4c5564bb/app/scripts/messenger-client-init/analytics-controller-init.ts#L59)). A count over both rows counts the event twice. ## Detection From 64e523ec120080c9a8cde561477aaf1d5bb66074 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Tue, 15 Sep 2026 16:15:53 -0400 Subject: [PATCH 5/8] Move the product-analytics knowledge into `platform`, beside the `analytics` skill --- domains/observability/skills/instrumentation/skill.md | 4 ++-- domains/observability/skills/sentry-quota/skill.md | 6 +++--- .../{data => platform}/knowledge/metrametrics-identity.md | 2 +- domains/{data => platform}/knowledge/segment-governance.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename domains/{data => platform}/knowledge/metrametrics-identity.md (99%) rename domains/{data => platform}/knowledge/segment-governance.md (98%) diff --git a/domains/observability/skills/instrumentation/skill.md b/domains/observability/skills/instrumentation/skill.md index 9c88000e..83af6af0 100644 --- a/domains/observability/skills/instrumentation/skill.md +++ b/domains/observability/skills/instrumentation/skill.md @@ -47,8 +47,8 @@ description: Create and update Sentry spans, MetaMetrics events, and Segment eve 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. **Pass `excludeMetaMetricsId: true` only for an event that must not carry the user's identity.** It sends the event under the shared anonymous id and drops the profile ids, for every user, not only those who have not opted in. Event names matching `/^send|^confirm/iu` get it by default unless the caller passes `excludeMetaMetricsId: false` (see data domain `knowledge/metrametrics-identity.md`). -5. **Open a data governance review** before merging. There is usually no CI enforcement on schema registration — this step is easy to skip (see data domain `knowledge/segment-governance.md`). +4. **Pass `excludeMetaMetricsId: true` only for an event that must not carry the user's identity.** It sends the event under the shared anonymous id and drops the profile ids, for every user, not only those who have not opted in. Event names matching `/^send|^confirm/iu` get it by default unless the caller passes `excludeMetaMetricsId: false` (see platform domain `knowledge/metrametrics-identity.md`). +5. **Open a data governance review** before merging. There is usually no CI enforcement on schema registration — this step is easy to skip (see platform domain `knowledge/segment-governance.md`). 6. **Register in the team's segment tracking plan** before shipping. ### Updating an Event diff --git a/domains/observability/skills/sentry-quota/skill.md b/domains/observability/skills/sentry-quota/skill.md index be000d0c..245118fe 100644 --- a/domains/observability/skills/sentry-quota/skill.md +++ b/domains/observability/skills/sentry-quota/skill.md @@ -18,7 +18,7 @@ Find and fix custom Sentry span instrumentation that blows the project span budg ## Do Not Use When - Reading the live span counts themselves — that's `sentry-mcp-queries` (Volume Estimation). -- Product-analytics events (Segment / `trackEvent`) — that's `instrumentation`, with data domain `knowledge/segment-governance.md` for Segment governance. +- Product-analytics events (Segment / `trackEvent`) — that's `instrumentation`, with platform domain `knowledge/segment-governance.md` for Segment governance. - The span is already behind a per-trace sample gate **and** a kill-switch — already mitigated. - Error volume. Errors are metered separately from spans and transactions, so no change here moves the error quota. @@ -67,7 +67,7 @@ Pick the lowest tier that stops the bleed. | **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. A sampler fix in a newer build does not change that release's rates unless it reads its rate remotely | 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. There is no inbound filter by transaction name, only a fixed health-check one. Filtered events do not consume quota, so confirm the drop in the filtered outcomes (`stats_v2` grouped by `reason`), not in Explore. It is not instant: one recorded release filter took 3.8 days from filing to taking effect | | **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 its events can ship unregistered, with no CI check and no billing review (data domain `knowledge/segment-governance.md`), so it is not a free lunch | +| **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 its events can ship unregistered, with no CI check and no billing review (platform domain `knowledge/segment-governance.md`), so it is not a free lunch | Tier 0 + 1 stop the bleed; Tier 2 is the follow-up so the metric returns. @@ -82,7 +82,7 @@ Tier 0 + 1 stop the bleed; Tier 2 is the follow-up so the metric returns. | 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 (data domain `knowledge/segment-governance.md`) | +| Treat "move to Segment" as free | Segment events ship without CI governance or billing review (platform domain `knowledge/segment-governance.md`) | | 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 | | An optional `trace?` param passes review because it emits nothing | It is a dormant fan-out — it detonates when any caller supplies the argument. Remove the *param*, not just the argument, so one line can't re-arm it. | | Disable one entry point of a multi-path change | One change can reach the backend by more than one path (a controller callback *and* a selector param). Audit every entry point it added, not just the one that fired. | diff --git a/domains/data/knowledge/metrametrics-identity.md b/domains/platform/knowledge/metrametrics-identity.md similarity index 99% rename from domains/data/knowledge/metrametrics-identity.md rename to domains/platform/knowledge/metrametrics-identity.md index da04845f..3696b6bf 100644 --- a/domains/data/knowledge/metrametrics-identity.md +++ b/domains/platform/knowledge/metrametrics-identity.md @@ -1,6 +1,6 @@ --- name: metrametrics-identity -domain: data +domain: platform description: Which extension MetaMetrics events are sent without the user's analytics ID, set by the excludeMetaMetricsId option or by an event name starting with Send or Confirm --- diff --git a/domains/data/knowledge/segment-governance.md b/domains/platform/knowledge/segment-governance.md similarity index 98% rename from domains/data/knowledge/segment-governance.md rename to domains/platform/knowledge/segment-governance.md index bd58d6b5..07c6bde9 100644 --- a/domains/data/knowledge/segment-governance.md +++ b/domains/platform/knowledge/segment-governance.md @@ -1,6 +1,6 @@ --- name: segment-governance -domain: data +domain: platform description: Segment event governance via segment-schema is advisory — no CI enforcement prevents unregistered events from shipping --- From 888aa10792f9c0d7c4875593c08e6730d5aa90ab Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 16 Sep 2026 10:51:59 -0400 Subject: [PATCH 6/8] Give `analytics` a MetaMask Extension overlay and move event guidance into it --- CHANGELOG.md | 5 + README.md | 2 +- .../repos/metamask-extension.md | 18 +- .../skills/instrumentation/skill.md | 29 +-- .../skills/sentry-quota/skill.md | 2 +- .../analytics/repos/metamask-extension.md | 188 ++++++++++++++++++ domains/platform/skills/analytics/skill.md | 9 +- 7 files changed, 205 insertions(+), 48 deletions(-) create mode 100644 domains/platform/skills/analytics/repos/metamask-extension.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d552444..589762ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `navigation` skill with a repo-agnostic base and a MetaMask Mobile overlay for `Routes` and `NavigationService`. Marked `base: true` so it installs even when its domain is filtered out. - Add `feature-flags` skill with a repo-agnostic base and a MetaMask Mobile overlay for version-gated remote flags. Marked `base: true` so it installs even when its domain is filtered out. ([#147](https://github.com/MetaMask/skills/pull/147)) - Add `analytics` skill (`platform/analytics`, moved from `coding`) with a repo-agnostic base and a MetaMask Mobile overlay for the canonical tracking API. Marked `base: true` so it installs even when its domain is filtered out. ([#140](https://github.com/MetaMask/skills/pull/140)) +- Add a MetaMask Extension overlay to `analytics` for MetaMetrics and Segment events, and the `metrametrics-identity` and `segment-governance` knowledge files to the `platform` domain, which now install beside `analytics` in both repos. ([#143](https://github.com/MetaMask/skills/pull/143)) + +### Changed + +- Move MetaMetrics and Segment event guidance from `observability/instrumentation` to `platform/analytics`. `instrumentation` now covers Sentry spans and volume estimation from Sentry span data. ([#143](https://github.com/MetaMask/skills/pull/143)) ## [0.3.1] diff --git a/README.md b/README.md index 1e82a148..73c4545b 100644 --- a/README.md +++ b/README.md @@ -391,7 +391,7 @@ homepage) are preserved through install — only `name`, `description`, `--exclude` / `SKILLS_EXCLUDE` still wins. The maturity filter runs before the base bypass, so `--maturity stable` drops a `base: true` experimental skill. A skill with a `repos/` directory and no overlay for `--repo` is skipped -(this `analytics` skill installs for Mobile and is skipped for Extension). +(the `navigation` skill installs for Mobile and is skipped for Extension). The 1,536-character ceiling is a repo budget rather than an operator limit — the description is always-on context for every installed skill, so it is capped diff --git a/domains/observability/skills/instrumentation/repos/metamask-extension.md b/domains/observability/skills/instrumentation/repos/metamask-extension.md index 33d06bc3..0b4fc599 100644 --- a/domains/observability/skills/instrumentation/repos/metamask-extension.md +++ b/domains/observability/skills/instrumentation/repos/metamask-extension.md @@ -9,12 +9,9 @@ parent: instrumentation |---------|------| | Sentry trace wrapper | `shared/lib/trace.ts` | | Trace name enum | `shared/lib/trace.ts` → `TraceName` | -| MetaMetrics controller | `app/scripts/controllers/metametrics-controller.ts` | -| Anonymous-event marking (`excludeMetaMetricsId`) | `app/scripts/controllers/analytics/analytics.ts` → `applyAnonymousEventOptions()` | -| Event enum | `shared/constants/metametrics.ts` → `MetaMetricsEventName` | +| Traces buffered until MetaMetrics opt-in | `app/scripts/controllers/metametrics-controller.ts` → `bufferedTrace()` | | Sentry setup + sample rate | `app/scripts/lib/setupSentry.js` → `getTracesSampleRate()` | | Sentry `user.id` (set to the MetaMetrics `analyticsId`) | `app/scripts/lib/sentry-metametrics.ts` → `metaMetricsIntegration()` | -| Segment tracking plan | `Consensys/segment-schema` → `tracking-plans/metamask-extension.yaml`, which lists event libraries. Event definitions live in `libraries/events//` | ## Cross-Process Context (UI → Background) @@ -53,16 +50,3 @@ Query: span.op:http.client span.description:*{endpoint}* Group by: span.description, transaction Sort: -count(span.duration) ``` - -## Detect `excludeMetaMetricsId` Misuse - -```bash -grep -rn "excludeMetaMetricsId: true" app/ ui/ shared/ --include="*.ts" --include="*.tsx" --include="*.js" -# Each hit sends its event under the shared anonymous id. Confirm the event must not carry identity. -# Event names matching /^send|^confirm/iu are anonymous by default: check new event names too. -``` - -## Data Council Contact - -- Slack: `#metamask-metametrics` -- Team: `@consensys/data-council` diff --git a/domains/observability/skills/instrumentation/skill.md b/domains/observability/skills/instrumentation/skill.md index 83af6af0..6f7d3db2 100644 --- a/domains/observability/skills/instrumentation/skill.md +++ b/domains/observability/skills/instrumentation/skill.md @@ -1,22 +1,22 @@ --- maturity: experimental name: instrumentation -description: Create and update Sentry spans, MetaMetrics events, and Segment events — methodology, policies, common pitfalls +description: Create and update Sentry performance spans, and estimate span or event volume from Sentry span data — methodology, policies, common pitfalls --- -# Analytics Instrumentation +# Sentry Span 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 +- Auditing existing span instrumentation for correctness --- ## Do Not Use When +- Adding or modifying a MetaMetrics / Segment event (use the `analytics` skill, `platform/analytics`) - 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 @@ -40,25 +40,6 @@ description: Create and update Sentry spans, MetaMetrics events, and Segment eve --- -## 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. **Pass `excludeMetaMetricsId: true` only for an event that must not carry the user's identity.** It sends the event under the shared anonymous id and drops the profile ids, for every user, not only those who have not opted in. Event names matching `/^send|^confirm/iu` get it by default unless the caller passes `excludeMetaMetricsId: false` (see platform domain `knowledge/metrametrics-identity.md`). -5. **Open a data governance review** before merging. There is usually no CI enforcement on schema registration — this step is easy to skip (see platform domain `knowledge/segment-governance.md`). -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: @@ -79,8 +60,6 @@ Caveats: sample population is MetaMetrics opted-in users only. The extension's S | Mistake | Correct Approach | |---------|-----------------| -| `excludeMetaMetricsId: true` on an event that needs user identity | It sends the event under the shared anonymous id for every user. Reserve it for events that must be anonymous | -| 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 a span `count()` by `1 / tracesSampleRate` | `count()` is already extrapolated, so read it as the estimate | diff --git a/domains/observability/skills/sentry-quota/skill.md b/domains/observability/skills/sentry-quota/skill.md index 245118fe..7505ed88 100644 --- a/domains/observability/skills/sentry-quota/skill.md +++ b/domains/observability/skills/sentry-quota/skill.md @@ -18,7 +18,7 @@ Find and fix custom Sentry span instrumentation that blows the project span budg ## Do Not Use When - Reading the live span counts themselves — that's `sentry-mcp-queries` (Volume Estimation). -- Product-analytics events (Segment / `trackEvent`) — that's `instrumentation`, with platform domain `knowledge/segment-governance.md` for Segment governance. +- Product-analytics events (Segment / `trackEvent`) — that's the `analytics` skill (`platform/analytics`), with platform domain `knowledge/segment-governance.md` for Segment governance. - The span is already behind a per-trace sample gate **and** a kill-switch — already mitigated. - Error volume. Errors are metered separately from spans and transactions, so no change here moves the error quota. diff --git a/domains/platform/skills/analytics/repos/metamask-extension.md b/domains/platform/skills/analytics/repos/metamask-extension.md new file mode 100644 index 00000000..ff79ed36 --- /dev/null +++ b/domains/platform/skills/analytics/repos/metamask-extension.md @@ -0,0 +1,188 @@ +--- +repo: metamask-extension +parent: analytics +--- + +# Analytics — MetaMask Extension + +Read at `metamask-extension` [`e81ed46`](https://github.com/MetaMask/metamask-extension/commit/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f). A/B enrichment: `docs/ab-testing.md`. How an event loses the user's analytics ID: `knowledge/metrametrics-identity.md`. Tracking-plan governance and Data Council contacts: `knowledge/segment-governance.md`. + +## Canonical API + +| Role | Path | +|------|------| +| Helper (UI) | `ui/hooks/useAnalytics.ts` → `useAnalytics` | +| Helper (background) | `app/scripts/controllers/analytics/index.ts` → `trackEvent`, `createEventBuilder` | +| Helper (Redux thunks) | `ui/store/actions.ts` → `trackAnalyticsEvent` | +| Event builder | `shared/lib/analytics/create-event-builder.ts` → `createEventBuilder` | +| Event names | `shared/constants/metametrics.ts` → `MetaMetricsEventName` | +| Categories | `shared/constants/metametrics.ts` → `MetaMetricsEventCategory` | +| Anonymous-event marking | `app/scripts/controllers/analytics/analytics.ts` → `applyAnonymousEventOptions` | +| A/B registry | `shared/lib/ab-testing/ab-test-analytics.ts` → `AB_TEST_ANALYTICS_MAPPINGS` | +| Tracking plan | `Consensys/segment-schema` → `tracking-plans/metamask-extension.yaml`, which lists event libraries. Event definitions live in `libraries/events//` | + +`useAnalytics()` returns `trackEvent` and `createEventBuilder` ([`useAnalytics.ts:28-31`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/ui/hooks/useAnalytics.ts#L28-L31)). Its `trackEvent` sends the built event to the background through `trackAnalyticsEvent`, which the background maps to `trackEvent` ([`metamask-controller.js:3528`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/app/scripts/metamask-controller.js#L3528)). + +Every path ends in the background `trackEvent` ([`analytics.ts:344-394`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/app/scripts/controllers/analytics/analytics.ts#L344-L394)). It merges the options given to `.build()` with any passed beside the event, applies the anonymous marker, and sends through `AnalyticsController:trackEvent` (`MetricsOptOut` takes a separate path). It catches every error and reports it to Sentry, and the UI hook discards a failed send ([`useAnalytics.ts:56`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/ui/hooks/useAnalytics.ts#L56)), so a dropped event raises nothing at the call site. + +`MetaMetricsContext` in `ui/contexts/metametrics.tsx` also exposes a `trackEvent`, which takes the older `{ event, category, properties }` payload and builds the same event ([`metametrics.tsx:177-222`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/ui/contexts/metametrics.tsx#L177-L222)). Event call sites use `useAnalytics`, and the context serves buffered traces ([metamask-extension#44380 (remove MetaMetricsController shims and finish background migration)](https://github.com/MetaMask/metamask-extension/pull/44380)). + +## Requirements + +- UI: `useAnalytics` from `ui/hooks/useAnalytics.ts` +- Background: `trackEvent` and `createEventBuilder` from `app/scripts/controllers/analytics` +- Redux thunks in `ui/store/actions.ts`: `trackAnalyticsEvent` +- Event names from `MetaMetricsEventName`, categories from `MetaMetricsEventCategory` +- Properties via `.addProperties(...)`, category via `.addCategory(...)`, options via `.build(options)`. `.build()` accepts `excludeMetaMetricsId`, `matomoEvent`, `environmentType`, `page` and `referrer` ([`create-event-builder.ts:15-22`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/lib/analytics/create-event-builder.ts#L15-L22)) +- Page title: `useAnalytics` has no `contextPropsIntoEventProperties` option. Add `[MetaMetricsContextProp.PageTitle]: segmentContext.page?.title` to the properties, with `segmentContext` from `useSegmentContext()` ([`about-info.tsx:40-50`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/ui/pages/settings/about-tab/about-info.tsx#L40-L50)) + +An event with `addSensitiveProperties` is sent twice, and cannot also set `excludeMetaMetricsId: true` (`knowledge/metrametrics-identity.md`, Sensitive Properties). + +UI (`ui/components/app/balance-empty-state/balance-empty-state.tsx`): + +```ts +import { useAnalytics } from '../../../hooks/useAnalytics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; + +const { trackEvent, createEventBuilder } = useAnalytics(); + +const handleAction = useCallback(() => { + trackEvent( + createEventBuilder(MetaMetricsEventName.NavBuyButtonClicked) + .addCategory(MetaMetricsEventCategory.Navigation) + .addProperties({ + location: 'balance_empty_state', + text: 'Add funds', + chainId, + }) + .build(), + ); + setIsModalOpen(true); +}, [chainId, trackEvent]); +``` + +Background, with an anonymous event (`app/scripts/metamask-controller.js`): + +```js +import { + createEventBuilder, + trackEvent, +} from './controllers/analytics'; + +trackEvent( + createEventBuilder(MetaMetricsEventName.ProceedAnywayClicked) + .addCategory(MetaMetricsEventCategory.Phishing) + .addProperties({ + url: origin, + referrer: { + url: origin, + }, + }) + .build({ + referrer: { + url: origin, + }, + excludeMetaMetricsId: true, + }), +); +``` + +Redux thunk (`ui/store/actions.ts`): + +```ts +trackAnalyticsEvent( + createEventBuilder(MetaMetricsEventName.SettingsUpdated) + .addCategory(MetaMetricsEventCategory.Settings) + .addProperties({ + stx_opt_in: value, + prev_stx_opt_in: smartTransactionsOptInStatus, + }) + .build(), +); +``` + +`trackAnalyticsEvent` takes the options as its second argument, and `useAnalytics` fills in `environmentType` there. `ui/store/actions.ts` is `@ts-nocheck`, so a thunk call without that argument compiles, and the background then records `environment_type` as `background` ([`analytics.ts:256-265`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/app/scripts/controllers/analytics/analytics.ts#L256-L265)). + +## Adding an event + +1. **Search `MetaMetricsEventName`** ([`metametrics.ts:798-1212`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/constants/metametrics.ts#L798-L1212)). The event may already exist under a different phrasing. +2. **Search the tracking plan.** It names an event by the enum's string value, not its key: `AccountAdded = 'Account Added'` is `name: Account Added` in `libraries/events/metamask-account-mgmt/account-added.yaml`. +3. **Add the enum entry**, then the `trackEvent` call. The `Consensys/segment-schema` `CONTRIBUTING.md` asks for an object plus past-tense verb in Title Case (`Wallet Created`) and `snake_case` properties. +4. **Pass `excludeMetaMetricsId: true` to `.build()` only for an event that must not carry the user's identity.** It sends the event under the shared anonymous ID and drops the profile IDs, for every user, not only those who have not opted in. An event whose name matches `/^send|^confirm/iu` gets it by default unless `.build()` receives `excludeMetaMetricsId: false` (`knowledge/metrametrics-identity.md`). +5. **Open the `Consensys/segment-schema` pull request before merging, and get it merged before the event ships.** Its `CONTRIBUTING.md` requires two approvals, one from a Data Council delegate, and its workflow attaches an Impact Report to review. At `e81ed46` no workflow in metamask-extension checks an event against the tracking plan, so skipping this step fails nothing (`knowledge/segment-governance.md`). +6. **Register A/B enrichment** if the event belongs to an experiment: add its name to an `ABTestAnalyticsMapping` in `AB_TEST_ANALYTICS_MAPPINGS` ([`ab-test-analytics.ts:19-22`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/lib/ab-testing/ab-test-analytics.ts#L19-L22)), as `docs/ab-testing.md` describes. + +## Updating an event + +- Adding or changing a property: update the event's YAML in `Consensys/segment-schema`, reviewed like a new event. +- Renaming an event: the `Consensys/segment-schema` `README.md` asks first whether historical continuity is needed, which it almost always is for a KPI event. If it is, a Segment Transformation maps the old name to the new one. A property change in the same move also needs a dbt migration. +- Removing an event: confirm no dashboard depends on it. The `Consensys/segment-schema` convention marks an event that powers key dashboards with `kpi: true` under `labels`, and a change to a KPI-labeled event needs a Data Council approval. + +## Testing + +UI tests mock the hook and keep the real builder, then assert the built event: + +```ts +const mockTrackEvent = jest.fn(); + +jest.mock('../../../hooks/useAnalytics', () => { + const { createEventBuilder } = jest.requireActual( + '../../../../shared/lib/analytics/create-event-builder', + ); + return { + useAnalytics: () => ({ + trackEvent: mockTrackEvent, + createEventBuilder, + }), + }; +}); + +expect(mockTrackEvent).toHaveBeenCalledWith({ + name: MetaMetricsEventName.EmptyBuyBannerDisplayed, + properties: { + category: MetaMetricsEventCategory.Navigation, + locale: 'en', + network: 'Goerli', + referrer: ORIGIN_METAMASK, + location: 'balance_empty_state', + }, + sensitiveProperties: {}, +}); +``` + +`addCategory` stores the category inside `properties` ([`create-event-builder.ts:72-78`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/lib/analytics/create-event-builder.ts#L72-L78)). `build()` drops `undefined` property values and adds an `options` key only when it receives options ([`create-event-builder.ts:106-111`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/lib/analytics/create-event-builder.ts#L106-L111)). + +`ui/__mocks__/useAnalytics.ts` is a Storybook alias ([`.storybook/main.js:41-49`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/.storybook/main.js#L41-L49)), not a Jest manual mock, so a Jest test still mocks the hook itself. + +Background tests mock the module the same way (`app/scripts/lib/ramps/trackRampsCheckoutAnalytics.test.ts`): + +```ts +jest.mock('../../controllers/analytics', () => ({ + createEventBuilder: jest.requireActual('../../controllers/analytics') + .createEventBuilder, + trackEvent: jest.fn(), +})); + +const built = jest.mocked(trackEvent).mock.calls[0][0]; +expect(built.name).toBe(MetaMetricsEventName.RampsCheckoutOpened); +``` + +## Review + +```bash +git grep -n 'excludeMetaMetricsId: true' -- app shared ui ':!*.test.*' +``` + +Each hit sends its event under the shared anonymous ID. Confirm the event must not carry identity. A new event name beginning with `Send` or `Confirm` is anonymous by default, so check new names too. + +## Reject + +- New `MetaMetricsContext.trackEvent` call sites. Use `useAnalytics` +- `excludeMetaMetricsId: true` on an event that needs user identity. It sends the event under the shared anonymous ID for every user +- `excludeMetaMetricsId: true` on an event with sensitive properties. The background throws, reports the error to Sentry, and does not send the event +- `matomoEvent: true` on a new event. The option is for Matomo holdovers that do not conform to the schema ([`metametrics.ts:153-158`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/constants/metametrics.ts#L153-L158)), and the background adds `legacy_event: true` to the event ([`analytics.ts:335-342`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/app/scripts/controllers/analytics/analytics.ts#L335-L342)) +- Shipping an event before its `Consensys/segment-schema` pull request merges. No CI check catches it, so put the tracking-plan PR on the PR checklist +- An event name string at a call site that has no `MetaMetricsEventName` entry diff --git a/domains/platform/skills/analytics/skill.md b/domains/platform/skills/analytics/skill.md index b1149b43..e639c14a 100644 --- a/domains/platform/skills/analytics/skill.md +++ b/domains/platform/skills/analytics/skill.md @@ -1,8 +1,9 @@ --- name: analytics description: >- - Product analytics and event tracking. Use when adding, migrating, or - reviewing tracked events, or when writing tests for analytics call sites. + Product analytics and event tracking (MetaMetrics / Segment events). Use when + adding, migrating, or reviewing tracked events, or when writing tests for + analytics call sites. maturity: stable base: true --- @@ -19,7 +20,7 @@ Use this skill for product event tracking. ## Workflow -1. Register this interaction in the catalog (`EVENT_NAME` + `generateOpt` in catalog modules). Reuse an existing catalog name only when this control is another instance of that same interaction (same dashboard event, same owners). +1. Register this interaction in the repo's event catalog. Reuse an existing catalog name only when this control is another instance of that same interaction (same dashboard event, same owners). 2. Attach properties on the event builder. 3. Send the built event through the tracking entry point. -4. In UI tests, wrap `useAnalytics` with the test factory (including files that already mock the hook). In non-React tests, assert the builder and the helper or Engine tracking util. +4. In UI tests, mock `useAnalytics` as the repo overlay specifies (including files that already mock the hook). In non-React tests, assert the builder and the tracking helper. From 31c26c48406eb9f4abf18a7047d5d7649e88819a Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 16 Sep 2026 10:54:23 -0400 Subject: [PATCH 7/8] Name the Data Council's review role instead of its team handle --- domains/platform/knowledge/segment-governance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domains/platform/knowledge/segment-governance.md b/domains/platform/knowledge/segment-governance.md index 07c6bde9..741655c4 100644 --- a/domains/platform/knowledge/segment-governance.md +++ b/domains/platform/knowledge/segment-governance.md @@ -13,7 +13,7 @@ description: Segment event governance via segment-schema is advisory — no CI e | 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` | +| Governance contact | `#metamask-metametrics` in Slack, and a Data Council delegate, who is a required reviewer on each `Consensys/segment-schema` pull request | ## The Gap From 66de4887620fc49436fe6138ad25474401a58793 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Wed, 16 Sep 2026 11:02:36 -0400 Subject: [PATCH 8/8] Spell `metametrics-identity` correctly, and scope the CHANGELOG entry to every `platform` skill --- CHANGELOG.md | 2 +- .../{metrametrics-identity.md => metametrics-identity.md} | 2 +- .../platform/skills/analytics/repos/metamask-extension.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename domains/platform/knowledge/{metrametrics-identity.md => metametrics-identity.md} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 589762ae..abb08af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `navigation` skill with a repo-agnostic base and a MetaMask Mobile overlay for `Routes` and `NavigationService`. Marked `base: true` so it installs even when its domain is filtered out. - Add `feature-flags` skill with a repo-agnostic base and a MetaMask Mobile overlay for version-gated remote flags. Marked `base: true` so it installs even when its domain is filtered out. ([#147](https://github.com/MetaMask/skills/pull/147)) - Add `analytics` skill (`platform/analytics`, moved from `coding`) with a repo-agnostic base and a MetaMask Mobile overlay for the canonical tracking API. Marked `base: true` so it installs even when its domain is filtered out. ([#140](https://github.com/MetaMask/skills/pull/140)) -- Add a MetaMask Extension overlay to `analytics` for MetaMetrics and Segment events, and the `metrametrics-identity` and `segment-governance` knowledge files to the `platform` domain, which now install beside `analytics` in both repos. ([#143](https://github.com/MetaMask/skills/pull/143)) +- Add a MetaMask Extension overlay to `analytics` for MetaMetrics and Segment events, and the `metametrics-identity` and `segment-governance` knowledge files to the `platform` domain, which install beside each `platform` skill an install selects: `analytics` in MetaMask Extension, and `analytics`, `feature-flags` and `navigation` in MetaMask Mobile. ([#143](https://github.com/MetaMask/skills/pull/143)) ### Changed diff --git a/domains/platform/knowledge/metrametrics-identity.md b/domains/platform/knowledge/metametrics-identity.md similarity index 99% rename from domains/platform/knowledge/metrametrics-identity.md rename to domains/platform/knowledge/metametrics-identity.md index 3696b6bf..885114cd 100644 --- a/domains/platform/knowledge/metrametrics-identity.md +++ b/domains/platform/knowledge/metametrics-identity.md @@ -1,5 +1,5 @@ --- -name: metrametrics-identity +name: metametrics-identity domain: platform description: Which extension MetaMetrics events are sent without the user's analytics ID, set by the excludeMetaMetricsId option or by an event name starting with Send or Confirm --- diff --git a/domains/platform/skills/analytics/repos/metamask-extension.md b/domains/platform/skills/analytics/repos/metamask-extension.md index ff79ed36..cdd9f134 100644 --- a/domains/platform/skills/analytics/repos/metamask-extension.md +++ b/domains/platform/skills/analytics/repos/metamask-extension.md @@ -5,7 +5,7 @@ parent: analytics # Analytics — MetaMask Extension -Read at `metamask-extension` [`e81ed46`](https://github.com/MetaMask/metamask-extension/commit/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f). A/B enrichment: `docs/ab-testing.md`. How an event loses the user's analytics ID: `knowledge/metrametrics-identity.md`. Tracking-plan governance and Data Council contacts: `knowledge/segment-governance.md`. +Read at `metamask-extension` [`e81ed46`](https://github.com/MetaMask/metamask-extension/commit/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f). A/B enrichment: `docs/ab-testing.md`. How an event loses the user's analytics ID: `knowledge/metametrics-identity.md`. Tracking-plan governance and Data Council contacts: `knowledge/segment-governance.md`. ## Canonical API @@ -36,7 +36,7 @@ Every path ends in the background `trackEvent` ([`analytics.ts:344-394`](https:/ - Properties via `.addProperties(...)`, category via `.addCategory(...)`, options via `.build(options)`. `.build()` accepts `excludeMetaMetricsId`, `matomoEvent`, `environmentType`, `page` and `referrer` ([`create-event-builder.ts:15-22`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/lib/analytics/create-event-builder.ts#L15-L22)) - Page title: `useAnalytics` has no `contextPropsIntoEventProperties` option. Add `[MetaMetricsContextProp.PageTitle]: segmentContext.page?.title` to the properties, with `segmentContext` from `useSegmentContext()` ([`about-info.tsx:40-50`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/ui/pages/settings/about-tab/about-info.tsx#L40-L50)) -An event with `addSensitiveProperties` is sent twice, and cannot also set `excludeMetaMetricsId: true` (`knowledge/metrametrics-identity.md`, Sensitive Properties). +An event with `addSensitiveProperties` is sent twice, and cannot also set `excludeMetaMetricsId: true` (`knowledge/metametrics-identity.md`, Sensitive Properties). UI (`ui/components/app/balance-empty-state/balance-empty-state.tsx`): @@ -111,7 +111,7 @@ trackAnalyticsEvent( 1. **Search `MetaMetricsEventName`** ([`metametrics.ts:798-1212`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/constants/metametrics.ts#L798-L1212)). The event may already exist under a different phrasing. 2. **Search the tracking plan.** It names an event by the enum's string value, not its key: `AccountAdded = 'Account Added'` is `name: Account Added` in `libraries/events/metamask-account-mgmt/account-added.yaml`. 3. **Add the enum entry**, then the `trackEvent` call. The `Consensys/segment-schema` `CONTRIBUTING.md` asks for an object plus past-tense verb in Title Case (`Wallet Created`) and `snake_case` properties. -4. **Pass `excludeMetaMetricsId: true` to `.build()` only for an event that must not carry the user's identity.** It sends the event under the shared anonymous ID and drops the profile IDs, for every user, not only those who have not opted in. An event whose name matches `/^send|^confirm/iu` gets it by default unless `.build()` receives `excludeMetaMetricsId: false` (`knowledge/metrametrics-identity.md`). +4. **Pass `excludeMetaMetricsId: true` to `.build()` only for an event that must not carry the user's identity.** It sends the event under the shared anonymous ID and drops the profile IDs, for every user, not only those who have not opted in. An event whose name matches `/^send|^confirm/iu` gets it by default unless `.build()` receives `excludeMetaMetricsId: false` (`knowledge/metametrics-identity.md`). 5. **Open the `Consensys/segment-schema` pull request before merging, and get it merged before the event ships.** Its `CONTRIBUTING.md` requires two approvals, one from a Data Council delegate, and its workflow attaches an Impact Report to review. At `e81ed46` no workflow in metamask-extension checks an event against the tracking plan, so skipping this step fails nothing (`knowledge/segment-governance.md`). 6. **Register A/B enrichment** if the event belongs to an experiment: add its name to an `ABTestAnalyticsMapping` in `AB_TEST_ANALYTICS_MAPPINGS` ([`ab-test-analytics.ts:19-22`](https://github.com/MetaMask/metamask-extension/blob/e81ed463b0b0af60a1ef01f9b95b2ed5792a7c2f/shared/lib/ab-testing/ab-test-analytics.ts#L19-L22)), as `docs/ab-testing.md` describes.