import {RouterFixture} from 'sentry-fixture/routerFixture'; import {initializeOrg} from 'sentry-test/initializeOrg'; import { render, screen, userEvent, waitFor, within, } from 'sentry-test/reactTestingLibrary'; import PageFiltersStore from 'sentry/stores/pageFiltersStore'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {PageParamsProvider} from 'sentry/views/explore/contexts/pageParamsContext'; import {SpanTagsProvider} from 'sentry/views/explore/contexts/spanTagsContext'; import {MultiQueryModeContent} from 'sentry/views/explore/multiQueryMode/content'; import {useReadQueriesFromLocation} from 'sentry/views/explore/multiQueryMode/locationUtils'; jest.mock('sentry/components/lazyRender', () => ({ LazyRender: ({children}: {children: React.ReactNode}) => children, })); describe('MultiQueryModeContent', function () { const {organization, project} = initializeOrg({ organization: { features: ['visibility-explore-rpc'], }, }); let eventsRequest: any; let eventsStatsRequest: any; beforeEach(function () { // without this the `CompactSelect` component errors with a bunch of async updates jest.spyOn(console, 'error').mockImplementation(); MockApiClient.clearMockResponses(); PageFiltersStore.init(); PageFiltersStore.onInitializeUrlState( { projects: [project].map(p => parseInt(p.id, 10)), environments: [], datetime: { period: '7d', start: null, end: null, utc: null, }, }, new Set() ); MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/spans/fields/`, method: 'GET', body: [{key: 'span.op', name: 'span.op'}], }); eventsRequest = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, method: 'GET', body: {}, }); eventsStatsRequest = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events-stats/`, method: 'GET', body: {}, }); }); it('updates visualization and outdated sorts', async function () { let queries: any; function Component() { queries = useReadQueriesFromLocation(); return ; } render( , {disableRouterMocks: true} ); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); const section = screen.getByTestId('section-visualize-0'); await userEvent.click(within(section).getByRole('button', {name: 'span.duration'})); await userEvent.click(within(section).getByRole('option', {name: 'span.self_time'})); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.self_time)'], sortBys: [ { field: 'id', kind: 'desc', }, ], fields: ['id', 'span.self_time'], groupBys: [], query: '', }, ]); }); it('updates sorts', async function () { let queries: any; function Component() { queries = useReadQueriesFromLocation(); return ; } render( , {disableRouterMocks: true} ); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); const section = screen.getByTestId('section-sort-by-0'); await userEvent.click(within(section).getByRole('button', {name: 'span.duration'})); await userEvent.click(within(section).getByRole('option', {name: 'id'})); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'id', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); }); it('updates group bys and outdated sorts', async function () { let queries: any; function Component() { queries = useReadQueriesFromLocation(); return ; } render( , {disableRouterMocks: true} ); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); const section = screen.getByTestId('section-group-by-0'); await userEvent.click(within(section).getByRole('button', {name: 'None'})); await userEvent.click(within(section).getByRole('option', {name: 'span.op'})); expect(queries).toEqual([ { yAxes: ['avg(span.duration)'], chartType: 1, sortBys: [ { field: 'avg(span.duration)', kind: 'desc', }, ], query: '', groupBys: ['span.op'], fields: ['id', 'span.duration'], }, ]); }); it('updates query at the correct index', async function () { let queries: any; function Component() { queries = useReadQueriesFromLocation(); return ; } render( , {disableRouterMocks: true} ); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); // Add chart await userEvent.click(screen.getByRole('button', {name: 'Add Query'})); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); const section = screen.getByTestId('section-visualize-0'); await userEvent.click(within(section).getByRole('button', {name: 'span.duration'})); await userEvent.click(within(section).getByRole('option', {name: 'span.self_time'})); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.self_time)'], sortBys: [ { field: 'id', kind: 'desc', }, ], fields: ['id', 'span.self_time'], groupBys: [], query: '', }, { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); await userEvent.click(screen.getAllByLabelText('Delete Query')[0]!); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); }); it('calls events and stats APIs', async function () { let queries: any; function Component() { queries = useReadQueriesFromLocation(); return ; } render( , {disableRouterMocks: true} ); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); const section = screen.getByTestId('section-group-by-0'); await userEvent.click(within(section).getByRole('button', {name: 'None'})); await userEvent.click(within(section).getByRole('option', {name: 'span.op'})); await waitFor(() => expect(eventsStatsRequest).toHaveBeenNthCalledWith( 1, `/organizations/${organization.slug}/events-stats/`, expect.objectContaining({ query: expect.objectContaining({ dataset: 'spans', field: [], interval: '1h', orderby: undefined, project: ['2'], query: '!transaction.span_id:00', referrer: 'api.explorer.stats', statsPeriod: '7d', topEvents: undefined, useRpc: '1', yAxis: 'avg(span.duration)', }), }) ) ); await waitFor(() => expect(eventsRequest).toHaveBeenNthCalledWith( 1, `/organizations/${organization.slug}/events/`, expect.objectContaining({ query: expect.objectContaining({ dataset: 'spans', environment: [], field: [ 'id', 'span.duration', 'transaction.span_id', 'trace', 'project', 'timestamp', ], per_page: 10, project: ['2'], query: '!transaction.span_id:00', referrer: 'api.explore.multi-query-spans-table', sort: '-span.duration', statsPeriod: '7d', useRpc: '1', }), }) ) ); // group by requests await waitFor(() => expect(eventsStatsRequest).toHaveBeenNthCalledWith( 2, `/organizations/${organization.slug}/events-stats/`, expect.objectContaining({ query: expect.objectContaining({ dataset: 'spans', excludeOther: 0, field: ['span.op', 'avg(span.duration)'], interval: '1h', orderby: '-avg_span_duration', project: ['2'], query: '!transaction.span_id:00', referrer: 'api.explorer.stats', sort: '-avg_span_duration', statsPeriod: '7d', topEvents: '5', useRpc: '1', yAxis: 'avg(span.duration)', }), }) ) ); await waitFor(() => expect(eventsRequest).toHaveBeenNthCalledWith( 2, `/organizations/${organization.slug}/events/`, expect.objectContaining({ query: expect.objectContaining({ dataset: 'spans', environment: [], field: ['span.op', 'avg(span.duration)'], per_page: 10, project: ['2'], query: '!transaction.span_id:00', referrer: 'api.explore.multi-query-spans-table', sort: '-avg_span_duration', statsPeriod: '7d', useRpc: '1', }), }) ) ); }); it('unstacking group by puts you in sample mode', async function () { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, method: 'GET', body: { data: [ { 'span.op': 'POST', 'avg(span.duration)': 147.02002059925093, }, { 'span.op': 'GET', 'avg(span.duration)': 1.9993342331511974, }, ], }, match: [ function (_url: string, options: Record) { return options.query.field.includes('span.op'); }, ], }); let queries: any; function Component() { queries = useReadQueriesFromLocation(); return ; } render( , {disableRouterMocks: true} ); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'span.duration', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: '', }, ]); const section = screen.getByTestId('section-group-by-0'); await userEvent.click(within(section).getByRole('button', {name: 'None'})); await userEvent.click(within(section).getByRole('option', {name: 'span.op'})); await userEvent.click(screen.getAllByTestId('unstack-link')[0]!); expect(queries).toEqual([ { chartType: 1, yAxes: ['avg(span.duration)'], sortBys: [ { field: 'id', kind: 'desc', }, ], fields: ['id', 'span.duration'], groupBys: [], query: 'span.op:POST', }, ]); }); it('sets interval correctly', async function () { const router = RouterFixture({ location: { pathname: '/traces/compare', query: { queries: [ '{"groupBys":[],"query":"","sortBys":["-timestamp"],"yAxes":["avg(span.duration)"]}', ], }, }, }); function Component() { return ; } render( , {router, organization} ); const section = screen.getByTestId('section-visualization-0'); expect( await within(section).findByRole('button', {name: '1 hour'}) ).toBeInTheDocument(); await userEvent.click(within(section).getByRole('button', {name: '1 hour'})); await userEvent.click(within(section).getByRole('option', {name: '30 minutes'})); expect(router.push).toHaveBeenCalledWith({ pathname: '/traces/compare', query: expect.objectContaining({ interval: '30m', queries: [ '{"groupBys":[],"query":"","sortBys":["-timestamp"],"yAxes":["avg(span.duration)"]}', ], }), }); }); });