Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c2701bb
feat: add analytics skill with MetaMask Mobile overlay
NicolasMassart Aug 31, 2026
c8889c9
docs(analytics): update workflow and testing guidelines for event tra…
NicolasMassart Sep 3, 2026
416e7f9
docs(analytics): clarify emission paths and requirements in tracking …
NicolasMassart Sep 3, 2026
d3a7e40
docs(analytics): update CHANGELOG and README for analytics skill and …
NicolasMassart Sep 3, 2026
6a8e0fb
Merge branch 'main' into feat/MCWP-826_Analytics-skill
NicolasMassart Sep 4, 2026
478dd8d
feat(platform): add feature-flags skill with MetaMask Mobile overlay
NicolasMassart Sep 4, 2026
2d99fc0
docs: add PR link to feature-flags changelog entry
NicolasMassart Sep 9, 2026
abc8798
Add the `metamask-extension` overlay for `feature-flags`
MajorLift Sep 9, 2026
9d6c626
feat: add analytics skill with MetaMask Mobile overlay
NicolasMassart Aug 31, 2026
9eee97f
docs(analytics): update workflow and testing guidelines for event tra…
NicolasMassart Sep 3, 2026
6f7422f
docs(analytics): clarify emission paths and requirements in tracking …
NicolasMassart Sep 3, 2026
3140f96
docs(analytics): update CHANGELOG and README for analytics skill and …
NicolasMassart Sep 3, 2026
cda130c
docs(changelog): keep analytics entries under Unreleased after 0.3.1
NicolasMassart Sep 11, 2026
8aeb8e3
fix(analytics): align Mobile overlay with live Engine tracking
NicolasMassart Sep 11, 2026
b08eed3
fix(analytics): copy live Mobile fences and drop messenger A/B hatch
NicolasMassart Sep 14, 2026
9790948
feat(platform): add feature-flags skill with MetaMask Mobile overlay
NicolasMassart Sep 4, 2026
83c169a
Merge branch 'feat/MCWP-827_feature-flags' into jongsun/add/feature-f…
MajorLift Sep 14, 2026
bb1173b
Say which users never fetch flags, and how to test the fetch path in E2E
MajorLift Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
/domains/coding/ @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform
/domains/general/ @MetaMask/extension-platform @MetaMask/mobile-platform
/domains/performance/ @MetaMask/extension-platform @MetaMask/mobile-platform
/domains/platform/ @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform
/domains/perps/ @MetaMask/perps
/domains/pr-workflow/ @MetaMask/extension-platform @MetaMask/mobile-platform
/domains/swaps/ @MetaMask/swaps-engineers
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- 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))

## [0.3.1]

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ tools/
| -------------- | ----------------- | ------------------------------------------- |
| `web3-tools` | dApp builders | `gator-cli`, `smart-accounts-kit`, `oh-my-opencode` |
| `coding` | MM product eng | Coding guidelines, controller patterns |
| `platform` | MM product eng | Product analytics and other platform skills |
| `agentic` | MM product eng | Experimental recipe workflows and runtime proof tools |
| `assets` | MM product eng | Assets domain skills |
| `general` | All agents | `codex`, `gemini` CLI usage guides |
Expand Down Expand Up @@ -386,6 +387,12 @@ Extra metadata blocks (e.g. OpenClaw-style `metadata:` with emoji and
homepage) are preserved through install — only `name`, `description`,
`maturity`, `base`, and `scope` are read by the CLI.

`base: true` installs the skill even when its domain is filtered out.
`--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 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
deliberately. It is enforced by `yarn audit:skills` from
Expand Down
194 changes: 194 additions & 0 deletions domains/platform/skills/analytics/repos/metamask-mobile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
---
repo: metamask-mobile
parent: analytics
---

# Analytics — MetaMask Mobile

Human-facing file map: `app/core/Analytics/README.md`. A/B enrichment SSOT: `docs/ab-testing.md`.

## Canonical API

| Role | Path |
|------|------|
| Helper (non-React) | `app/util/analytics/analytics.ts` → `analytics.trackEvent` |
| Helper (UI) | `app/components/hooks/useAnalytics/useAnalytics.ts` → `useAnalytics` |
| Engine (controllers) | `app/core/Engine/utils/analytics.ts` → `trackEvent`, `buildAndTrackEvent` |
| Event builder | `app/util/analytics/AnalyticsEventBuilder.ts` → `AnalyticsEventBuilder.createEventBuilder` |
| Catalog | `app/core/Analytics/` → `MetaMetricsEvents` at call sites; `EVENT_NAME` in catalog modules |
| Typed helpers | `app/util/analytics/actionButtonTracking.ts` (sibling files matching `*Tracking.ts`) |
| A/B registry | `app/util/analytics/abTestAnalyticsRegistry.ts` (feature-local `abTestConfig.ts`, e.g. `app/components/Views/Homepage/abTestConfig.ts`) |
| Test factory | `app/util/test/analyticsMock.ts` → `createMockUseAnalyticsHook` (default); `createMockEventBuilder` (optional standalone double) |

`useAnalytics()` returns `trackEvent`, `createEventBuilder`, `identify`, `enable`,
`isEnabled`, `getAnalyticsId`, and data-deletion helpers.

Controllers that already talk to Engine import `trackEvent` / `buildAndTrackEvent`
from `app/core/Engine/utils/analytics.ts`. Those helpers always wrap the
messenger call in try/catch. `enrichWithABTests` runs only when the event name
is registered in `app/util/analytics/abTestAnalyticsRegistry.ts` (fed by
feature-local `abTestConfig.ts`). New experiment events: follow
`docs/ab-testing.md`. Do not copy Engine-util internals.

`createMockEventBuilder()` default `build()` is
`{ name: 'mock-event', properties: {}, sensitiveProperties: {} }`. Use it only
as a standalone builder double, wrapped in `jest.fn(() => createMockEventBuilder())`.

## Requirements

- UI: platform `useAnalytics` from `app/components/hooks/useAnalytics/useAnalytics.ts`
- Non-React: `analytics.trackEvent`
- Controllers: `trackEvent` / `buildAndTrackEvent` from `app/core/Engine/utils/analytics.ts`
- When a typed helper exists in `app/util/analytics/` (files matching `*Tracking.ts`) for this event, call it
- Call sites (new and existing) import `MetaMetricsEvents.*`. Register new names as `EVENT_NAME` + `generateOpt` in catalog modules, then emit via `MetaMetricsEvents`. Reuse a catalog name only when this control is the same interaction as existing call sites (same event, same product meaning).
- Properties via `.addProperties(...).build()`
- UI tests: `createMockUseAnalyticsHook` wrapping `useAnalytics`, including when the file already mocks the hook. Default: `createMockUseAnalyticsHook({ trackEvent: mockTrackEvent })`. Tests that assert `addProperties` keep `AnalyticsEventBuilder.createEventBuilder`
- Non-React tests: assert `AnalyticsEventBuilder.createEventBuilder` and `analytics.trackEvent` or Engine `trackEvent` / `buildAndTrackEvent`

Generic UI (`app/components/UI/BalanceEmptyState/BalanceEmptyState.tsx`):

```ts
import React from 'react';
import { MetaMetricsEvents } from '../../../core/Analytics';
import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics';

const BalanceEmptyState: React.FC<BalanceEmptyStateProps> = ({
testID = 'balance-empty-state',
...props
}) => {
const { trackEvent, createEventBuilder } = useAnalytics();

const handleAction = () => {
trackEvent(
createEventBuilder(MetaMetricsEvents.RAMPS_BUTTON_CLICKED)
.addProperties({
button_text: 'Add funds',
location: 'BalanceEmptyState',
ramp_type: 'UNIFIED_BUY_2',
})
.build(),
);
};
```

Typed helper (`app/components/Views/Homepage/components/HomepageActionButtonsGrid/buttons/SendButton.tsx`):

```ts
import React, { useCallback } from 'react';
import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics';
import {
ActionButtonType,
ActionLocation,
trackActionButtonClick,
} from '../../../../../../util/analytics/actionButtonTracking';

const SendButton = ({
actionPosition,
allowTwoLineLabel,
onSend,
}: SendButtonProps) => {
const { trackEvent, createEventBuilder } = useAnalytics();

const handlePress = useCallback(() => {
trackActionButtonClick(trackEvent, createEventBuilder, {
action_name: ActionButtonType.SEND,
action_position: actionPosition,
button_label: label,
location: ActionLocation.HOME,
});
onSend();
}, [actionPosition, createEventBuilder, label, onSend, trackEvent]);
```

Non-React (`app/util/analytics/accountAccessTracking.ts`):

```ts
import { MetaMetricsEvents } from '../../core/Analytics/MetaMetrics.events';
import { analytics } from './analytics';
import { AnalyticsEventBuilder } from './AnalyticsEventBuilder';

analytics.trackEvent(
AnalyticsEventBuilder.createEventBuilder(
MetaMetricsEvents.APP_UNLOCKED_FAILED,
)
.addProperties({
unlock_error_type: unlockErrorType,
forced_reset: forcedReset,
})
.build(),
);
```

Controllers:

```ts
import { buildAndTrackEvent } from '../../core/Engine/utils/analytics';
import { MetaMetricsEvents } from '../../core/Analytics';

buildAndTrackEvent(
initMessenger,
MetaMetricsEvents.PROFILE_ACTIVITY_UPDATED.category,
{
profile_id: profileId,
feature_name: 'Contacts Sync',
action: 'Contacts Sync Contact Updated',
},
);
```

`createEventBuilder` copies only `category` from `IMetaMetricsEvent`. When
migrating a wrapper that used `generateOpt(name, action, description)`, re-apply
`properties.action` and `properties.name` with `addProperties`.

`generateOpt` belongs in catalog modules: `app/core/Analytics/MetaMetrics.events.ts`,
`app/core/Analytics/events/`, and feature-local `<feature>/analytics/events.ts`
(see SampleFeature). Component files import catalog entries; they do not call
`generateOpt` themselves.

Tests mock the hook with the factory, not a hand-built object.
Call `createMockUseAnalyticsHook` again in `beforeEach` after
`jest.resetAllMocks()` — that wipes mock implementations. `jest.clearAllMocks()`
does not.

```ts
import { useAnalytics } from '../../hooks/useAnalytics/useAnalytics';
import { createMockUseAnalyticsHook } from '../../../util/test/analyticsMock';

jest.mock('../../hooks/useAnalytics/useAnalytics');

beforeEach(() => {
jest.resetAllMocks();
jest.mocked(useAnalytics).mockReturnValue(
createMockUseAnalyticsHook({
trackEvent: mockTrackEvent,
}),
);
});
```

Standalone builder double (only when the test needs one):

```ts
createEventBuilder: jest.fn(() => createMockEventBuilder()),
```

## Reject

- `addSensitiveProperties` on new tracking. Existing call sites: drop those
fields only. Moving the last sensitive field into `addProperties` flips
`isAnonymous` (true iff `sensitiveProperties` is nonempty). Do not relocate
without human sign-off.
- A new feature-local tracker that is not a file matching `*Tracking.ts` under
`app/util/analytics/`, a feature-local `abTestConfig.ts`, or a catalog
`generateOpt` module (`app/core/Analytics/MetaMetrics.events.ts`,
`app/core/Analytics/events/`, `<feature>/analytics/events.ts`)
- Replacing `MetaMetricsEvents.*` at a call site with `EVENT_NAME.*`
- Reintroducing `useMetrics` (removed) or MetaMetrics internals at call sites
- Dropping `generateOpt` `action` / `name` when migrating `IMetaMetricsEvent` call sites (until the catalog migration lands)
- Hand-built `useAnalytics` mock objects — use `createMockUseAnalyticsHook`
- Raw `initMessenger.call('AnalyticsController:trackEvent', …)` when Engine
`trackEvent` / `buildAndTrackEvent` is available
- Defaulting UI tests to `createEventBuilder: jest.fn(() => createMockEventBuilder())`
when `createMockUseAnalyticsHook()` already stubs the builder
- Attaching a new control to a catalog event whose live call sites are a different product (example: `VIEW_ALL_ASSETS_CLICKED` is wallet tokens/NFTs `asset_type`, not a homepage section)
- Firing an existing catalog event at a new lifecycle (example: `TOKEN_DETECTED` on controller init). Add a catalog name for that lifecycle.
25 changes: 25 additions & 0 deletions domains/platform/skills/analytics/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: analytics
description: >-
Product analytics and event tracking. Use when adding, migrating, or
reviewing tracked events, or when writing tests for analytics call sites.
maturity: stable
base: true
---

# Analytics

Use this skill for product event tracking.

## When to use

- Adding or migrating event tracking in UI or non-UI code
- Writing or updating tests for analytics call sites
- Reviewing a PR that introduces or changes tracked events

## 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).
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.
Loading