diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 06a7f1e0..cfcdec58 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -20,6 +20,7 @@ /domains/observability/skills/*/repos/metamask-extension.md @MetaMask/extension-platform /domains/observability/skills/*/repos/metamask-mobile.md @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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 725dcca6..0a6e812d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `analytics` 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 diff --git a/README.md b/README.md index c1608837..1e82a148 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 diff --git a/domains/platform/skills/analytics/repos/metamask-mobile.md b/domains/platform/skills/analytics/repos/metamask-mobile.md new file mode 100644 index 00000000..68462864 --- /dev/null +++ b/domains/platform/skills/analytics/repos/metamask-mobile.md @@ -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 = ({ + 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 `/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/`, `/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. diff --git a/domains/platform/skills/analytics/skill.md b/domains/platform/skills/analytics/skill.md new file mode 100644 index 00000000..b1149b43 --- /dev/null +++ b/domains/platform/skills/analytics/skill.md @@ -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.