Skip to content

Commit 99b3e05

Browse files
authored
feat: track analytics events for View All button clicks in NFT and Token lists (MetaMask#23107)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Implemented analytics tracking for the "View All" buttons on the Tokens and NFTs pages to measure user engagement with the homepage performance optimization feature. **Reason for change:** The recent homepage redesign added "View All" buttons to limit the number of tokens and NFTs displayed initially. However, we weren't tracking user interactions with these buttons, making it difficult to understand adoption and usage patterns. **Solution:** - Added new Segment event `View All Assets Clicked` that fires when users tap either button - Event includes `asset_type` property to differentiate between Token and NFT interactions - Updated both TokenList and NftGrid components to track the event - Added comprehensive unit tests to verify tracking implementation ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: TMCU-206 ## **Manual testing steps** ```gherkin Feature: View All Assets Analytics Tracking Scenario: user views all tokens from homepage Given the user is on the wallet homepage with more than 10 tokens When user taps the "View all tokens" button Then the "View All Assets Clicked" event is sent to Segment with asset_type: Token And user navigates to the full tokens list view Scenario: user views all NFTs from homepage Given the user is on the wallet homepage with more than 18 NFTs When user taps the "View all NFTs" button Then the "View All Assets Clicked" event is sent to Segment with asset_type: NFT And user navigates to the full NFTs list view ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds `VIEW_ALL_ASSETS_CLICKED` tracking with `asset_type` for Tokens and NFTs and updates tests accordingly. > > - **Analytics** > - Add `MetaMetricsEvents.VIEW_ALL_ASSETS_CLICKED` to `app/core/Analytics/MetaMetrics.events.ts` (enum and `events` map). > - **UI** > - `app/components/UI/Tokens/TokenList/index.tsx`: Track `VIEW_ALL_ASSETS_CLICKED` with `{ asset_type: 'Token' }` in `handleViewAllTokens`; include metrics deps in callback. > - `app/components/UI/NftGrid/NftGrid.tsx`: Track `VIEW_ALL_ASSETS_CLICKED` with `{ asset_type: 'NFT' }` in `handleViewAllNfts`; include metrics deps in callback. > - **Tests** > - `app/components/UI/Tokens/TokenList/index.test.tsx`: Mock metrics and assert event fired on "View all tokens" press. > - `app/components/UI/NftGrid/NftGrid.test.tsx`: Assert event fired on "View all NFTs" press. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d2d75cd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 44ac101 commit 99b3e05

5 files changed

Lines changed: 91 additions & 2 deletions

File tree

app/components/UI/NftGrid/NftGrid.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,45 @@ describe('NftGrid', () => {
434434
});
435435
});
436436

437+
it('tracks analytics event when view all button is clicked', async () => {
438+
const mockCollectibles = {
439+
'0x1': Array.from({ length: 20 }, (_, i) => ({
440+
...mockNft,
441+
tokenId: `${i}`,
442+
})),
443+
};
444+
mockUseSelector
445+
.mockReturnValueOnce(false) // isNftFetchingProgress
446+
.mockReturnValueOnce(true) // selectHomepageRedesignV1Enabled (maxItems = 18)
447+
.mockReturnValueOnce(mockCollectibles); // multichainCollectiblesByEnabledNetworksSelector
448+
const store = mockStore(initialState);
449+
450+
const { getByTestId } = render(
451+
<Provider store={store}>
452+
<NftGrid />
453+
</Provider>,
454+
);
455+
456+
act(() => {
457+
jest.advanceTimersByTime(100);
458+
});
459+
460+
await waitFor(() => {
461+
expect(getByTestId('view-all-nfts-button')).toBeOnTheScreen();
462+
});
463+
464+
fireEvent.press(getByTestId('view-all-nfts-button'));
465+
466+
expect(mockTrackEvent).toHaveBeenCalledWith(
467+
expect.objectContaining({
468+
name: 'View All Assets Clicked',
469+
properties: expect.objectContaining({
470+
asset_type: 'NFT',
471+
}),
472+
}),
473+
);
474+
});
475+
437476
it('hides view all button when homepage redesign is disabled', async () => {
438477
const mockCollectibles = {
439478
'0x1': Array.from({ length: 20 }, (_, i) => ({

app/components/UI/NftGrid/NftGrid.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,13 @@ const NftGrid = ({ isFullView = false }: NftGridProps) => {
139139
}, [navigation, trackEvent, createEventBuilder]);
140140

141141
const handleViewAllNfts = useCallback(() => {
142+
trackEvent(
143+
createEventBuilder(MetaMetricsEvents.VIEW_ALL_ASSETS_CLICKED)
144+
.addProperties({ asset_type: 'NFT' })
145+
.build(),
146+
);
142147
navigation.navigate(Routes.WALLET.NFTS_FULL_VIEW);
143-
}, [navigation]);
148+
}, [navigation, trackEvent, createEventBuilder]);
144149

145150
const nftRowList =
146151
!isFullView && isHomepageRedesignV1Enabled ? (

app/components/UI/Tokens/TokenList/index.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import configureMockStore from 'redux-mock-store';
55
import { TokenList } from './index';
66
import { useNavigation } from '@react-navigation/native';
77
import { WalletViewSelectorsIDs } from '../../../../../e2e/selectors/wallet/WalletView.selectors';
8+
import { useMetrics } from '../../../hooks/useMetrics';
9+
import { MetricsEventBuilder } from '../../../../core/Analytics/MetricsEventBuilder';
810

911
// Mock external dependencies
1012
jest.mock('@react-navigation/native', () => ({
1113
useNavigation: jest.fn(),
1214
}));
1315

16+
jest.mock('../../../hooks/useMetrics');
17+
1418
jest.mock('../../../../util/theme', () => ({
1519
useTheme: () => ({
1620
colors: {
@@ -127,10 +131,12 @@ jest.mock('@shopify/flash-list', () => {
127131

128132
const mockStore = configureMockStore();
129133
const mockNavigate = jest.fn();
134+
const mockTrackEvent = jest.fn();
130135
const mockUseNavigation = useNavigation as jest.MockedFunction<
131136
typeof useNavigation
132137
>;
133138
const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
139+
const mockUseMetrics = useMetrics as jest.MockedFunction<typeof useMetrics>;
134140

135141
const mockTokenKeys = [
136142
{
@@ -162,6 +168,20 @@ describe('TokenList', () => {
162168
navigate: mockNavigate,
163169
} as unknown as ReturnType<typeof useNavigation>);
164170

171+
mockUseMetrics.mockReturnValue({
172+
trackEvent: mockTrackEvent,
173+
createEventBuilder: MetricsEventBuilder.createEventBuilder,
174+
enable: jest.fn(),
175+
addTraitsToUser: jest.fn(),
176+
createDataDeletionTask: jest.fn(),
177+
checkDataDeleteStatus: jest.fn(),
178+
getDeleteRegulationCreationDate: jest.fn(),
179+
getDeleteRegulationId: jest.fn(),
180+
isDataRecorded: jest.fn(),
181+
isEnabled: jest.fn(),
182+
getMetaMetricsId: jest.fn(),
183+
});
184+
165185
// Mock useSelector to call the selector function with empty state
166186
mockUseSelector.mockImplementation((selector) => selector({}));
167187
});
@@ -227,6 +247,22 @@ describe('TokenList', () => {
227247
expect(mockNavigate).toHaveBeenCalledWith('TokensFullView');
228248
});
229249

250+
it('tracks analytics event when view all button is pressed', () => {
251+
const { getByText } = renderComponent({ maxItems: 1 });
252+
253+
const viewAllButton = getByText('wallet.view_all_tokens');
254+
fireEvent.press(viewAllButton);
255+
256+
expect(mockTrackEvent).toHaveBeenCalledWith(
257+
expect.objectContaining({
258+
name: 'View All Assets Clicked',
259+
properties: expect.objectContaining({
260+
asset_type: 'Token',
261+
}),
262+
}),
263+
);
264+
});
265+
230266
it('renders container without items when tokenKeys is empty', () => {
231267
const { getByTestId, queryByTestId } = renderComponent({ tokenKeys: [] });
232268

app/components/UI/Tokens/TokenList/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
ButtonVariant,
2323
} from '@metamask/design-system-react-native';
2424
import { useTailwind } from '@metamask/design-system-twrnc-preset';
25+
import { MetaMetricsEvents, useMetrics } from '../../../hooks/useMetrics';
2526

2627
export interface FlashListAssetKey {
2728
address: string;
@@ -71,14 +72,20 @@ const TokenListComponent = ({
7172
const listRef = useRef<FlashListRef<FlashListAssetKey>>(null);
7273

7374
const navigation = useNavigation();
75+
const { trackEvent, createEventBuilder } = useMetrics();
7476

7577
useLayoutEffect(() => {
7678
listRef.current?.recomputeViewableItems();
7779
}, [isTokenNetworkFilterEqualCurrentNetwork]);
7880

7981
const handleViewAllTokens = useCallback(() => {
82+
trackEvent(
83+
createEventBuilder(MetaMetricsEvents.VIEW_ALL_ASSETS_CLICKED)
84+
.addProperties({ asset_type: 'Token' })
85+
.build(),
86+
);
8087
navigation.navigate(Routes.WALLET.TOKENS_FULL_VIEW);
81-
}, [navigation]);
88+
}, [navigation, trackEvent, createEventBuilder]);
8289

8390
// Apply maxItems limit if specified
8491
const displayTokenKeys = useMemo(

app/core/Analytics/MetaMetrics.events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ enum EVENT_NAME {
7777
TOKEN_LIST_ITEM_CLICKED = 'Token List Item Clicked',
7878
DEFI_TAB_SELECTED = 'DeFi Tab Selected',
7979
DEFI_PROTOCOL_DETAILS_OPENED = 'DeFi Protocol Details Opened',
80+
VIEW_ALL_ASSETS_CLICKED = 'View All Assets Clicked',
8081

8182
// Network
8283
NETWORK_SWITCHED = 'Network Switched',
@@ -682,6 +683,7 @@ const events = {
682683
DEFI_PROTOCOL_DETAILS_OPENED: generateOpt(
683684
EVENT_NAME.DEFI_PROTOCOL_DETAILS_OPENED,
684685
),
686+
VIEW_ALL_ASSETS_CLICKED: generateOpt(EVENT_NAME.VIEW_ALL_ASSETS_CLICKED),
685687
CURRENCY_CHANGED: generateOpt(EVENT_NAME.CURRENCY_CHANGED),
686688
NETWORK_SWITCHED: generateOpt(EVENT_NAME.NETWORK_SWITCHED),
687689
NETWORK_ADDED: generateOpt(EVENT_NAME.NETWORK_ADDED),

0 commit comments

Comments
 (0)