Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
2b95db2
feat: integrate analytics controller
gauthierpetetin May 22, 2026
59ed60c
fix: address analytics controller ci failures
gauthierpetetin May 23, 2026
2e57540
test: update metametrics analytics state expectations
gauthierpetetin May 23, 2026
d1ce560
test: align analytics default fixture state
gauthierpetetin May 26, 2026
d92fc32
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 26, 2026
aa61590
test: align sentry analytics state mask
gauthierpetetin May 26, 2026
a3a583d
fix: derive metametrics state from analytics controller
gauthierpetetin May 26, 2026
27c93f3
chore: merge origin/main
gauthierpetetin May 26, 2026
55deafb
fix: normalize metametrics tri-state usage
gauthierpetetin May 27, 2026
d88bc53
test: update metametrics integration state fixtures
gauthierpetetin May 27, 2026
0587881
test: update metametrics state fixtures
gauthierpetetin May 27, 2026
8c57f6d
test: align onboarding metrics fixture
gauthierpetetin May 27, 2026
6726715
Merge remote-tracking branch 'origin/main' into feat/analytics-contro…
gauthierpetetin May 27, 2026
23bfa81
chore: refresh PR branch
gauthierpetetin May 27, 2026
c3ad296
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 27, 2026
e7e9d52
chore: minimize metametrics controller diff
gauthierpetetin May 27, 2026
b773963
test: restore wallet import metrics buffering
gauthierpetetin May 27, 2026
6ac3707
test: update analytics fixture state
gauthierpetetin May 27, 2026
4d5c2bb
Merge remote-tracking branch 'origin/main' into feat/analytics-contro…
gauthierpetetin May 27, 2026
43f798b
test: keep default fixture metrics off
gauthierpetetin May 27, 2026
12249b6
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 27, 2026
28e2c5a
test: align analytics fixture snapshots
gauthierpetetin May 27, 2026
ef3dd18
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 27, 2026
f840046
test: stabilize analytics e2e expectations
gauthierpetetin May 28, 2026
c537429
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 28, 2026
6792038
test: reduce analytics integration diff
gauthierpetetin May 28, 2026
fd8bddb
chore: reduce analytics integration diff
gauthierpetetin May 28, 2026
fa96cba
refactor: clarify sentry metametrics state derivation
gauthierpetetin May 28, 2026
a6e2a61
test: complete analytics messenger setup
gauthierpetetin May 28, 2026
a04d840
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 28, 2026
8cbff1c
test: loosen opt-out flush assertion
gauthierpetetin May 28, 2026
cc68ab1
test: allow interleaved swap metrics events
gauthierpetetin May 28, 2026
94df549
fix: align metrics participation compatibility
gauthierpetetin May 28, 2026
dfb4b76
fix: migrate queued segment calls
gauthierpetetin May 28, 2026
e7e3618
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 28, 2026
0cb4ee1
fix: preserve analytics queue on metrics reset
gauthierpetetin May 28, 2026
37d047e
fix: preserve non-anonymous metrics timestamp
gauthierpetetin May 28, 2026
8126aeb
test: fix migration analytics payload lint
gauthierpetetin May 28, 2026
975cde0
fix: preserve falsy page metrics properties
gauthierpetetin May 28, 2026
82af9d0
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 28, 2026
8cd58f0
fix: type analytics event queue migration
gauthierpetetin May 28, 2026
9ca4546
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 28, 2026
23796a9
fix: preserve analytics state in vault backup
gauthierpetetin May 29, 2026
08a46b1
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin May 29, 2026
cfc73a0
Merge remote-tracking branch 'origin/main' into feat/analytics-contro…
gauthierpetetin Jun 2, 2026
656834f
test: align analytics sentry state snapshots after main merge
gauthierpetetin Jun 3, 2026
172d4f5
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin Jun 3, 2026
ace2b3a
test: add analytics controller keys to state-logs structure map
gauthierpetetin Jun 3, 2026
1f04572
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin Jun 3, 2026
482558b
fix: block analytics submission when metrics participation is null
gauthierpetetin Jun 3, 2026
6b83220
revert: restore opt-out on metrics participation reset
gauthierpetetin Jun 3, 2026
1e77548
Merge branch 'main' into feat/analytics-controller-extension-integration
gauthierpetetin Jun 3, 2026
5d73b11
Merge branch 'main' into feat/analytics-controller-extension-integration
itsyoboieltr Jun 3, 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
19 changes: 7 additions & 12 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -1276,7 +1276,7 @@ export async function loadStateFromPersistence(backup) {
* @param {number} [frameId] - The frame ID from chrome.runtime.MessageSender (0 = top-level, >0 = iframe)
*/
function emitDappViewedMetricEvent(origin, mainFrameOrigin, frameId) {
const { metaMetricsId } = controller.metaMetricsController.state;
const { metaMetricsId } = controller.getState();
if (!shouldEmitDappViewedEvent(metaMetricsId)) {
return;
}
Expand Down Expand Up @@ -1370,11 +1370,10 @@ function trackDappView(remotePort) {
* @param {string} environmentType - The environment type where the app is opening
*/
function emitAppOpenedMetricEvent(environmentType) {
const { metaMetricsId, participateInMetaMetrics } =
controller.metaMetricsController.state;
const { participateInMetaMetrics } = controller.getState();

// Skip if user hasn't opted into metrics
if (metaMetricsId === null && !participateInMetaMetrics) {
if (!participateInMetaMetrics) {
return;
}

Expand Down Expand Up @@ -2157,20 +2156,16 @@ const addAppInstalledEvent = async (installAttributionPromise) => {
properties: eventProperties,
};

const { participateInMetaMetrics, metaMetricsId } =
controller.metaMetricsController.state;
const { participateInMetaMetrics, metaMetricsId } = controller.getState();

if (participateInMetaMetrics === false) {
// We can skip tracking completely if they've already explicitly opted out
return;
}

// Track immediately only once consent is active and the controller has a
// persisted MetaMetrics ID. Otherwise keep the event buffered for the opt-in
// flush path so it is not dropped.
// No need to call getMetaMetricsId() first: setParticipateInMetaMetrics()
// generates and persists the ID before setting participation to true, and this
// install handler should not create a metrics ID outside that consent path.
// Track immediately only once consent is active and the compatibility
// MetaMetrics ID is available. Otherwise keep the event buffered for the
// opt-in flush path so it is not dropped.
if (participateInMetaMetrics === true && metaMetricsId) {
controller.metaMetricsController.trackEvent(appInstalledEvent);
} else {
Expand Down
9 changes: 6 additions & 3 deletions app/scripts/constants/sentry-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export const SENTRY_BACKGROUND_STATE: SentryBackgroundControllerMasks = {
unconnectedAccountAlertShownOrigins: false,
web3ShimUsageOrigins: false,
},
AnalyticsController: {
analyticsId: true,
eventQueue: false,
optedIn: true,
},
AnnouncementController: {
announcements: false,
},
Expand Down Expand Up @@ -190,12 +195,10 @@ export const SENTRY_BACKGROUND_STATE: SentryBackgroundControllerMasks = {
isUpdatingMetamaskNotificationsAccount: false,
},
MetaMetricsController: {
completedMetaMetricsOnboarding: true,
eventsBeforeMetricsOptIn: false,
tracesBeforeMetricsOptIn: false,
fragments: false,
metaMetricsId: true,
participateInMetaMetrics: true,
segmentApiCalls: false,
traits: false,
dataCollectionForMarketing: false,
marketingCampaignCookieId: true,
Expand Down
245 changes: 245 additions & 0 deletions app/scripts/controllers/analytics/platform-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { segment as sharedExtensionSegment } from '../../lib/segment';
import {
METAMETRICS_ANONYMOUS_ID,
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import {
AnonymousTransactionMetaMetricsEvent,
TransactionMetaMetricsEvent,
} from '../../../../shared/constants/transaction';
import {
ANONYMOUS_EVENT_PROPERTY,
createPlatformAdapter,
} from './platform-adapter';

const ANALYTICS_ID = '0xabc123';

type SegmentSpy = {
track: jest.SpiedFunction<typeof sharedExtensionSegment.track>;
identify: jest.SpiedFunction<typeof sharedExtensionSegment.identify>;
page: jest.SpiedFunction<typeof sharedExtensionSegment.page>;
};

function createSegmentSpy(): SegmentSpy {
return {
track: jest.spyOn(sharedExtensionSegment, 'track').mockImplementation(),
identify: jest
.spyOn(sharedExtensionSegment, 'identify')
.mockImplementation(),
page: jest.spyOn(sharedExtensionSegment, 'page').mockImplementation(),
};
}

function buildAdapter() {
const segment = createSegmentSpy();
const adapter = createPlatformAdapter();
// After PR MetaMask/core#8543 lands, AnalyticsController guarantees
// onSetupCompleted runs before track/identify/view; replicate that here.
adapter.onSetupCompleted(ANALYTICS_ID);
return { adapter, segment };
}

afterEach(() => {
jest.restoreAllMocks();
});

describe('createPlatformAdapter', () => {
describe('skipUUIDv4Check', () => {
it('is set to true so non-UUIDv4 extension analyticsIds are accepted', () => {
const adapter = createPlatformAdapter();
expect(adapter.skipUUIDv4Check).toBe(true);
});
});

describe('track', () => {
it('calls segment.track with the analyticsId as userId and the event name', () => {
const { adapter, segment } = buildAdapter();
adapter.track('Wallet Opened');
expect(segment.track).toHaveBeenCalledWith(
{ userId: ANALYTICS_ID, event: 'Wallet Opened' },
undefined,
);
});

it('forwards properties when provided', () => {
const { adapter, segment } = buildAdapter();
adapter.track('Wallet Opened', { chainId: '0x1' });
expect(segment.track).toHaveBeenCalledWith(
{
userId: ANALYTICS_ID,
event: 'Wallet Opened',
properties: { chainId: '0x1' },
},
undefined,
);
});

it('forwards context and delivery options', () => {
const { adapter, segment } = buildAdapter();
const callback = jest.fn();
const timestamp = new Date('2026-01-01T00:00:00Z');
adapter.track(
'Wallet Opened',
{ foo: 'bar' },
{ app: { name: 'MetaMask Extension', version: '1.0.0' } },
{
messageId: 'msg-1',
timestamp,
callback,
},
);
expect(segment.track).toHaveBeenCalledWith(
{
userId: ANALYTICS_ID,
event: 'Wallet Opened',
properties: { foo: 'bar' },
context: { app: { name: 'MetaMask Extension', version: '1.0.0' } },
messageId: 'msg-1',
timestamp,
},
callback,
);
});

it('downgrades anonymous-marked track payloads to the shared anonymous ID', () => {
const { adapter, segment } = buildAdapter();
const properties = {
foo: 'bar',
[ANONYMOUS_EVENT_PROPERTY]: true,
};

adapter.track('Wallet Opened', properties);

expect(segment.track).toHaveBeenCalledWith(
{
anonymousId: METAMETRICS_ANONYMOUS_ID,
event: 'Wallet Opened',
properties: { foo: 'bar' },
},
undefined,
);
expect(properties).toStrictEqual({
foo: 'bar',
[ANONYMOUS_EVENT_PROPERTY]: true,
});
});

it('overrides anonymous signature event names', () => {
const { adapter, segment } = buildAdapter();

adapter.track(MetaMetricsEventName.SignatureRequested, {
[ANONYMOUS_EVENT_PROPERTY]: true,
});

expect(segment.track).toHaveBeenCalledWith(
{
anonymousId: METAMETRICS_ANONYMOUS_ID,
event: MetaMetricsEventName.SignatureRequestedAnon,
properties: {},
},
undefined,
);
});

it('overrides anonymous transaction event names', () => {
const { adapter, segment } = buildAdapter();

adapter.track(TransactionMetaMetricsEvent.submitted, {
[ANONYMOUS_EVENT_PROPERTY]: true,
});

expect(segment.track).toHaveBeenCalledWith(
{
anonymousId: METAMETRICS_ANONYMOUS_ID,
event: AnonymousTransactionMetaMetricsEvent.submitted,
properties: {},
},
undefined,
);
});
});

describe('identify', () => {
it('forwards the userId argument from AnalyticsController and traits', () => {
const { adapter, segment } = buildAdapter();
adapter.identify('user-1', { plan: 'pro' });
expect(segment.identify).toHaveBeenCalledWith(
{ userId: 'user-1', traits: { plan: 'pro' } },
undefined,
);
});

it('omits traits when not provided', () => {
const { adapter, segment } = buildAdapter();
adapter.identify('user-1');
expect(segment.identify).toHaveBeenCalledWith(
{ userId: 'user-1' },
undefined,
);
});

it('forwards context and delivery options', () => {
const { adapter, segment } = buildAdapter();
const callback = jest.fn();
const timestamp = new Date('2026-01-01T00:00:00Z');
adapter.identify(
'user-1',
{ plan: 'pro' },
{ app: { name: 'MetaMask Extension' } },
{
messageId: 'id-1',
timestamp,
callback,
},
);
expect(segment.identify).toHaveBeenCalledWith(
{
userId: 'user-1',
traits: { plan: 'pro' },
context: { app: { name: 'MetaMask Extension' } },
messageId: 'id-1',
timestamp,
},
callback,
);
});
});

describe('view', () => {
it('forwards as a Segment page() call with the analyticsId', () => {
const { adapter, segment } = buildAdapter();
adapter.view('Home');
expect(segment.page).toHaveBeenCalledWith(
{ userId: ANALYTICS_ID, name: 'Home' },
undefined,
);
});

it('forwards properties, context, and delivery options', () => {
const { adapter, segment } = buildAdapter();
const callback = jest.fn();
const timestamp = new Date('2026-01-01T00:00:00Z');
adapter.view(
'Home',
{ section: 'tokens' },
{ app: { name: 'MetaMask Extension' } },
{
messageId: 'page-1',
timestamp,
callback,
},
);
expect(segment.page).toHaveBeenCalledWith(
{
userId: ANALYTICS_ID,
name: 'Home',
properties: { section: 'tokens' },
context: { app: { name: 'MetaMask Extension' } },
messageId: 'page-1',
timestamp,
},
callback,
);
});
});
});
Loading
Loading