import type {InjectedRouter} from 'react-router'; import {browserHistory} from 'react-router'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; import {TeamFixture} from 'sentry-fixture/team'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {makeTestQueryClient} from 'sentry-test/queryClient'; import { render, renderGlobalModal, screen, userEvent, waitFor, } from 'sentry-test/reactTestingLibrary'; import OrganizationStore from 'sentry/stores/organizationStore'; import ProjectsStore from 'sentry/stores/projectsStore'; import TeamStore from 'sentry/stores/teamStore'; import type {Project} from 'sentry/types'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality'; import { MEPSetting, MEPState, } from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {QueryClientProvider} from 'sentry/utils/queryClient'; import TransactionSummary from 'sentry/views/performance/transactionSummary/transactionOverview'; import {RouteContext} from 'sentry/views/routeContext'; const teams = [ TeamFixture({id: '1', slug: 'team1', name: 'Team 1'}), TeamFixture({id: '2', slug: 'team2', name: 'Team 2'}), ]; function initializeData({ features: additionalFeatures = [], query = {}, project: prj, projects, }: { features?: string[]; project?: Project; projects?: Project[]; query?: Record; } = {}) { const features = ['discover-basic', 'performance-view', ...additionalFeatures]; const project = prj ?? ProjectFixture({teams}); const organization = OrganizationFixture({ features, projects: projects ? projects : [project], }); const initialData = initializeOrg({ organization, router: { location: { query: { transaction: '/performance', project: project.id, transactionCursor: '1:0:0', ...query, }, }, }, }); ProjectsStore.loadInitialData(initialData.organization.projects); TeamStore.loadInitialData(teams, false, null); return initialData; } function TestComponent({ router, ...props }: React.ComponentProps & { router: InjectedRouter, any>; }) { if (!props.organization) { throw new Error('Missing organization'); } return ( ); } describe('Performance > TransactionSummary', function () { let eventStatsMock: jest.Mock; beforeEach(function () { // eslint-disable-next-line no-console jest.spyOn(console, 'error').mockImplementation(jest.fn()); MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/tags/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/tags/user.email/values/', body: [], }); eventStatsMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-stats/', body: {data: [[123, []]]}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/releases/stats/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/?limit=5&project=2&query=is%3Aunresolved%20transaction%3A%2Fperformance&sort=trends&statsPeriod=14d', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/users/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/recent-searches/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/recent-searches/', method: 'POST', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/sdk-updates/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/prompts-activity/', body: {}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-facets-performance/', body: {}, }); // Events Mock totals for the sidebar and other summary data MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', body: { meta: { fields: { 'count()': 'number', 'apdex()': 'number', 'count_miserable_user()': 'number', 'user_misery()': 'number', 'count_unique_user()': 'number', 'p95()': 'number', 'failure_rate()': 'number', 'tpm()': 'number', project_threshold_config: 'string', }, }, data: [ { 'count()': 2, 'apdex()': 0.6, 'count_miserable_user()': 122, 'user_misery()': 0.114, 'count_unique_user()': 1, 'p95()': 750.123, 'failure_rate()': 1, 'tpm()': 1, project_threshold_config: ['duration', 300], }, ], }, match: [ (_url, options) => { return options.query?.field?.includes('p95()'); }, ], }); // [Metrics Enhanced] Events Mock totals for the sidebar and other summary data MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', body: { meta: { fields: { 'count()': 'number', 'apdex()': 'number', 'count_miserable_user()': 'number', 'user_misery()': 'number', 'count_unique_user()': 'number', 'p95()': 'number', 'failure_rate()': 'number', 'tpm()': 'number', project_threshold_config: 'string', }, isMetricsData: true, }, data: [ { 'count()': 200, 'apdex()': 0.5, 'count_miserable_user()': 120, 'user_misery()': 0.1, 'count_unique_user()': 100, 'p95()': 731.3132, 'failure_rate()': 1, 'tpm()': 100, project_threshold_config: ['duration', 300], }, ], }, match: [ (_url, options) => { const isMetricsEnhanced = options.query?.dataset === DiscoverDatasets.METRICS_ENHANCED; return options.query?.field?.includes('p95()') && isMetricsEnhanced; }, ], }); // Events Mock unfiltered totals for percentage calculations MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', body: { meta: { fields: { 'tpm()': 'number', }, }, data: [ { 'tpm()': 1, }, ], }, match: [ (_url, options) => { return ( options.query?.field?.includes('tpm()') && !options.query?.field?.includes('p95()') ); }, ], }); // Events Mock count totals for histogram percentage calculations MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', body: { meta: { fields: { 'count()': 'number', }, }, data: [ { 'count()': 2, }, ], }, match: [ (_url, options) => { return ( options.query?.field?.includes('count()') && !options.query?.field?.includes('p95()') ); }, ], }); // Events Transaction list response MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', headers: { Link: '; rel="next"; results="true"; cursor="2:0:0",' + '; rel="previous"; results="false"; cursor="1:0:0"', }, body: { meta: { fields: { id: 'string', 'user.display': 'string', 'transaction.duration': 'duration', 'project.id': 'integer', timestamp: 'date', }, }, data: [ { id: 'deadbeef', 'user.display': 'uhoh@example.com', 'transaction.duration': 400, 'project.id': 2, timestamp: '2020-05-21T15:31:18+00:00', }, ], }, match: [ (_url, options) => { return options.query?.field?.includes('user.display'); }, ], }); // Events Mock totals for status breakdown MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', body: { meta: { fields: { 'transaction.status': 'string', 'count()': 'number', }, }, data: [ { 'count()': 2, 'transaction.status': 'ok', }, ], }, match: [ (_url, options) => { return options.query?.field?.includes('transaction.status'); }, ], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-facets/', body: [ { key: 'release', topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}], }, { key: 'environment', topValues: [ {count: 2, value: 'dev', name: 'dev'}, {count: 1, value: 'prod', name: 'prod'}, ], }, { key: 'foo', topValues: [ {count: 2, value: 'bar', name: 'bar'}, {count: 1, value: 'baz', name: 'baz'}, ], }, { key: 'user', topValues: [ {count: 2, value: 'id:100', name: '100'}, {count: 1, value: 'id:101', name: '101'}, ], }, ], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/project-transaction-threshold-override/', method: 'GET', body: { threshold: '800', metric: 'lcp', }, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-vitals/', body: { 'measurements.fcp': { poor: 3, meh: 100, good: 47, total: 150, p75: 1500, }, 'measurements.lcp': { poor: 2, meh: 38, good: 40, total: 80, p75: 2750, }, 'measurements.fid': { poor: 2, meh: 53, good: 5, total: 60, p75: 1000, }, 'measurements.cls': { poor: 3, meh: 10, good: 4, total: 17, p75: 0.2, }, }, }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/key-transactions-list/`, body: teams.map(({id}) => ({ team: id, count: 0, keyed: [], })), }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-has-measurements/', body: {measurements: false}, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-performance/', body: [ { op: 'ui.long-task', group: 'c777169faad84eb4', description: 'Main UI thread blocked', frequency: 713, count: 9040, avgOccurrences: null, sumExclusiveTime: 1743893.9822921753, p50ExclusiveTime: null, p75ExclusiveTime: 244.9998779296875, p95ExclusiveTime: null, p99ExclusiveTime: null, }, ], }); MockApiClient.addMockResponse({ url: `/projects/org-slug/project-slug/profiling/functions/`, body: {functions: []}, }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/metrics-compatibility/`, body: { compatible_projects: [], incompatible_projecs: [], }, }); MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/metrics-compatibility-sums/`, body: { sum: { metrics: 100, metrics_null: 0, metrics_unparam: 0, }, }, }); jest.spyOn(MEPSetting, 'get').mockImplementation(() => MEPState.AUTO); }); afterEach(function () { MockApiClient.clearMockResponses(); ProjectsStore.reset(); jest.clearAllMocks(); }); describe('with events', function () { it('renders basic UI elements', async function () { const {organization, router, routerContext} = initializeData(); render( , { context: routerContext, organization, } ); // It shows the header await screen.findByText('Transaction Summary'); expect(screen.getByRole('heading', {name: '/performance'})).toBeInTheDocument(); // It shows a chart expect( screen.getByRole('button', {name: 'Display Duration Breakdown'}) ).toBeInTheDocument(); // It shows a searchbar expect(screen.getByLabelText('Search events')).toBeInTheDocument(); // It shows a table expect(screen.getByTestId('transactions-table')).toBeInTheDocument(); // Ensure open in discover button exists. expect(screen.getByTestId('transaction-events-open')).toBeInTheDocument(); // Ensure open issues button exists. expect(screen.getByRole('button', {name: 'Open in Issues'})).toBeInTheDocument(); // Ensure transaction filter button exists expect( screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'}) ).toBeInTheDocument(); // Ensure create alert from discover is hidden without metric alert expect( screen.queryByRole('button', {name: 'Create Alert'}) ).not.toBeInTheDocument(); // Ensure status breakdown exists expect(screen.getByText('Status Breakdown')).toBeInTheDocument(); }); it('renders feature flagged UI elements', function () { const {organization, router, routerContext} = initializeData({ features: ['incidents'], }); render( , { context: routerContext, organization, } ); // Ensure create alert from discover is shown with metric alerts expect(screen.getByRole('button', {name: 'Create Alert'})).toBeInTheDocument(); }); it('renders Web Vitals widget', async function () { const {organization, router, routerContext} = initializeData({ project: ProjectFixture({teams, platform: 'javascript'}), query: { query: 'transaction.duration:<15m transaction.op:pageload event.type:transaction transaction:/organizations/:orgId/issues/', }, }); render( , { context: routerContext, organization, } ); // It renders the web vitals widget await screen.findByRole('heading', {name: 'Web Vitals'}); const vitalStatues = screen.getAllByTestId('vital-status'); expect(vitalStatues).toHaveLength(3); expect(vitalStatues[0]).toHaveTextContent('31%'); expect(vitalStatues[1]).toHaveTextContent('65%'); expect(vitalStatues[2]).toHaveTextContent('3%'); }); it('renders sidebar widgets', async function () { const {organization, router, routerContext} = initializeData({}); render( , { context: routerContext, organization, } ); // Renders Apdex widget await screen.findByRole('heading', {name: 'Apdex'}); expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6'); // Renders Failure Rate widget expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument(); expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%'); // Renders TPM widget expect( screen.getByRole('heading', {name: 'Percentage of Total Transactions'}) ).toBeInTheDocument(); expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent( '100%' ); }); it('renders project picker modal when no url does not have project id', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', body: { meta: { fields: { project: 'string', 'count()': 'number', }, }, data: [ { 'count()': 2, project: 'proj-slug-1', }, { 'count()': 3, project: 'proj-slug-2', }, ], }, match: [ (_url, options) => { return options.query?.field?.includes('project'); }, ], }); const projects = [ ProjectFixture({ slug: 'proj-slug-1', id: '1', name: 'Project Name 1', }), ProjectFixture({ slug: 'proj-slug-2', id: '2', name: 'Project Name 2', }), ]; OrganizationStore.onUpdate(OrganizationFixture({slug: 'org-slug'}), { replace: true, }); const {organization, router, routerContext} = initializeData({projects}); const spy = jest.spyOn(router, 'replace'); // Ensure project id is not in path delete router.location.query.project; render( , {context: routerContext, organization} ); renderGlobalModal(); const firstProjectOption = await screen.findByText('proj-slug-1'); expect(firstProjectOption).toBeInTheDocument(); expect(screen.getByText('proj-slug-2')).toBeInTheDocument(); expect(screen.getByText('My Projects')).toBeInTheDocument(); await userEvent.click(firstProjectOption); expect(spy).toHaveBeenCalledWith( '/organizations/org-slug/performance/summary/?transaction=/performance&statsPeriod=14d&referrer=performance-transaction-summary&transactionCursor=1:0:0&project=1' ); }); it('fetches transaction threshold', function () { const {organization, router, routerContext} = initializeData(); const getTransactionThresholdMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/project-transaction-threshold-override/', method: 'GET', body: { threshold: '800', metric: 'lcp', }, }); const getProjectThresholdMock = MockApiClient.addMockResponse({ url: '/projects/org-slug/project-slug/transaction-threshold/configure/', method: 'GET', body: { threshold: '200', metric: 'duration', }, }); render( , { context: routerContext, organization, } ); expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1); expect(getProjectThresholdMock).not.toHaveBeenCalled(); }); it('fetches project transaction threshdold', async function () { const {organization, router, routerContext} = initializeData(); const getTransactionThresholdMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/project-transaction-threshold-override/', method: 'GET', statusCode: 404, }); const getProjectThresholdMock = MockApiClient.addMockResponse({ url: '/projects/org-slug/project-slug/transaction-threshold/configure/', method: 'GET', body: { threshold: '200', metric: 'duration', }, }); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1); expect(getProjectThresholdMock).toHaveBeenCalledTimes(1); }); it('triggers a navigation on search', async function () { const {organization, router, routerContext} = initializeData(); render( , { context: routerContext, organization, } ); // Fill out the search box, and submit it. await userEvent.type( screen.getByLabelText('Search events'), 'user.email:uhoh*{enter}' ); // Check the navigation. expect(browserHistory.push).toHaveBeenCalledTimes(1); expect(browserHistory.push).toHaveBeenCalledWith({ pathname: undefined, query: { transaction: '/performance', project: '2', statsPeriod: '14d', query: 'user.email:uhoh*', transactionCursor: '1:0:0', }, }); }); it('can mark a transaction as key', async function () { const {organization, router, routerContext} = initializeData(); render( , { context: routerContext, organization, } ); const mockUpdate = MockApiClient.addMockResponse({ url: `/organizations/org-slug/key-transactions/`, method: 'POST', body: {}, }); await screen.findByRole('button', {name: 'Star for Team'}); // Click the key transaction button await userEvent.click(screen.getByRole('button', {name: 'Star for Team'})); await userEvent.click(screen.getByRole('option', {name: '#team1'})); // Ensure request was made. expect(mockUpdate).toHaveBeenCalled(); }); it('triggers a navigation on transaction filter', async function () { const {organization, router, routerContext} = initializeData(); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); await waitFor(() => { expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument(); }); // Open the transaction filter dropdown await userEvent.click( screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'}) ); await userEvent.click(screen.getAllByText('Slow Transactions (p95)')[1]); // Check the navigation. expect(browserHistory.push).toHaveBeenCalledWith({ pathname: undefined, query: { transaction: '/performance', project: '2', showTransactions: 'slow', transactionCursor: undefined, }, }); }); it('renders pagination buttons', async function () { const {organization, router, routerContext} = initializeData(); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); expect(await screen.findByLabelText('Previous')).toBeInTheDocument(); // Click the 'next' button await userEvent.click(screen.getByLabelText('Next')); // Check the navigation. expect(browserHistory.push).toHaveBeenCalledWith({ pathname: undefined, query: { transaction: '/performance', project: '2', transactionCursor: '2:0:0', }, }); }); it('forwards conditions to related issues', async function () { const issueGet = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=trends&statsPeriod=14d', body: [], }); const {organization, router, routerContext} = initializeData({ query: {query: 'tag:value'}, }); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); expect(issueGet).toHaveBeenCalled(); }); it('does not forward event type to related issues', async function () { const issueGet = MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=trends&statsPeriod=14d', body: [], match: [ (_, options) => { // event.type must NOT be in the query params return !options.query?.query?.includes('event.type'); }, ], }); const {organization, router, routerContext} = initializeData({ query: {query: 'tag:value event.type:transaction'}, }); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); expect(issueGet).toHaveBeenCalled(); }); it('renders the suspect spans table if the feature is enabled', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-performance/', body: [], }); const {organization, router, routerContext} = initializeData(); render( , { context: routerContext, organization, } ); expect(await screen.findByText('Suspect Spans')).toBeInTheDocument(); }); it('adds search condition on transaction status when clicking on status breakdown', async function () { const {organization, router, routerContext} = initializeData(); render( , { context: routerContext, organization, } ); await screen.findByTestId('status-ok'); await userEvent.click(screen.getByTestId('status-ok')); expect(browserHistory.push).toHaveBeenCalledTimes(1); expect(browserHistory.push).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({ query: expect.stringContaining('transaction.status:ok'), }), }) ); }); it('appends tag value to existing query when clicked', async function () { const {organization, router, routerContext} = initializeData(); render( , { context: routerContext, organization, } ); await screen.findByText('Tag Summary'); await userEvent.click( await screen.findByLabelText( 'environment, dev, 100% of all events. View events with this tag value.' ) ); await userEvent.click( await screen.findByLabelText( 'foo, bar, 100% of all events. View events with this tag value.' ) ); expect(router.push).toHaveBeenCalledTimes(2); expect(router.push).toHaveBeenNthCalledWith(1, { query: { project: '2', query: 'tags[environment]:dev', transaction: '/performance', transactionCursor: '1:0:0', }, }); expect(router.push).toHaveBeenNthCalledWith(2, { query: { project: '2', query: 'foo:bar', transaction: '/performance', transactionCursor: '1:0:0', }, }); }); it('does not use MEP dataset for stats query without features', async function () { const {organization, router, routerContext} = initializeData({ query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset features: [''], // No 'dynamic-sampling' feature to indicate it can use metrics dataset or metrics enhanced. }); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); await screen.findByRole('heading', {name: 'Apdex'}); expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6'); expect(eventStatsMock).toHaveBeenNthCalledWith( 1, expect.anything(), expect.objectContaining({ query: expect.objectContaining({ environment: [], interval: '30m', partial: '1', project: [2], query: 'transaction.op:pageload event.type:transaction transaction:/performance', referrer: 'api.performance.transaction-summary.duration-chart', statsPeriod: '14d', yAxis: [ 'p50(transaction.duration)', 'p75(transaction.duration)', 'p95(transaction.duration)', 'p99(transaction.duration)', 'p100(transaction.duration)', 'avg(transaction.duration)', ], }), }) ); }); it('uses MEP dataset for stats query', async function () { const {organization, router, routerContext} = initializeData({ query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset features: ['dynamic-sampling', 'mep-rollout-flag'], }); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); // Renders Apdex widget await screen.findByRole('heading', {name: 'Apdex'}); expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.5'); expect(eventStatsMock).toHaveBeenNthCalledWith( 1, expect.anything(), expect.objectContaining({ query: expect.objectContaining({ query: 'transaction.op:pageload event.type:transaction transaction:/performance', dataset: 'metricsEnhanced', }), }) ); // Renders Failure Rate widget expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument(); expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%'); // Renders TPM widget expect( screen.getByRole('heading', {name: 'Percentage of Total Transactions'}) ).toBeInTheDocument(); expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent( '100%' ); expect( screen.queryByTestId('search-metrics-fallback-warning') ).not.toBeInTheDocument(); }); it('does not use MEP dataset for stats query if cardinality fallback fails', async function () { MockApiClient.addMockResponse({ method: 'GET', url: `/organizations/org-slug/metrics-compatibility-sums/`, body: { sum: { metrics: 100, metrics_null: 100, metrics_unparam: 0, }, }, }); const {organization, router, routerContext} = initializeData({ query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset features: ['dynamic-sampling', 'mep-rollout-flag'], }); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); // Renders Apdex widget await screen.findByRole('heading', {name: 'Apdex'}); expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6'); expect(eventStatsMock).toHaveBeenNthCalledWith( 1, expect.anything(), expect.objectContaining({ query: expect.objectContaining({ query: 'transaction.op:pageload event.type:transaction transaction:/performance', }), }) ); // Renders TPM widget expect( screen.getByRole('heading', {name: 'Percentage of Total Transactions'}) ).toBeInTheDocument(); expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent( '100%' ); }); it('uses MEP dataset for stats query and shows fallback warning', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/issues/?limit=5&project=2&query=has%3Anot-compatible%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=trends&statsPeriod=14d', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', body: { meta: { fields: { 'count()': 'number', 'apdex()': 'number', 'count_miserable_user()': 'number', 'user_misery()': 'number', 'count_unique_user()': 'number', 'p95()': 'number', 'failure_rate()': 'number', 'tpm()': 'number', project_threshold_config: 'string', }, isMetricsData: false, // The total response is setting the metrics fallback behaviour. }, data: [ { 'count()': 200, 'apdex()': 0.5, 'count_miserable_user()': 120, 'user_misery()': 0.1, 'count_unique_user()': 100, 'p95()': 731.3132, 'failure_rate()': 1, 'tpm()': 100, project_threshold_config: ['duration', 300], }, ], }, match: [ (_url, options) => { const isMetricsEnhanced = options.query?.dataset === DiscoverDatasets.METRICS_ENHANCED; return ( options.query?.field?.includes('p95()') && isMetricsEnhanced && options.query?.query?.includes('not-compatible') ); }, ], }); const {organization, router, routerContext} = initializeData({ query: {query: 'transaction.op:pageload has:not-compatible'}, // Adds incompatible w/ metrics tag features: ['dynamic-sampling', 'mep-rollout-flag'], }); render( , { context: routerContext, organization, } ); await screen.findByText('Transaction Summary'); // Renders Apdex widget await screen.findByRole('heading', {name: 'Apdex'}); expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.5'); expect(eventStatsMock).toHaveBeenNthCalledWith( 1, expect.anything(), expect.objectContaining({ query: expect.objectContaining({ query: 'transaction.op:pageload has:not-compatible event.type:transaction transaction:/performance', dataset: 'metricsEnhanced', }), }) ); // Renders Failure Rate widget expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument(); expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%'); // Renders TPM widget expect( screen.getByRole('heading', {name: 'Percentage of Total Transactions'}) ).toBeInTheDocument(); expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent( '100%' ); expect(screen.getByTestId('search-metrics-fallback-warning')).toBeInTheDocument(); }); }); });