import { generateSuspectSpansResponse, initializeData as _initializeData, } from 'sentry-test/performance/initializePerformanceData'; import { act, render, screen, userEvent, waitFor, within, } from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; import {browserHistory} from 'sentry/utils/browserHistory'; import SpanDetails from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails'; import {spanDetailsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils'; function initializeData(settings) { const data = _initializeData(settings); act(() => void ProjectsStore.loadInitialData(data.projects)); return data; } describe('Performance > Transaction Spans > Span Summary', function () { beforeEach(function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/projects/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events/', body: {data: [{'count()': 1}]}, }); }); afterEach(function () { MockApiClient.clearMockResponses(); ProjectsStore.reset(); // need to typecast to any to be able to call mockReset (browserHistory.push as any).mockReset(); }); describe('Without Span Data', function () { beforeEach(function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-performance/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans/', body: [], }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-stats/', body: { 'percentileArray(spans_exclusive_time, 0.50)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.75)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.95)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.99)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, }, }); }); it('renders empty when missing project param', function () { const data = initializeData({query: {transaction: 'transaction'}}); const {container} = render( , {organization: data.organization} ); expect(container).toBeEmptyDOMElement(); }); it('renders empty when missing transaction param', function () { const data = initializeData({query: {project: '1'}}); const {container} = render( , {organization: data.organization} ); expect(container).toBeEmptyDOMElement(); }); it('renders no data when empty response', async function () { const data = initializeData({ features: ['performance-view'], query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); expect( await screen.findByText('No results found for your query') ).toBeInTheDocument(); expect(await screen.findByText('Self Time Breakdown')).toBeInTheDocument(); }); }); describe('With Bad Span Data', function () { it('filters examples missing spans', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-performance/', body: generateSuspectSpansResponse(), }); // just want to get one span in the response const badExamples = [generateSuspectSpansResponse({examplesOnly: true})[0]]; for (const example of badExamples[0].examples) { // make sure that the spans array is empty example.spans = []; } MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans/', body: badExamples, }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-stats/', body: { 'percentileArray(spans_exclusive_time, 0.50)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.75)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.95)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.99)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, }, }); const data = initializeData({ features: ['performance-view'], query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); expect(await screen.findByText('Event ID')).toBeInTheDocument(); expect(await screen.findByText('Timestamp')).toBeInTheDocument(); expect(await screen.findByText('Span Duration')).toBeInTheDocument(); expect(await screen.findByText('Count')).toBeInTheDocument(); expect(await screen.findByText('Cumulative Duration')).toBeInTheDocument(); }); }); describe('With Span Data', function () { beforeEach(function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-performance/', body: generateSuspectSpansResponse(), }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans/', body: generateSuspectSpansResponse({examplesOnly: true}), }); MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-stats/', body: { 'percentileArray(spans_exclusive_time, 0.50)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.75)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.95)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, 'percentileArray(spans_exclusive_time, 0.99)': { data: [ [0, [{count: 0}]], [10, [{count: 0}]], ], order: 2, start: 0, end: 10, }, }, }); }); it('renders header elements', async function () { const data = initializeData({ features: ['performance-view'], query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); expect(await screen.findByText('Span Summary')).toBeInTheDocument(); const operationNameHeader = await screen.findByTestId('header-operation-name'); expect( await within(operationNameHeader).findByText('Span Operation') ).toBeInTheDocument(); // TODO: add an expect for the span description here expect( await within(operationNameHeader).findByTestId('operation-name') ).toHaveTextContent('op'); const percentilesHeader = await screen.findByTestId('header-percentiles'); expect( await within(percentilesHeader).findByText('Self Time Percentiles') ).toBeInTheDocument(); const p50Section = await within(percentilesHeader).findByTestId('section-p50'); expect(await within(p50Section).findByText('1.00ms')).toBeInTheDocument(); expect(await within(p50Section).findByText('p50')).toBeInTheDocument(); const p75Section = await within(percentilesHeader).findByTestId('section-p75'); expect(await within(p75Section).findByText('2.00ms')).toBeInTheDocument(); expect(await within(p75Section).findByText('p75')).toBeInTheDocument(); const p95Section = await within(percentilesHeader).findByTestId('section-p95'); expect(await within(p95Section).findByText('3.00ms')).toBeInTheDocument(); expect(await within(p95Section).findByText('p95')).toBeInTheDocument(); const p99Section = await within(percentilesHeader).findByTestId('section-p99'); expect(await within(p99Section).findByText('4.00ms')).toBeInTheDocument(); expect(await within(p99Section).findByText('p99')).toBeInTheDocument(); const frequencyHeader = await screen.findByTestId('header-frequency'); expect(await within(frequencyHeader).findByText('100%')).toBeInTheDocument(); expect( await within(frequencyHeader).findByText((_content, element) => Boolean( element && element.tagName === 'DIV' && element.textContent === '1.00 times per event' ) ) ).toBeInTheDocument(); const totalExclusiveTimeHeader = await screen.findByTestId( 'header-total-exclusive-time' ); expect( await within(totalExclusiveTimeHeader).findByText('5.00ms') ).toBeInTheDocument(); expect( await within(totalExclusiveTimeHeader).findByText((_content, element) => Boolean( element && element.tagName === 'DIV' && element.textContent === '1 events' ) ) ).toBeInTheDocument(); }); it('renders timeseries chart', async function () { const data = initializeData({ features: ['performance-view'], query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); expect(await screen.findByText('Self Time Breakdown')).toBeInTheDocument(); }); it('renders table headers', async function () { const data = initializeData({ features: ['performance-view'], query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); expect(await screen.findByText('Event ID')).toBeInTheDocument(); expect(await screen.findByText('Timestamp')).toBeInTheDocument(); expect(await screen.findByText('Span Duration')).toBeInTheDocument(); expect(await screen.findByText('Count')).toBeInTheDocument(); expect(await screen.findByText('Cumulative Duration')).toBeInTheDocument(); }); describe('With histogram view feature flag enabled', function () { const FEATURES = ['performance-view', 'performance-span-histogram-view']; beforeEach(function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/recent-searches/', method: 'GET', body: [], }); }); it('renders a search bar', async function () { const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); const searchBarNode = await screen.findByPlaceholderText('Filter Transactions'); expect(searchBarNode).toBeInTheDocument(); }); it('disables reset button when no min or max query parameters were set', async function () { const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); const resetButton = await screen.findByRole('button', { name: /reset view/i, }); expect(resetButton).toBeInTheDocument(); expect(resetButton).toBeDisabled(); }); it('enables reset button when min and max are set', async function () { const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction', min: '10', max: '100'}, }); render(, { router: data.router, organization: data.organization, }); const resetButton = await screen.findByRole('button', { name: /reset view/i, }); expect(resetButton).toBeEnabled(); }); it('clears min and max query parameters when reset button is clicked', async function () { const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction', min: '10', max: '100'}, }); render(, { router: data.router, organization: data.organization, }); const resetButton = await screen.findByRole('button', { name: /reset view/i, }); await userEvent.click(resetButton); expect(browserHistory.push).toHaveBeenCalledWith( expect.not.objectContaining({min: expect.any(String), max: expect.any(String)}) ); }); it('does not add aggregate filters to the query', async function () { const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); const searchBarNode = await screen.findByPlaceholderText('Filter Transactions'); await userEvent.click(searchBarNode); await userEvent.paste('count():>3'); expect(searchBarNode).toHaveTextContent('count():>3'); expect(browserHistory.push).not.toHaveBeenCalled(); }); it('renders a display toggle that changes a chart view between timeseries and histogram by pushing it to the browser history', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-histogram/', body: [ {bin: 0, count: 0}, {bin: 10, count: 2}, {bin: 20, count: 4}, ], }); const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction'}, }); render(, { router: data.router, organization: data.organization, }); expect(await screen.findByTestId('total-value')).toBeInTheDocument(); const chartTitleNodes = await screen.findAllByText('Self Time Breakdown'); expect(chartTitleNodes[0]).toBeInTheDocument(); const displayToggle = await screen.findByTestId('display-toggle'); expect(displayToggle).toBeInTheDocument(); expect(await within(displayToggle).findByRole('button')).toHaveTextContent( 'Self Time Breakdown' ); await userEvent.click(await within(displayToggle).findByRole('button')); await userEvent.click( await within(displayToggle).findByRole('option', { name: 'Self Time Distribution', }) ); expect(browserHistory.push).toHaveBeenCalledWith( expect.objectContaining({ query: { display: 'histogram', project: '1', transaction: 'transaction', }, }) ); }); it('renders a histogram when display is set to histogram', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-histogram/', body: [ {bin: 0, count: 0}, {bin: 10, count: 2}, {bin: 20, count: 4}, ], }); const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction', display: 'histogram'}, }); render(, { router: data.router, organization: data.organization, }); const displayToggle = await screen.findByTestId('display-toggle'); expect(await within(displayToggle).findByRole('button')).toHaveTextContent( 'Self Time Distribution' ); const nodes = await screen.findAllByText('Self Time Distribution'); expect(nodes[0]).toBeInTheDocument(); }); it('gracefully handles error response', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-histogram/', statusCode: 400, }); const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction', display: 'histogram'}, }); render(, { router: data.router, organization: data.organization, }); expect(await screen.findByTestId('histogram-error-panel')).toBeInTheDocument(); }); it('gracefully renders empty histogram when empty buckets are received', async function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-histogram/', body: [ {bin: 0, count: 0}, {bin: 10, count: 0}, {bin: 20, count: 0}, ], }); const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction', display: 'histogram'}, }); render(, { router: data.router, organization: data.organization, }); const nodes = await screen.findAllByText('Self Time Distribution'); expect(nodes[0]).toBeInTheDocument(); }); it('sends min and max to span example query', async function () { const mock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans/', body: {}, }); const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction', min: '10', max: '120'}, }); render(, { router: data.router, organization: data.organization, }); await waitFor(() => { expect(mock).toHaveBeenLastCalledWith( '/organizations/org-slug/events-spans/', expect.objectContaining({ query: expect.objectContaining({ min_exclusive_time: '10', max_exclusive_time: '120', }), }) ); }); }); it('sends min and max to suspect spans query', async function () { const mock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/events-spans-performance/', body: {}, }); const data = initializeData({ features: FEATURES, query: {project: '1', transaction: 'transaction', min: '10', max: '120'}, }); render(, { router: data.router, organization: data.organization, }); await waitFor(() => { expect(mock).toHaveBeenLastCalledWith( '/organizations/org-slug/events-spans-performance/', expect.objectContaining({ query: expect.objectContaining({ min_exclusive_time: '10', max_exclusive_time: '120', }), }) ); }); }); }); }); }); describe('spanDetailsRouteWithQuery', function () { it('should encode slashes in span op', function () { const target = spanDetailsRouteWithQuery({ orgSlug: 'org-slug', transaction: 'transaction', query: {}, spanSlug: {op: 'o/p', group: 'aaaaaaaaaaaaaaaa'}, projectID: '1', }); expect(target).toEqual( expect.objectContaining({ pathname: '/organizations/org-slug/performance/summary/spans/o%2Fp:aaaaaaaaaaaaaaaa/', }) ); }); });