From 4f7cb6871eacd2b43c091d99ab2fc00d6467c986 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 23 Mar 2026 14:54:39 +0000 Subject: [PATCH 1/3] chore: adds metrics --- .../PerpsMarketDetailsView.test.tsx | 184 ++++++++++++++++++ .../PerpsMarketDetailsView.tsx | 17 +- 2 files changed, 198 insertions(+), 3 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx index 0c4c30acf07..6f580ea06b0 100644 --- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx @@ -10,6 +10,8 @@ import { import { PerpsConnectionProvider } from '../../providers/PerpsConnectionProvider'; import { useDefaultPayWithTokenWhenNoPerpsBalance } from '../../hooks/useDefaultPayWithTokenWhenNoPerpsBalance'; import { Linking } from 'react-native'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; +import Routes from '../../../../../constants/navigation/Routes'; // Mock Linking jest.mock('react-native/Libraries/Linking/Linking', () => ({ @@ -394,6 +396,31 @@ jest.mock('../../hooks/usePerpsEventTracking', () => ({ })), })); +const mockUseMarketInsights = jest.fn(() => ({ + report: null, + isLoading: false, + error: null, + timeAgo: '', +})); + +jest.mock('../../../MarketInsights', () => ({ + useMarketInsights: (...args: unknown[]) => mockUseMarketInsights(...args), + MarketInsightsEntryCard: ({ onPress }: { onPress: () => void }) => { + const { TouchableOpacity } = jest.requireActual('react-native'); + return ( + + ); + }, + selectMarketInsightsEnabled: jest.fn(), +})); + +jest.mock( + '../../../../../selectors/featureFlagController/marketInsights', + () => ({ + selectMarketInsightsPerpsEnabled: jest.fn(), + }), +); + jest.mock('../../hooks/usePerpsPrices', () => ({ usePerpsPrices: jest.fn(() => ({})), })); @@ -3264,4 +3291,161 @@ describe('PerpsMarketDetailsView', () => { expect(queryByText('25x')).toBeNull(); }); }); + + describe('Market Insights analytics', () => { + const mockReport = { + summary: 'BTC momentum is building with increased buying pressure.', + sentiment: 'bullish', + generatedAt: new Date().toISOString(), + }; + + // Stable track mock reference set up in beforeEach via mockImplementation + const mockTrack = jest.fn(); + + beforeEach(() => { + // Override usePerpsEventTracking to expose a capturable track mock + const { usePerpsEventTracking: mockUsePerpsEventTrackingFn } = + jest.requireMock('../../hooks/usePerpsEventTracking'); + mockUsePerpsEventTrackingFn.mockImplementation(() => ({ + track: mockTrack, + })); + + // Enable perps market insights feature flag + const { useSelector } = jest.requireMock('react-redux'); + const { selectPerpsEligibility } = jest.requireMock( + '../../selectors/perpsController', + ); + const { selectMarketInsightsPerpsEnabled } = jest.requireMock( + '../../../../../selectors/featureFlagController/marketInsights', + ); + useSelector.mockImplementation((selector: unknown) => { + if (selector === selectPerpsEligibility) return true; + if (selector === selectMarketInsightsPerpsEnabled) return true; + return undefined; + }); + + // Default: a report is available and loading is complete + mockUseMarketInsights.mockReturnValue({ + report: mockReport, + isLoading: false, + error: null, + timeAgo: '5m ago', + }); + }); + + afterEach(() => { + mockTrack.mockClear(); + }); + + it('fires MARKET_INSIGHTS_OPENED with perps_market when entry card is pressed', () => { + const { getByTestId } = renderWithProvider( + + + , + { state: initialState }, + ); + + fireEvent.press(getByTestId('market-insights-entry-card')); + + expect(mockTrack).toHaveBeenCalledWith( + MetaMetricsEvents.MARKET_INSIGHTS_OPENED, + expect.objectContaining({ perps_market: 'BTC' }), + ); + }); + + it('navigates to MarketInsightsView with isPerps flag when entry card is pressed', () => { + const { getByTestId } = renderWithProvider( + + + , + { state: initialState }, + ); + + fireEvent.press(getByTestId('market-insights-entry-card')); + + expect(mockNavigate).toHaveBeenCalledWith( + Routes.MARKET_INSIGHTS.VIEW, + expect.objectContaining({ + assetIdentifier: 'BTC', + isPerps: true, + }), + ); + }); + + it('passes market_insights_displayed: true to PERPS_SCREEN_VIEWED when a report is available', () => { + renderWithProvider( + + + , + { state: initialState }, + ); + + const { usePerpsEventTracking: mockUsePerpsEventTrackingFn } = + jest.requireMock('../../hooks/usePerpsEventTracking'); + + expect(mockUsePerpsEventTrackingFn).toHaveBeenCalledWith( + expect.objectContaining({ + eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, + properties: expect.objectContaining({ + market_insights_displayed: true, + }), + }), + ); + }); + + it('passes market_insights_displayed: false to PERPS_SCREEN_VIEWED when no report is returned', () => { + mockUseMarketInsights.mockReturnValue({ + report: null, + isLoading: false, + error: null, + timeAgo: '', + }); + + renderWithProvider( + + + , + { state: initialState }, + ); + + const { usePerpsEventTracking: mockUsePerpsEventTrackingFn } = + jest.requireMock('../../hooks/usePerpsEventTracking'); + + expect(mockUsePerpsEventTrackingFn).toHaveBeenCalledWith( + expect.objectContaining({ + eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, + properties: expect.objectContaining({ + market_insights_displayed: false, + }), + }), + ); + }); + + it('blocks PERPS_SCREEN_VIEWED until market insights loading resolves', () => { + mockUseMarketInsights.mockReturnValue({ + report: null, + isLoading: true, + error: null, + timeAgo: '', + }); + + renderWithProvider( + + + , + { state: initialState }, + ); + + const { usePerpsEventTracking: mockUsePerpsEventTrackingFn } = + jest.requireMock('../../hooks/usePerpsEventTracking'); + + // The conditions array must include false (!isLoading = false) to block tracking + expect(mockUsePerpsEventTrackingFn).toHaveBeenCalledWith( + expect.objectContaining({ + eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, + conditions: expect.arrayContaining([false]), + }), + ); + }); + }); }); diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx index 8f879e8bfc9..8fccd94f0b9 100644 --- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.tsx @@ -229,8 +229,11 @@ const PerpsMarketDetailsView: React.FC = () => { // Feature flag for Market Insights in Perps const isPerpsInsightsEnabled = useSelector(selectMarketInsightsPerpsEnabled); - const { report: perpsInsightsReport, timeAgo: perpsInsightsTimeAgo } = - useMarketInsights(market?.symbol, isPerpsInsightsEnabled); + const { + report: perpsInsightsReport, + timeAgo: perpsInsightsTimeAgo, + isLoading: isPerpsInsightsLoading, + } = useMarketInsights(market?.symbol, isPerpsInsightsEnabled); // Check if current market is in watchlist const selectIsWatchlist = useMemo( @@ -542,6 +545,8 @@ const PerpsMarketDetailsView: React.FC = () => { }); // Track asset screen viewed event - declarative (main's event name) + // Waits for market insights to finish loading so market_insights_displayed + // reflects the actual display state rather than a loading-time snapshot. usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, conditions: [ @@ -549,6 +554,7 @@ const PerpsMarketDetailsView: React.FC = () => { !!marketStats, !isLoadingHistory, !isLoadingPosition, + !isPerpsInsightsLoading, ], properties: { [PERPS_EVENT_PROPERTY.SCREEN_TYPE]: @@ -558,6 +564,8 @@ const PerpsMarketDetailsView: React.FC = () => { source || PERPS_EVENT_VALUE.SOURCE.PERP_MARKETS, [PERPS_EVENT_PROPERTY.OPEN_POSITION]: existingPosition ? 1 : 0, [PERPS_EVENT_PROPERTY.OPEN_ORDER]: openOrders.length, + market_insights_displayed: + isPerpsInsightsEnabled && Boolean(perpsInsightsReport), // A/B Test context (TAT-1937) - for baseline exposure tracking ...(isButtonColorTestEnabled && { [PERPS_EVENT_PROPERTY.AB_TEST_BUTTON_COLOR]: buttonColorVariant, @@ -1021,6 +1029,9 @@ const PerpsMarketDetailsView: React.FC = () => { // Handler for market insights card tap - navigates to full market insights view const handleMarketInsightsPress = useCallback(() => { if (!market?.symbol) return; + track(MetaMetricsEvents.MARKET_INSIGHTS_OPENED, { + perps_market: market.symbol, + }); trace({ name: TraceName.MarketInsightsViewLoad, op: TraceOperation.MarketInsightsLoad, @@ -1030,7 +1041,7 @@ const PerpsMarketDetailsView: React.FC = () => { assetIdentifier: market.symbol, isPerps: true, }); - }, [market?.symbol, navigation]); + }, [market?.symbol, navigation, track]); // Handler for order selection - navigates to order details const handleOrderSelect = useCallback( From 4cd8929fe90d88d0217aefed6e5cb7a454abca8c Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 23 Mar 2026 14:59:30 +0000 Subject: [PATCH 2/3] chore: clean up --- .../PerpsMarketDetailsView.test.tsx | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx index 6f580ea06b0..fda578669f1 100644 --- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx @@ -3420,32 +3420,5 @@ describe('PerpsMarketDetailsView', () => { }), ); }); - - it('blocks PERPS_SCREEN_VIEWED until market insights loading resolves', () => { - mockUseMarketInsights.mockReturnValue({ - report: null, - isLoading: true, - error: null, - timeAgo: '', - }); - - renderWithProvider( - - - , - { state: initialState }, - ); - - const { usePerpsEventTracking: mockUsePerpsEventTrackingFn } = - jest.requireMock('../../hooks/usePerpsEventTracking'); - - // The conditions array must include false (!isLoading = false) to block tracking - expect(mockUsePerpsEventTrackingFn).toHaveBeenCalledWith( - expect.objectContaining({ - eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, - conditions: expect.arrayContaining([false]), - }), - ); - }); }); }); From dc97116e56c15cbf0514e47c2529982ba03b75b8 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Mon, 23 Mar 2026 15:08:41 +0000 Subject: [PATCH 3/3] chore: lint fix and test update --- .../PerpsMarketDetailsView.test.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx index fda578669f1..28bd6e07d45 100644 --- a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx @@ -396,15 +396,18 @@ jest.mock('../../hooks/usePerpsEventTracking', () => ({ })), })); -const mockUseMarketInsights = jest.fn(() => ({ - report: null, - isLoading: false, - error: null, - timeAgo: '', -})); +const mockUseMarketInsights = jest.fn( + (_assetId?: string | null, _isEnabled?: boolean) => ({ + report: null as Record | null, + isLoading: false, + error: null, + timeAgo: '', + }), +); jest.mock('../../../MarketInsights', () => ({ - useMarketInsights: (...args: unknown[]) => mockUseMarketInsights(...args), + useMarketInsights: (assetId: string | null | undefined, isEnabled: boolean) => + mockUseMarketInsights(assetId, isEnabled), MarketInsightsEntryCard: ({ onPress }: { onPress: () => void }) => { const { TouchableOpacity } = jest.requireActual('react-native'); return (