diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsView/PerpsMarketDetailsView.test.tsx index 0c4c30acf07..28bd6e07d45 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,34 @@ jest.mock('../../hooks/usePerpsEventTracking', () => ({ })), })); +const mockUseMarketInsights = jest.fn( + (_assetId?: string | null, _isEnabled?: boolean) => ({ + report: null as Record | null, + isLoading: false, + error: null, + timeAgo: '', + }), +); + +jest.mock('../../../MarketInsights', () => ({ + useMarketInsights: (assetId: string | null | undefined, isEnabled: boolean) => + mockUseMarketInsights(assetId, isEnabled), + 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 +3294,134 @@ 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, + }), + }), + ); + }); + }); }); 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(