diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d552444..abb08af6 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 `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 + +- 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 9c88000e..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 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`). -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 be000d0c..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 data 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. @@ -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/platform/knowledge/metametrics-identity.md b/domains/platform/knowledge/metametrics-identity.md new file mode 100644 index 00000000..885114cd --- /dev/null +++ b/domains/platform/knowledge/metametrics-identity.md @@ -0,0 +1,40 @@ +--- +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 +--- + +# MetaMetrics Identity on Anonymous Events + +Read at `metamask-extension` [`c31416a`](https://github.com/MetaMask/metamask-extension/commit/c31416a47811bc4355a904925021a30f4c5564bb). + +## 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 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. +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`. + +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)). + +## The Name Default + +`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. + +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. + +## Sensitive Properties + +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 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 + +```bash +git grep -n 'excludeMetaMetricsId: true' -- app shared ui ':!*.test.*' +``` + +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. diff --git a/domains/platform/knowledge/segment-governance.md b/domains/platform/knowledge/segment-governance.md new file mode 100644 index 00000000..741655c4 --- /dev/null +++ b/domains/platform/knowledge/segment-governance.md @@ -0,0 +1,40 @@ +--- +name: segment-governance +domain: platform +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 contact | `#metamask-metametrics` in Slack, and a Data Council delegate, who is a required reviewer on each `Consensys/segment-schema` pull request | + +## 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/platform/skills/analytics/repos/metamask-extension.md b/domains/platform/skills/analytics/repos/metamask-extension.md new file mode 100644 index 00000000..cdd9f134 --- /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/metametrics-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/metametrics-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/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. + +## 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.