Skip to content

Commit b65bcbd

Browse files
authored
chore: track explore conversions in predict cp-7.80.0 (MetaMask#30722)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Track explore conversions in predict <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: track explore conversions in predict ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3271 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **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** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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] > **Low Risk** > Analytics attribution and navigation params only; no changes to trading, auth, or payment logic. > > **Overview** > This PR wires **Predict analytics `entryPoint`** through navigation and the feed so Explore-driven journeys can be measured as conversions. > > **`PredictFeed`** now accepts an optional `entryPoint` prop (overriding route params), uses that value for session attribution and performance traces, and passes a resolved `listEntryPoint` into market cards—defaulting to `predict_feed` when nothing is set. Search results still use the search entry point. > > **Explore → Predict list:** `navigateToPredictionsList` always sends `entryPoint` in route params (default **explore**); a new `navigateToExplorePredictionsList` helper is used from Explore tabs (Now, Crypto, Sports, Macro, RWAs). **Homepage** embedded feed explicitly passes `predict_feed`. > > Tests assert explore entry points on buy preview order events, market details, feed market cards, and homepage discovery tabs. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8c59bfc. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8592d48 commit b65bcbd

13 files changed

Lines changed: 252 additions & 28 deletions

File tree

app/components/UI/Predict/views/PredictBuyPreview/PredictBuyPreview.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,6 +2185,33 @@ describe('PredictBuyPreview', () => {
21852185
).toBeOnTheScreen();
21862186
});
21872187

2188+
it('tracks initiated trade transaction with explore entry point', () => {
2189+
mockUseRoute.mockReturnValue({
2190+
...mockRoute,
2191+
params: {
2192+
...mockRoute.params,
2193+
entryPoint: PredictEventValues.ENTRY_POINT.EXPLORE,
2194+
},
2195+
});
2196+
mockBalance = 1000;
2197+
mockBalanceLoading = false;
2198+
2199+
renderWithProvider(<PredictBuyPreview />, { state: initialState });
2200+
2201+
const trackPredictOrderEvent =
2202+
// eslint-disable-next-line @typescript-eslint/no-require-imports
2203+
require('../../../../../core/Engine').context.PredictController
2204+
.trackPredictOrderEvent;
2205+
2206+
expect(trackPredictOrderEvent).toHaveBeenCalledWith(
2207+
expect.objectContaining({
2208+
analyticsProperties: expect.objectContaining({
2209+
entryPoint: PredictEventValues.ENTRY_POINT.EXPLORE,
2210+
}),
2211+
}),
2212+
);
2213+
});
2214+
21882215
it('handles undefined entryPoint with fallback', () => {
21892216
const routeWithoutEntryPoint = {
21902217
...mockRoute,

app/components/UI/Predict/views/PredictFeed/PredictFeed.test.tsx

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,22 @@ jest.mock('../../components/PredictMarket', () => {
132132
const { View, Text } = jest.requireActual('react-native');
133133
return {
134134
__esModule: true,
135-
default: jest.fn(({ testID }) => (
135+
default: jest.fn(({ testID, entryPoint }) => (
136136
<View testID={testID}>
137137
<Text>Market Card</Text>
138138
</View>
139139
)),
140140
};
141141
});
142142

143+
import PredictMarket from '../../components/PredictMarket';
144+
import { PredictEventValues } from '../../constants/eventNames';
145+
146+
const mockPredictMarket = PredictMarket as jest.Mock;
147+
148+
const getPredictMarketEntryPoints = () =>
149+
mockPredictMarket.mock.calls.map(([props]) => props.entryPoint);
150+
143151
jest.mock('../../components/PredictMarketSkeleton', () => {
144152
const { View } = jest.requireActual('react-native');
145153
return {
@@ -795,7 +803,7 @@ describe('PredictFeed', () => {
795803
);
796804
});
797805

798-
it('starts session with undefined entry point when not provided', () => {
806+
it('preserves an unattributed session when entry point is not provided', () => {
799807
mockUseRoute.mockReturnValue({
800808
params: {},
801809
});
@@ -807,6 +815,72 @@ describe('PredictFeed', () => {
807815
'trending',
808816
);
809817
});
818+
819+
it('defaults market list items to predict_feed when entry point is not provided', () => {
820+
mockUseRoute.mockReturnValue({
821+
params: {},
822+
});
823+
824+
render(<PredictFeed />);
825+
826+
const entryPoints = getPredictMarketEntryPoints();
827+
expect(entryPoints.length).toBeGreaterThan(0);
828+
expect(
829+
entryPoints.every(
830+
(entryPoint) =>
831+
entryPoint === PredictEventValues.ENTRY_POINT.PREDICT_FEED,
832+
),
833+
).toBe(true);
834+
});
835+
836+
it('passes explore entryPoint to market list items from route params', () => {
837+
mockUseRoute.mockReturnValue({
838+
params: {
839+
entryPoint: PredictEventValues.ENTRY_POINT.EXPLORE,
840+
},
841+
});
842+
843+
render(<PredictFeed />);
844+
845+
expect(mockSessionManager.startSession).toHaveBeenCalledWith(
846+
PredictEventValues.ENTRY_POINT.EXPLORE,
847+
'trending',
848+
);
849+
const entryPoints = getPredictMarketEntryPoints();
850+
expect(entryPoints.length).toBeGreaterThan(0);
851+
expect(
852+
entryPoints.every(
853+
(entryPoint) => entryPoint === PredictEventValues.ENTRY_POINT.EXPLORE,
854+
),
855+
).toBe(true);
856+
});
857+
858+
it('uses prop entryPoint for embedded feed list items and session attribution', () => {
859+
mockUseRoute.mockReturnValue({
860+
params: {
861+
entryPoint: PredictEventValues.ENTRY_POINT.EXPLORE,
862+
},
863+
});
864+
865+
render(
866+
<PredictFeed
867+
entryPoint={PredictEventValues.ENTRY_POINT.HOME_SECTION}
868+
/>,
869+
);
870+
871+
expect(mockSessionManager.startSession).toHaveBeenCalledWith(
872+
PredictEventValues.ENTRY_POINT.HOME_SECTION,
873+
'trending',
874+
);
875+
const entryPoints = getPredictMarketEntryPoints();
876+
expect(entryPoints.length).toBeGreaterThan(0);
877+
expect(
878+
entryPoints.every(
879+
(entryPoint) =>
880+
entryPoint === PredictEventValues.ENTRY_POINT.HOME_SECTION,
881+
),
882+
).toBe(true);
883+
});
810884
});
811885

812886
describe('search debounce behavior', () => {

app/components/UI/Predict/views/PredictFeed/PredictFeed.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ const PredictMarketListItem: React.FC<PredictMarketListItemProps> = ({
252252
interface PredictTabContentProps {
253253
category: PredictCategory;
254254
isActive: boolean;
255+
listEntryPoint: PredictEntryPoint;
255256
scrollHandler: ReturnType<typeof useAnimatedScrollHandler>;
256257
headerHeight: number;
257258
tabBarHeight: number;
@@ -263,6 +264,7 @@ interface PredictTabContentProps {
263264
const PredictTabContent: React.FC<PredictTabContentProps> = ({
264265
category,
265266
isActive,
267+
listEntryPoint,
266268
scrollHandler,
267269
headerHeight,
268270
tabBarHeight,
@@ -317,15 +319,15 @@ const PredictTabContent: React.FC<PredictTabContentProps> = ({
317319
(info: { item: PredictMarketType; index: number }) => (
318320
<PredictMarketListItem
319321
market={info.item}
320-
entryPoint={PredictEventValues.ENTRY_POINT.PREDICT_FEED}
322+
entryPoint={listEntryPoint}
321323
testID={getPredictMarketListSelector.marketCardByCategory(
322324
category,
323325
info.index + 1, // E2E tests use 1-based indexing
324326
)}
325327
transactionActiveAbTests={transactionActiveAbTests}
326328
/>
327329
),
328-
[category, transactionActiveAbTests],
330+
[category, listEntryPoint, transactionActiveAbTests],
329331
);
330332

331333
const keyExtractor = useCallback((item: PredictMarketType) => item.id, []);
@@ -448,6 +450,7 @@ interface PredictFeedTabsProps {
448450
tabs: FeedTab[];
449451
activeIndex: number;
450452
onPageChange: (index: number) => void;
453+
listEntryPoint: PredictEntryPoint;
451454
scrollHandler: ReturnType<typeof useAnimatedScrollHandler>;
452455
headerHeight: number;
453456
tabBarHeight: number;
@@ -460,6 +463,7 @@ const PredictFeedTabs: React.FC<PredictFeedTabsProps> = ({
460463
tabs,
461464
activeIndex,
462465
onPageChange,
466+
listEntryPoint,
463467
scrollHandler,
464468
headerHeight,
465469
tabBarHeight,
@@ -499,6 +503,7 @@ const PredictFeedTabs: React.FC<PredictFeedTabsProps> = ({
499503
<PredictTabContent
500504
category={tab.key}
501505
isActive={index === activeIndex}
506+
listEntryPoint={listEntryPoint}
502507
scrollHandler={scrollHandler}
503508
headerHeight={headerHeight}
504509
tabBarHeight={tabBarHeight}
@@ -644,13 +649,15 @@ const PredictSearchOverlay: React.FC<PredictSearchOverlayProps> = ({
644649

645650
interface PredictFeedProps {
646651
hideHeader?: boolean;
652+
entryPoint?: PredictEntryPoint;
647653
onHeaderHiddenChange?: (hidden: boolean) => void;
648654
walletHeaderTranslateY?: SharedValue<number>;
649655
walletHeaderHeight?: number;
650656
}
651657

652658
const PredictFeed: React.FC<PredictFeedProps> = ({
653659
hideHeader = false,
660+
entryPoint: propEntryPoint,
654661
onHeaderHiddenChange,
655662
walletHeaderTranslateY,
656663
walletHeaderHeight,
@@ -663,6 +670,9 @@ const PredictFeed: React.FC<PredictFeedProps> = ({
663670
const route =
664671
useRoute<RouteProp<PredictNavigationParamList, 'PredictMarketList'>>();
665672
const transactionActiveAbTests = route.params?.transactionActiveAbTests;
673+
const feedEntryPoint = propEntryPoint ?? route.params?.entryPoint;
674+
const listEntryPoint =
675+
feedEntryPoint ?? PredictEventValues.ENTRY_POINT.PREDICT_FEED;
666676

667677
const headerRef = useRef<View>(null);
668678
const tabBarRef = useRef<View>(null);
@@ -691,20 +701,20 @@ const PredictFeed: React.FC<PredictFeedProps> = ({
691701
traceName: TraceName.PredictFeedView,
692702
conditions: [!isSearchVisible],
693703
debugContext: {
694-
entryPoint: route.params?.entryPoint,
704+
entryPoint: feedEntryPoint,
695705
isSearchVisible,
696706
},
697707
});
698708

699709
useEffect(() => {
700710
sessionManager.enableAppStateListener();
701-
sessionManager.startSession(route.params?.entryPoint, initialTabKey);
711+
sessionManager.startSession(feedEntryPoint, initialTabKey);
702712

703713
return () => {
704714
sessionManager.endSession();
705715
sessionManager.disableAppStateListener();
706716
};
707-
}, [route.params?.entryPoint, sessionManager, initialTabKey]);
717+
}, [feedEntryPoint, sessionManager, initialTabKey]);
708718

709719
useFocusEffect(
710720
useCallback(() => {
@@ -808,6 +818,7 @@ const PredictFeed: React.FC<PredictFeedProps> = ({
808818
tabs={tabs}
809819
activeIndex={activeIndex}
810820
onPageChange={handlePageChange}
821+
listEntryPoint={listEntryPoint}
811822
scrollHandler={scrollHandler}
812823
headerHeight={headerHeight}
813824
tabBarHeight={tabBarHeight + 6}

app/components/UI/Predict/views/PredictMarketDetails/PredictMarketDetails.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,31 @@ describe('PredictMarketDetails', () => {
798798
expect(screen.getByText(mockMarket.title)).toBeOnTheScreen();
799799
});
800800

801+
it('tracks market details opened with explore entry point from route params', async () => {
802+
setupPredictMarketDetailsTest(
803+
{},
804+
{
805+
params: {
806+
marketId: 'market-1',
807+
entryPoint: PredictEventValues.ENTRY_POINT.EXPLORE,
808+
},
809+
},
810+
);
811+
812+
const trackMarketDetailsOpened =
813+
// eslint-disable-next-line @typescript-eslint/no-require-imports
814+
require('../../../../../core/Engine').context.PredictController
815+
.trackMarketDetailsOpened;
816+
817+
await waitFor(() => {
818+
expect(trackMarketDetailsOpened).toHaveBeenCalledWith(
819+
expect.objectContaining({
820+
entryPoint: PredictEventValues.ENTRY_POINT.EXPLORE,
821+
}),
822+
);
823+
});
824+
});
825+
801826
it('displays loading state when market is fetching', () => {
802827
setupPredictMarketDetailsTest(
803828
{},

app/components/Views/Homepage/components/HomepageDiscoveryTabs/HomepageDiscoveryTabs.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { InteractionManager, Text } from 'react-native';
33
import { act, fireEvent, render, screen } from '@testing-library/react-native';
44
import type { SectionRefreshHandle } from '../../types';
55
import HomepageDiscoveryTabs from './HomepageDiscoveryTabs';
6+
import { PredictEventValues } from '../../../../UI/Predict/constants/eventNames';
67
import { HomeTabNames } from '../../hooks/useTabViewedEvent';
78

89
const mockTrackTabViewed = jest.fn();
@@ -213,6 +214,18 @@ describe('HomepageDiscoveryTabs', () => {
213214
});
214215
});
215216

217+
describe('Predictions tab', () => {
218+
it('passes Predict feed entry point explicitly to embedded PredictFeed', async () => {
219+
renderComponent();
220+
221+
await pressTab('Predictions');
222+
223+
expect(mockPredictFeedProps.current).toMatchObject({
224+
entryPoint: PredictEventValues.ENTRY_POINT.PREDICT_FEED,
225+
});
226+
});
227+
});
228+
216229
describe('walletHeaderOffset prop', () => {
217230
it('renders without throwing when walletHeaderOffset is provided', () => {
218231
expect(() => renderComponent({ walletHeaderOffset: 100 })).not.toThrow();

app/components/Views/Homepage/components/HomepageDiscoveryTabs/HomepageDiscoveryTabs.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { TabsIconListRef } from '../../../../../component-library/components-tem
2525
import Homepage from '../../Homepage';
2626
import PerpsHomeView from '../../../../UI/Perps/Views/PerpsHomeView/PerpsHomeView';
2727
import PredictFeed from '../../../../UI/Predict/views/PredictFeed';
28+
import { PredictEventValues } from '../../../../UI/Predict/constants/eventNames';
2829
import { PerpsConnectionProvider } from '../../../../UI/Perps/providers/PerpsConnectionProvider';
2930
import {
3031
PerpsStreamProvider,
@@ -388,6 +389,7 @@ const HomepageDiscoveryTabs = forwardRef<
388389
<PredictPreviewSheetProvider disableBottomSheet>
389390
<PredictFeed
390391
hideHeader
392+
entryPoint={PredictEventValues.ENTRY_POINT.PREDICT_FEED}
391393
walletHeaderTranslateY={walletHeaderTranslateY}
392394
walletHeaderHeight={walletHeaderHeight}
393395
onHeaderHiddenChange={animateIcons}

0 commit comments

Comments
 (0)