From 88a5e005c137269468651d4708d15e5bc3be3318 Mon Sep 17 00:00:00 2001 From: Jongsun Suh Date: Tue, 1 Sep 2026 09:51:55 -0400 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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