import {
MetricsFieldFixture,
MetricsSessionUserCountByStatusByReleaseFixture,
} from 'sentry-fixture/metrics';
import {SessionsFieldFixture} from 'sentry-fixture/sessions';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
import {resetMockDate, setMockDate} from 'sentry-test/utils';
import {
DashboardFilterKeys,
DisplayType,
WidgetType,
} from 'sentry/views/dashboards/types';
import ReleaseWidgetQueries from 'sentry/views/dashboards/widgetCard/releaseWidgetQueries';
describe('Dashboards > ReleaseWidgetQueries', function () {
const {organization} = initializeOrg();
const badMessage = 'Bad request data';
const multipleQueryWidget = {
title: 'Sessions vs. Users',
interval: '5m',
displayType: DisplayType.LINE,
queries: [
{
conditions: '',
fields: [`sum(session)`],
aggregates: [`sum(session)`],
columns: [],
name: 'sessions',
orderby: '',
},
{
conditions: 'environment:prod',
fields: [`sum(session)`],
aggregates: [`sum(session)`],
columns: [],
name: 'users',
orderby: '',
},
],
widgetType: WidgetType.RELEASE,
};
const singleQueryWidget = {
title: 'Sessions',
interval: '5m',
displayType: DisplayType.LINE,
queries: [
{
conditions: '',
fields: [`count_unique(user)`],
aggregates: [`count_unique(user)`],
columns: [],
name: 'sessions',
orderby: '-count_unique(user)',
},
],
widgetType: WidgetType.RELEASE,
};
const selection = {
projects: [1],
environments: ['prod'],
datetime: {
period: '14d',
start: null,
end: null,
utc: null,
},
};
const api = new MockApiClient();
beforeEach(function () {
setMockDate(new Date('2022-08-02'));
});
afterEach(function () {
MockApiClient.clearMockResponses();
resetMockDate();
});
it('can send chart requests', async function () {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: MetricsFieldFixture(`session.all`),
});
const children = jest.fn(() =>
);
render(
{children}
);
expect(mock).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(children).toHaveBeenLastCalledWith(
expect.objectContaining({
tableResults: undefined,
timeseriesResults: [
{
data: expect.arrayContaining([
{name: '2021-12-01T16:15:00Z', value: 443.6200417187068},
{name: '2021-12-01T16:30:00Z', value: 471.7512262596214},
{name: '2021-12-01T16:45:00Z', value: 632.5356294251225},
{name: '2021-12-01T17:00:00Z', value: 538.6063865509535},
]),
seriesName: 'sessions > sum(session)',
},
],
})
)
);
});
it('fetches release data when sorting on release for metrics api', async function () {
const mockRelease = MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/',
body: [
{
version: 'be1ddfb18126dd2cbde26bfe75488503280e716e',
},
],
});
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: MetricsSessionUserCountByStatusByReleaseFixture(),
});
const children = jest.fn(() => );
const queries = [
{
conditions: '',
fields: [`count_unique(user)`],
aggregates: [`count_unique(user)`],
columns: ['release'],
name: 'sessions',
orderby: '-release',
},
];
render(
{children}
);
await waitFor(() => expect(mockRelease).toHaveBeenCalledTimes(1));
expect(mockRelease).toHaveBeenCalledWith(
'/organizations/org-slug/releases/',
expect.objectContaining({
data: {
environment: ['prod'],
per_page: 50,
project: [1],
sort: 'date',
},
})
);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(
'/organizations/org-slug/metrics/data/',
expect.objectContaining({
query: {
environment: ['prod'],
field: ['count_unique(sentry.sessions.user)'],
groupBy: ['release'],
includeSeries: 1,
includeTotals: 1,
interval: '12h',
per_page: 100,
project: [1],
query: ' release:be1ddfb18126dd2cbde26bfe75488503280e716e',
statsPeriod: '14d',
},
})
);
});
it('calls session api when session.status is a group by', async function () {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/sessions/',
body: MetricsFieldFixture(`count_unique(user)`),
});
const children = jest.fn(() => );
const queries = [
{
conditions: '',
fields: [`count_unique(user)`],
aggregates: [`count_unique(user)`],
columns: ['session.status'],
name: 'sessions',
orderby: '-count_unique(user)',
},
];
render(
{children}
);
await waitFor(() => {
expect(mock).toHaveBeenCalledWith(
'/organizations/org-slug/sessions/',
expect.objectContaining({
query: expect.objectContaining({
environment: ['prod'],
field: ['count_unique(user)'],
groupBy: ['session.status'],
interval: '30m',
project: [1],
statsPeriod: '14d',
}),
})
);
});
});
it('appends dashboard filters to releases request', async function () {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: MetricsSessionUserCountByStatusByReleaseFixture(),
});
render(
{() => }
);
await screen.findByTestId('child');
expect(mock).toHaveBeenCalledWith(
'/organizations/org-slug/metrics/data/',
expect.objectContaining({
query: expect.objectContaining({
query: ' release:"abc@1.3.0" ',
}),
})
);
});
it('strips injected sort columns', async function () {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: MetricsSessionUserCountByStatusByReleaseFixture(),
});
const children = jest.fn(() => );
const injectedOrderby = {
title: 'Sessions',
interval: '5m',
displayType: DisplayType.LINE,
queries: [
{
conditions: '',
fields: [`sum(session)`],
aggregates: [`sum(session)`],
columns: [],
name: 'sessions',
orderby: '-count_unique(user)',
},
],
widgetType: WidgetType.RELEASE,
};
render(
{children}
);
expect(mock).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(children).toHaveBeenLastCalledWith(
expect.objectContaining({
errorMessage: undefined,
loading: false,
timeseriesResults: [
{
data: [
{name: '2022-01-15T00:00:00Z', value: 0},
{name: '2022-01-16T00:00:00Z', value: 0},
{name: '2022-01-17T00:00:00Z', value: 0},
{name: '2022-01-18T00:00:00Z', value: 0},
{name: '2022-01-19T00:00:00Z', value: 0},
{name: '2022-01-20T00:00:00Z', value: 0},
{name: '2022-01-21T00:00:00Z', value: 0},
{name: '2022-01-22T00:00:00Z', value: 0},
{name: '2022-01-23T00:00:00Z', value: 0},
{name: '2022-01-24T00:00:00Z', value: 23},
{name: '2022-01-25T00:00:00Z', value: 11},
{name: '2022-01-26T00:00:00Z', value: 0},
{name: '2022-01-27T00:00:00Z', value: 0},
{name: '2022-01-28T00:00:00Z', value: 0},
],
seriesName: 'sessions > crashed, 1 : sum(session)',
},
{
data: [
{name: '2022-01-15T00:00:00Z', value: 1},
{name: '2022-01-16T00:00:00Z', value: 0},
{name: '2022-01-17T00:00:00Z', value: 0},
{name: '2022-01-18T00:00:00Z', value: 0},
{name: '2022-01-19T00:00:00Z', value: 0},
{name: '2022-01-20T00:00:00Z', value: 0},
{name: '2022-01-21T00:00:00Z', value: 0},
{name: '2022-01-22T00:00:00Z', value: 0},
{name: '2022-01-23T00:00:00Z', value: 0},
{name: '2022-01-24T00:00:00Z', value: 0},
{name: '2022-01-25T00:00:00Z', value: 0},
{name: '2022-01-26T00:00:00Z', value: 0},
{name: '2022-01-27T00:00:00Z', value: 0},
{name: '2022-01-28T00:00:00Z', value: 0},
],
seriesName: 'sessions > abnormal, 1 : sum(session)',
},
{
data: [
{name: '2022-01-15T00:00:00Z', value: 0},
{name: '2022-01-16T00:00:00Z', value: 0},
{name: '2022-01-17T00:00:00Z', value: 0},
{name: '2022-01-18T00:00:00Z', value: 0},
{name: '2022-01-19T00:00:00Z', value: 0},
{name: '2022-01-20T00:00:00Z', value: 37},
{name: '2022-01-21T00:00:00Z', value: 0},
{name: '2022-01-22T00:00:00Z', value: 0},
{name: '2022-01-23T00:00:00Z', value: 0},
{name: '2022-01-24T00:00:00Z', value: 335},
{name: '2022-01-25T00:00:00Z', value: 79},
{name: '2022-01-26T00:00:00Z', value: 0},
{name: '2022-01-27T00:00:00Z', value: 0},
{name: '2022-01-28T00:00:00Z', value: 0},
],
seriesName: 'sessions > errored, 1 : sum(session)',
},
{
data: [
{name: '2022-01-15T00:00:00Z', value: 0},
{name: '2022-01-16T00:00:00Z', value: 0},
{name: '2022-01-17T00:00:00Z', value: 0},
{name: '2022-01-18T00:00:00Z', value: 0},
{name: '2022-01-19T00:00:00Z', value: 0},
{name: '2022-01-20T00:00:00Z', value: 2503},
{name: '2022-01-21T00:00:00Z', value: 661},
{name: '2022-01-22T00:00:00Z', value: 0},
{name: '2022-01-23T00:00:00Z', value: 0},
{name: '2022-01-24T00:00:00Z', value: 1464},
{name: '2022-01-25T00:00:00Z', value: 430},
{name: '2022-01-26T00:00:00Z', value: 0},
{name: '2022-01-27T00:00:00Z', value: 0},
{name: '2022-01-28T00:00:00Z', value: 0},
],
seriesName: 'sessions > healthy, 1 : sum(session)',
},
{
data: [
{name: '2022-01-15T00:00:00Z', value: 1},
{name: '2022-01-16T00:00:00Z', value: 0},
{name: '2022-01-17T00:00:00Z', value: 0},
{name: '2022-01-18T00:00:00Z', value: 0},
{name: '2022-01-19T00:00:00Z', value: 0},
{name: '2022-01-20T00:00:00Z', value: 0},
{name: '2022-01-21T00:00:00Z', value: 0},
{name: '2022-01-22T00:00:00Z', value: 0},
{name: '2022-01-23T00:00:00Z', value: 0},
{name: '2022-01-24T00:00:00Z', value: 23},
{name: '2022-01-25T00:00:00Z', value: 11},
{name: '2022-01-26T00:00:00Z', value: 0},
{name: '2022-01-27T00:00:00Z', value: 0},
{name: '2022-01-28T00:00:00Z', value: 0},
],
seriesName: 'sessions > crashed, 2 : sum(session)',
},
{
data: [
{name: '2022-01-15T00:00:00Z', value: 1},
{name: '2022-01-16T00:00:00Z', value: 0},
{name: '2022-01-17T00:00:00Z', value: 0},
{name: '2022-01-18T00:00:00Z', value: 0},
{name: '2022-01-19T00:00:00Z', value: 0},
{name: '2022-01-20T00:00:00Z', value: 0},
{name: '2022-01-21T00:00:00Z', value: 0},
{name: '2022-01-22T00:00:00Z', value: 0},
{name: '2022-01-23T00:00:00Z', value: 0},
{name: '2022-01-24T00:00:00Z', value: 0},
{name: '2022-01-25T00:00:00Z', value: 0},
{name: '2022-01-26T00:00:00Z', value: 0},
{name: '2022-01-27T00:00:00Z', value: 0},
{name: '2022-01-28T00:00:00Z', value: 0},
],
seriesName: 'sessions > abnormal, 2 : sum(session)',
},
{
data: [
{name: '2022-01-15T00:00:00Z', value: 1},
{name: '2022-01-16T00:00:00Z', value: 0},
{name: '2022-01-17T00:00:00Z', value: 0},
{name: '2022-01-18T00:00:00Z', value: 0},
{name: '2022-01-19T00:00:00Z', value: 0},
{name: '2022-01-20T00:00:00Z', value: 37},
{name: '2022-01-21T00:00:00Z', value: 0},
{name: '2022-01-22T00:00:00Z', value: 0},
{name: '2022-01-23T00:00:00Z', value: 0},
{name: '2022-01-24T00:00:00Z', value: 335},
{name: '2022-01-25T00:00:00Z', value: 79},
{name: '2022-01-26T00:00:00Z', value: 0},
{name: '2022-01-27T00:00:00Z', value: 0},
{name: '2022-01-28T00:00:00Z', value: 0},
],
seriesName: 'sessions > errored, 2 : sum(session)',
},
{
data: [
{name: '2022-01-15T00:00:00Z', value: 1},
{name: '2022-01-16T00:00:00Z', value: 0},
{name: '2022-01-17T00:00:00Z', value: 0},
{name: '2022-01-18T00:00:00Z', value: 0},
{name: '2022-01-19T00:00:00Z', value: 0},
{name: '2022-01-20T00:00:00Z', value: 2503},
{name: '2022-01-21T00:00:00Z', value: 661},
{name: '2022-01-22T00:00:00Z', value: 0},
{name: '2022-01-23T00:00:00Z', value: 0},
{name: '2022-01-24T00:00:00Z', value: 1464},
{name: '2022-01-25T00:00:00Z', value: 430},
{name: '2022-01-26T00:00:00Z', value: 0},
{name: '2022-01-27T00:00:00Z', value: 0},
{name: '2022-01-28T00:00:00Z', value: 0},
],
seriesName: 'sessions > healthy, 2 : sum(session)',
},
],
})
)
);
});
it('can send table requests', async function () {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: MetricsSessionUserCountByStatusByReleaseFixture(),
});
const children = jest.fn(() => );
render(
{children}
);
expect(mock).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(children).toHaveBeenLastCalledWith(
expect.objectContaining({
errorMessage: undefined,
loading: false,
tableResults: [
{
data: [
{
'count_unique(user)': 1,
id: '0',
release: '1',
'session.status': 'crashed',
'sum(session)': 34,
},
{
'count_unique(user)': 1,
id: '1',
release: '1',
'session.status': 'abnormal',
'sum(session)': 1,
},
{
'count_unique(user)': 2,
id: '2',
release: '1',
'session.status': 'errored',
'sum(session)': 451,
},
{
'count_unique(user)': 3,
id: '3',
release: '1',
'session.status': 'healthy',
'sum(session)': 5058,
},
{
'count_unique(user)': 2,
id: '4',
release: '2',
'session.status': 'crashed',
'sum(session)': 35,
},
{
'count_unique(user)': 1,
id: '5',
release: '2',
'session.status': 'abnormal',
'sum(session)': 1,
},
{
'count_unique(user)': 1,
id: '6',
release: '2',
'session.status': 'errored',
'sum(session)': 452,
},
{
'count_unique(user)': 10,
id: '7',
release: '2',
'session.status': 'healthy',
'sum(session)': 5059,
},
],
meta: {
'count_unique(user)': 'integer',
release: 'string',
'session.status': 'string',
'sum(session)': 'integer',
fields: {
'count_unique(user)': 'integer',
release: 'string',
'session.status': 'string',
'sum(session)': 'integer',
},
},
title: 'sessions',
},
],
timeseriesResults: undefined,
})
)
);
});
it('can send big number requests', async function () {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: MetricsFieldFixture(`count_unique(sentry.sessions.user)`),
});
const children = jest.fn(() => );
render(
{children}
);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: expect.objectContaining({
field: ['count_unique(sentry.sessions.user)'],
orderBy: '-count_unique(sentry.sessions.user)',
}),
})
);
await waitFor(() =>
expect(children).toHaveBeenLastCalledWith(
expect.objectContaining({
loading: false,
tableResults: [
{
data: [{'count_unique(user)': 51292.95404741901, id: '0'}],
meta: {
'count_unique(user)': 'integer',
fields: {'count_unique(user)': 'integer'},
},
title: 'sessions',
},
],
})
)
);
});
it('can send multiple API requests', async function () {
const metricsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: SessionsFieldFixture(`session.all`),
match: [
MockApiClient.matchQuery({
field: [`session.all`],
}),
],
});
render(
{() => }
);
// Child should be rendered and 2 requests should be sent.
expect(await screen.findByTestId('child')).toBeInTheDocument();
expect(metricsMock).toHaveBeenCalledTimes(2);
expect(metricsMock).toHaveBeenNthCalledWith(
1,
'/organizations/org-slug/metrics/data/',
expect.objectContaining({
query: {
environment: ['prod'],
field: ['session.all'],
groupBy: [],
interval: '30m',
project: [1],
statsPeriod: '14d',
per_page: 1,
includeSeries: 1,
includeTotals: 0,
},
})
);
expect(metricsMock).toHaveBeenNthCalledWith(
2,
'/organizations/org-slug/metrics/data/',
expect.objectContaining({
query: {
environment: ['prod'],
field: ['session.all'],
groupBy: [],
interval: '30m',
project: [1],
query: 'environment:prod',
statsPeriod: '14d',
per_page: 1,
includeSeries: 1,
includeTotals: 0,
},
})
);
});
it('sets errorMessage when the first request fails', async function () {
const failMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
statusCode: 400,
body: {detail: badMessage},
match: [
MockApiClient.matchQuery({
field: [`session.all`],
}),
],
});
const children = jest.fn(() => );
render(
{children}
);
// Child should be rendered and 2 requests should be sent.
expect(screen.getByTestId('child')).toBeInTheDocument();
expect(failMock).toHaveBeenCalledTimes(2);
await waitFor(() =>
expect(children).toHaveBeenLastCalledWith(
expect.objectContaining({errorMessage: badMessage})
)
);
});
it('adjusts interval based on date window', async function () {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: SessionsFieldFixture(`session.all`),
});
render(
{() => }
);
expect(await screen.findByTestId('child')).toBeInTheDocument();
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
query: expect.objectContaining({
interval: '30m',
statsPeriod: '14d',
environment: ['prod'],
project: [1],
}),
})
);
});
it('does not re-fetch when renaming legend alias / adding falsy fields', async () => {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: SessionsFieldFixture(`session.all`),
});
const children = jest.fn(() => );
const {rerender} = render(
{children}
);
await waitFor(() => {
expect(mock).toHaveBeenCalledTimes(1);
});
rerender(
{children}
);
// no additional request has been sent, the total count of requests is still 1
await waitFor(() => {
expect(mock).toHaveBeenCalledTimes(1);
});
});
it('does not re-fetch when dashboard filter remains the same', async () => {
const mock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/metrics/data/',
body: SessionsFieldFixture(`session.all`),
});
const children = jest.fn(() => );
const {rerender} = render(
{children}
);
await waitFor(() => {
expect(mock).toHaveBeenCalledTimes(1);
});
rerender(
{children}
);
// no additional request has been sent, the total count of requests is still 1
await waitFor(() => {
expect(mock).toHaveBeenCalledTimes(1);
});
});
});