Skip to content

Commit a33a1ed

Browse files
adrigugcursoragentPrithpal-Sooriya
authored
feat(analytics): add ASSET_VIEWED for Predict, Perps, and Swaps funnel views (MetaMask#30069)
## Summary Unifies Segment funnel reporting by emitting a new MetaMetrics event `ASSET_VIEWED` whenever any of these legacy events fire, with the **same properties** plus: - `trade_type`: `Predict` | `Perps` | `Swaps` (mapped from the legacy event) - `implementation_type`: `native` (mobile) Legacy events are **unchanged**; `ASSET_VIEWED` is an additional duplicate for downstream analytics. ## **Changelog** CHANGELOG entry: refactor: update analytic events ## Implementation | Legacy event | Trigger | |--------------|---------| | Predict Feed Viewed | `PredictAnalytics` (`feedViewed` / `trackFeedViewed`) | | Perp Screen Viewed | `usePerpsEventTracking` `track()`; `PerpsOpenOrderCard` geo path (`useAnalytics`) | | Unified SwapBridge Page Viewed | `useTrackSwapPageViewed` | Shared merge helper: `app/core/Analytics/trade-transaction-funnel/assetViewedAnalytics.ts` (exported via `app/core/Analytics/index.ts`). Swap-related AB test mappings include `ASSET_VIEWED` so `enrichWithABTests` applies the same assignments as `Unified SwapBridge Page Viewed`. ## Testing ```bash yarn jest app/core/Analytics/trade-transaction-funnel/assetViewedAnalytics.test.ts \ app/components/UI/Perps/hooks/usePerpsEventTracking.test.ts \ app/components/UI/Predict/controllers/PredictAnalytics.test.ts \ app/components/UI/Bridge/hooks/useTrackSwapPageViewed/index.test.ts \ app/util/analytics/enrichWithABTests.test.ts ``` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Analytics-only duplication of existing events with guarded AB enrichment and one Perps screen-type exclusion; no auth, payments, or transaction logic changes. > > **Overview** > Introduces a unified Segment **`Asset Viewed`** event that fires **in addition to** existing Predict, Perps, and Swaps screen-view events, carrying the same payload plus `trade_type` (`Predict` | `Perps` | `Swaps`) and `implementation_type: native` via **`mergeAssetViewedProperties`**. > > **Swaps:** `useTrackSwapPageViewed` now emits `ASSET_VIEWED` next to `Unified SwapBridge Page Viewed`. Swap numpad and token-selector AB mappings include `ASSET_VIEWED` with **`eventPropertyRequirements`** so `enrichWithABTests` only attaches swap experiments when `trade_type` is `Swaps`. > > **Perps:** `usePerpsEventTracking` mirrors `Perp Screen Viewed` with `ASSET_VIEWED`, except **`cancel_all_orders`** screens (avoids mis-mapping `open_position`). Geo-block cancel flow in **`PerpsOpenOrderCard`** also emits `ASSET_VIEWED`. > > **Predict:** `PredictAnalytics` emits `ASSET_VIEWED` for **feed viewed** and **market details opened**. Explore **`trackExploreSectionSeeAll`** adds Predict funnel `ASSET_VIEWED` for **`predictions_trending`** only. > > Helper normalizes open-position fields to **`open_positions_count`** on Asset Viewed. Tests and integration expectations updated accordingly. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3e5bc2f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Prithpal Sooriya <prithpal.sooriya@gmail.com> Co-authored-by: Prithpal Sooriya <prithpal.sooriya@consensys.net>
1 parent 044726b commit a33a1ed

22 files changed

Lines changed: 738 additions & 33 deletions

File tree

app/components/UI/Bridge/components/GaslessQuickPickOptions/abTestConfig.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ASSET_VIEWED_PROPERTY } from '../../../../../core/Analytics/trade-transaction-funnel/assetViewedAnalytics';
12
import { EVENT_NAME } from '../../../../../core/Analytics/MetaMetrics.events';
23
import type { ABTestAnalyticsMapping } from '../../../../../util/analytics/abTestAnalytics.types';
34

@@ -30,5 +31,10 @@ export const NUMPAD_QUICK_ACTIONS_AB_TEST_ANALYTICS_MAPPING: ABTestAnalyticsMapp
3031
{
3132
flagKey: NUMPAD_QUICK_ACTIONS_AB_KEY,
3233
validVariants: Object.values(NumpadQuickActionsVariant),
33-
eventNames: [EVENT_NAME.SWAP_PAGE_VIEWED],
34+
eventNames: [EVENT_NAME.SWAP_PAGE_VIEWED, EVENT_NAME.ASSET_VIEWED],
35+
eventPropertyRequirements: {
36+
[EVENT_NAME.ASSET_VIEWED]: {
37+
[ASSET_VIEWED_PROPERTY.TRADE_TYPE]: 'Swaps',
38+
},
39+
},
3440
};

app/components/UI/Bridge/components/TokenSelectorItem.abTestConfig.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ASSET_VIEWED_PROPERTY } from '../../../../core/Analytics/trade-transaction-funnel/assetViewedAnalytics';
12
import { EVENT_NAME } from '../../../../core/Analytics/MetaMetrics.events';
23
import type { ABTestAnalyticsMapping } from '../../../../util/analytics/abTestAnalytics.types';
34

@@ -32,5 +33,10 @@ export const TOKEN_SELECTOR_BALANCE_LAYOUT_AB_TEST_ANALYTICS_MAPPING: ABTestAnal
3233
{
3334
flagKey: TOKEN_SELECTOR_BALANCE_LAYOUT_AB_KEY,
3435
validVariants: Object.values(TokenSelectorBalanceLayoutVariant),
35-
eventNames: [EVENT_NAME.SWAP_PAGE_VIEWED],
36+
eventNames: [EVENT_NAME.SWAP_PAGE_VIEWED, EVENT_NAME.ASSET_VIEWED],
37+
eventPropertyRequirements: {
38+
[EVENT_NAME.ASSET_VIEWED]: {
39+
[ASSET_VIEWED_PROPERTY.TRADE_TYPE]: 'Swaps',
40+
},
41+
},
3642
};
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { renderHook } from '@testing-library/react-native';
2+
import { useSelector } from 'react-redux';
3+
import {
4+
selectDestToken,
5+
selectSourceToken,
6+
} from '../../../../../core/redux/slices/bridge';
7+
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
8+
import { MetaMetricsEvents } from '../../../../../core/Analytics';
9+
import { useTrackSwapPageViewed } from './index';
10+
11+
jest.mock('react-redux', () => ({
12+
...jest.requireActual('react-redux'),
13+
useSelector: jest.fn(),
14+
}));
15+
16+
jest.mock('../../../../hooks/useAnalytics/useAnalytics');
17+
18+
const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
19+
const mockTrackEvent = jest.fn();
20+
const mockCreateEventBuilder = jest.fn();
21+
22+
const sourceToken = {
23+
symbol: 'ETH',
24+
chainId: '0x1',
25+
address: '0x0000000000000000000000000000000000000000',
26+
};
27+
28+
const destToken = {
29+
symbol: 'USDC',
30+
chainId: '0x89',
31+
address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
32+
};
33+
34+
describe('useTrackSwapPageViewed', () => {
35+
beforeEach(() => {
36+
jest.clearAllMocks();
37+
mockCreateEventBuilder.mockImplementation(() => ({
38+
addProperties: jest.fn().mockReturnThis(),
39+
build: jest.fn(() => ({ type: 'built' })),
40+
}));
41+
jest.mocked(useAnalytics).mockReturnValue({
42+
trackEvent: mockTrackEvent,
43+
createEventBuilder: mockCreateEventBuilder,
44+
} as unknown as ReturnType<typeof useAnalytics>);
45+
});
46+
47+
it('does not track when the source token is missing', () => {
48+
mockUseSelector.mockImplementation((selector: unknown) => {
49+
if (selector === selectSourceToken) {
50+
return null;
51+
}
52+
if (selector === selectDestToken) {
53+
return destToken;
54+
}
55+
return undefined;
56+
});
57+
58+
renderHook(() => useTrackSwapPageViewed());
59+
60+
expect(mockTrackEvent).not.toHaveBeenCalled();
61+
expect(mockCreateEventBuilder).not.toHaveBeenCalled();
62+
});
63+
64+
it('tracks SWAP_PAGE_VIEWED and Asset Viewed with swap page properties when the source token is set', () => {
65+
mockUseSelector.mockImplementation((selector: unknown) => {
66+
if (selector === selectSourceToken) {
67+
return sourceToken;
68+
}
69+
if (selector === selectDestToken) {
70+
return destToken;
71+
}
72+
return undefined;
73+
});
74+
75+
renderHook(() => useTrackSwapPageViewed());
76+
77+
const expectedPageProperties = {
78+
chain_id_source: '1',
79+
chain_id_destination: '137',
80+
token_symbol_source: 'ETH',
81+
token_symbol_destination: 'USDC',
82+
token_address_source: sourceToken.address,
83+
token_address_destination: destToken.address,
84+
};
85+
86+
expect(mockTrackEvent).toHaveBeenCalledTimes(2);
87+
expect(mockCreateEventBuilder.mock.calls[0][0]).toBe(
88+
MetaMetricsEvents.SWAP_PAGE_VIEWED,
89+
);
90+
expect(mockCreateEventBuilder.mock.calls[1][0]).toBe(
91+
MetaMetricsEvents.ASSET_VIEWED,
92+
);
93+
94+
const swapBuilder = mockCreateEventBuilder.mock.results[0].value;
95+
const assetViewedBuilder = mockCreateEventBuilder.mock.results[1].value;
96+
97+
expect(swapBuilder.addProperties).toHaveBeenCalledWith(
98+
expectedPageProperties,
99+
);
100+
expect(assetViewedBuilder.addProperties).toHaveBeenCalledWith({
101+
...expectedPageProperties,
102+
trade_type: 'Swaps',
103+
implementation_type: 'native',
104+
});
105+
});
106+
107+
it('tracks at most once per hook mount when the source token stays set', () => {
108+
mockUseSelector.mockImplementation((selector: unknown) => {
109+
if (selector === selectSourceToken) {
110+
return sourceToken;
111+
}
112+
if (selector === selectDestToken) {
113+
return destToken;
114+
}
115+
return undefined;
116+
});
117+
118+
const { rerender } = renderHook(() => useTrackSwapPageViewed());
119+
120+
rerender(undefined);
121+
122+
expect(mockTrackEvent).toHaveBeenCalledTimes(2);
123+
});
124+
});

app/components/UI/Bridge/hooks/useTrackSwapPageViewed/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { useEffect, useRef } from 'react';
22
import { useSelector } from 'react-redux';
33
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
4-
import { MetaMetricsEvents } from '../../../../../core/Analytics';
4+
import {
5+
MetaMetricsEvents,
6+
mergeAssetViewedProperties,
7+
} from '../../../../../core/Analytics';
58
import { getDecimalChainId } from '../../../../../util/networks';
69
import {
710
selectDestToken,
@@ -33,6 +36,13 @@ export const useTrackSwapPageViewed = () => {
3336
.addProperties(pageViewedProperties)
3437
.build(),
3538
);
39+
trackEvent(
40+
createEventBuilder(MetaMetricsEvents.ASSET_VIEWED)
41+
.addProperties(
42+
mergeAssetViewedProperties('Swaps', pageViewedProperties),
43+
)
44+
.build(),
45+
);
3646
}
3747
}, [sourceToken, destToken, trackEvent, createEventBuilder]);
3848
};

app/components/UI/Perps/components/PerpsOpenOrderCard/PerpsOpenOrderCard.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ import {
4040
PERPS_EVENT_PROPERTY,
4141
PERPS_EVENT_VALUE,
4242
} from '@metamask/perps-controller';
43-
import { MetaMetricsEvents } from '../../../../../core/Analytics';
43+
import {
44+
MetaMetricsEvents,
45+
mergeAssetViewedProperties,
46+
} from '../../../../../core/Analytics';
4447
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
4548

4649
/**
@@ -144,14 +147,21 @@ const PerpsOpenOrderCard: React.FC<PerpsOpenOrderCardProps> = ({
144147

145148
if (!isEligible) {
146149
// Track geo-block screen viewed
150+
const geoBlockProperties = {
151+
[PERPS_EVENT_PROPERTY.SCREEN_TYPE]:
152+
PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF,
153+
[PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.CANCEL_ORDER,
154+
};
147155
trackEvent(
148156
createEventBuilder(MetaMetricsEvents.PERPS_SCREEN_VIEWED)
149-
.addProperties({
150-
[PERPS_EVENT_PROPERTY.SCREEN_TYPE]:
151-
PERPS_EVENT_VALUE.SCREEN_TYPE.GEO_BLOCK_NOTIF,
152-
[PERPS_EVENT_PROPERTY.SOURCE]:
153-
PERPS_EVENT_VALUE.SOURCE.CANCEL_ORDER,
154-
})
157+
.addProperties(geoBlockProperties)
158+
.build(),
159+
);
160+
trackEvent(
161+
createEventBuilder(MetaMetricsEvents.ASSET_VIEWED)
162+
.addProperties(
163+
mergeAssetViewedProperties('Perps', geoBlockProperties),
164+
)
155165
.build(),
156166
);
157167
setIsEligibilityModalVisible(true);

app/components/UI/Perps/hooks/usePerpsEventTracking.test.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { renderHook, act } from '@testing-library/react-native';
22
import { usePerpsEventTracking } from './usePerpsEventTracking';
3-
import { PERPS_EVENT_PROPERTY } from '@metamask/perps-controller';
3+
import {
4+
PERPS_EVENT_PROPERTY,
5+
PERPS_EVENT_VALUE,
6+
} from '@metamask/perps-controller';
47
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
8+
import { MetaMetricsEvents } from '../../../../core/Analytics';
59

610
const mockTrackEvent = jest.fn();
711
const mockCreateEventBuilder = jest.fn();
@@ -13,11 +17,10 @@ describe('usePerpsEventTracking', () => {
1317
jest.clearAllMocks();
1418
jest.spyOn(Date, 'now').mockReturnValue(1234567890);
1519

16-
const eventBuilder = {
20+
mockCreateEventBuilder.mockImplementation(() => ({
1721
addProperties: jest.fn().mockReturnThis(),
1822
build: jest.fn().mockReturnValue({ type: 'mock-event' }),
19-
};
20-
mockCreateEventBuilder.mockReturnValue(eventBuilder);
23+
}));
2124
jest.mocked(useAnalytics).mockReturnValue({
2225
trackEvent: mockTrackEvent,
2326
createEventBuilder: mockCreateEventBuilder,
@@ -67,5 +70,77 @@ describe('usePerpsEventTracking', () => {
6770
...customProps,
6871
});
6972
});
73+
74+
it('tracks Asset Viewed when PERPS_SCREEN_VIEWED is tracked', () => {
75+
const { result } = renderHook(() => usePerpsEventTracking());
76+
const customProps = {
77+
screen_type: 'home',
78+
[PERPS_EVENT_PROPERTY.OPEN_POSITION]: 2,
79+
};
80+
81+
act(() => {
82+
result.current.track(
83+
MetaMetricsEvents.PERPS_SCREEN_VIEWED,
84+
customProps,
85+
);
86+
});
87+
88+
expect(mockTrackEvent).toHaveBeenCalledTimes(2);
89+
expect(mockCreateEventBuilder.mock.calls[0][0]).toBe(
90+
MetaMetricsEvents.PERPS_SCREEN_VIEWED,
91+
);
92+
expect(mockCreateEventBuilder.mock.calls[1][0]).toBe(
93+
MetaMetricsEvents.ASSET_VIEWED,
94+
);
95+
96+
const perpsBuilder = mockCreateEventBuilder.mock.results[0].value;
97+
const assetBuilder = mockCreateEventBuilder.mock.results[1].value;
98+
expect(perpsBuilder.addProperties).toHaveBeenCalledWith({
99+
[PERPS_EVENT_PROPERTY.TIMESTAMP]: 1234567890,
100+
...customProps,
101+
});
102+
const assetViewedProperties = assetBuilder.addProperties.mock
103+
.calls[0][0] as Record<string, unknown>;
104+
expect(assetViewedProperties).toEqual({
105+
[PERPS_EVENT_PROPERTY.TIMESTAMP]: 1234567890,
106+
screen_type: 'home',
107+
open_positions_count: 2,
108+
trade_type: 'Perps',
109+
implementation_type: 'native',
110+
});
111+
expect(assetViewedProperties).not.toHaveProperty(
112+
PERPS_EVENT_PROPERTY.OPEN_POSITION,
113+
);
114+
});
115+
116+
it('does not track Asset Viewed for cancel_all_orders', () => {
117+
const { result } = renderHook(() => usePerpsEventTracking());
118+
const customProps = {
119+
[PERPS_EVENT_PROPERTY.SCREEN_TYPE]:
120+
PERPS_EVENT_VALUE.SCREEN_TYPE.CANCEL_ALL_ORDERS,
121+
[PERPS_EVENT_PROPERTY.OPEN_POSITION]: 3,
122+
[PERPS_EVENT_PROPERTY.SOURCE]:
123+
PERPS_EVENT_VALUE.SOURCE.CANCEL_ALL_ORDERS_BUTTON,
124+
};
125+
126+
act(() => {
127+
result.current.track(
128+
MetaMetricsEvents.PERPS_SCREEN_VIEWED,
129+
customProps,
130+
);
131+
});
132+
133+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
134+
expect(mockCreateEventBuilder).toHaveBeenCalledTimes(1);
135+
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
136+
MetaMetricsEvents.PERPS_SCREEN_VIEWED,
137+
);
138+
139+
const perpsBuilder = mockCreateEventBuilder.mock.results[0].value;
140+
expect(perpsBuilder.addProperties).toHaveBeenCalledWith({
141+
[PERPS_EVENT_PROPERTY.TIMESTAMP]: 1234567890,
142+
...customProps,
143+
});
144+
});
70145
});
71146
});

app/components/UI/Perps/hooks/usePerpsEventTracking.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
11
import { useCallback, useEffect, useRef, useMemo } from 'react';
2-
import { MetaMetricsEvents } from '../../../../core/Analytics';
2+
import {
3+
MetaMetricsEvents,
4+
mergeAssetViewedProperties,
5+
} from '../../../../core/Analytics';
36
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
4-
import { PERPS_EVENT_PROPERTY } from '@metamask/perps-controller';
7+
import {
8+
PERPS_EVENT_PROPERTY,
9+
PERPS_EVENT_VALUE,
10+
} from '@metamask/perps-controller';
511

612
// Static helper function - moved outside component to avoid recreation
713
const allTrue = (conditionArray: boolean[]): boolean =>
814
conditionArray.length > 0 && conditionArray.every(Boolean);
915

16+
/**
17+
* Perps screen types excluded from the parallel Asset Viewed emission.
18+
*
19+
* `screen_type: 'cancel_all_orders'` (PERPS_EVENT_VALUE.SCREEN_TYPE.CANCEL_ALL_ORDERS)
20+
* sends open-order count in legacy `open_position`. `mergeAssetViewedProperties`
21+
* would map that to `open_positions_count`, so we skip Asset Viewed for this flow.
22+
*/
23+
const PERPS_SCREEN_TYPES_SKIP_ASSET_VIEWED: ReadonlySet<string> = new Set([
24+
PERPS_EVENT_VALUE.SCREEN_TYPE.CANCEL_ALL_ORDERS,
25+
]);
26+
27+
const shouldEmitAssetViewedForPerpsScreenViewed = (
28+
perpsScreenViewedProperties: Record<string, unknown>,
29+
): boolean => {
30+
const screenType =
31+
perpsScreenViewedProperties[PERPS_EVENT_PROPERTY.SCREEN_TYPE];
32+
return !(
33+
typeof screenType === 'string' &&
34+
PERPS_SCREEN_TYPES_SKIP_ASSET_VIEWED.has(screenType)
35+
);
36+
};
37+
1038
interface EventTrackingOptions {
1139
eventName: (typeof MetaMetricsEvents)[keyof typeof MetaMetricsEvents];
1240
properties?: Record<string, unknown>;
@@ -63,6 +91,17 @@ export const usePerpsEventTracking = (options?: EventTrackingOptions) => {
6391
...properties,
6492
};
6593
trackEvent(createEventBuilder(eventName).addProperties(props).build());
94+
95+
if (
96+
eventName === MetaMetricsEvents.PERPS_SCREEN_VIEWED &&
97+
shouldEmitAssetViewedForPerpsScreenViewed(props)
98+
) {
99+
trackEvent(
100+
createEventBuilder(MetaMetricsEvents.ASSET_VIEWED)
101+
.addProperties(mergeAssetViewedProperties('Perps', props))
102+
.build(),
103+
);
104+
}
66105
},
67106
[trackEvent, createEventBuilder],
68107
);

0 commit comments

Comments
 (0)