import {browserHistory} from 'react-router'; import {mountWithTheme} from 'sentry-test/enzyme'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {act} from 'sentry-test/reactTestingLibrary'; import {DEFAULT_RELATIVE_PERIODS, DEFAULT_STATS_PERIOD} from 'sentry/constants'; import ProjectsStore from 'sentry/stores/projectsStore'; import {DataCategory} from 'sentry/types'; import {OrganizationStats} from 'sentry/views/organizationStats'; import {CHART_OPTIONS_DATA_TRANSFORM} from 'sentry/views/organizationStats/usageChart'; describe('OrganizationStats', function () { const router = TestStubs.router(); const {organization, routerContext} = initializeOrg({router}); const statsUrl = `/organizations/${organization.slug}/stats_v2/`; const ninetyDays = Object.keys(DEFAULT_RELATIVE_PERIODS)[5]; const {mockOrgStats} = getMockResponse(); let mock; beforeEach(() => { MockApiClient.clearMockResponses(); mock = MockApiClient.addMockResponse({ url: statsUrl, body: mockOrgStats, }); }); it('renders with default state', async function () { const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect(wrapper.text()).toContain('Organization Usage Stats'); expect(wrapper.find('UsageChart')).toHaveLength(1); expect(wrapper.find('UsageTable')).toHaveLength(1); expect(wrapper.find('IconWarning')).toHaveLength(0); const orgAsync = wrapper.find('UsageStatsOrganization'); expect(orgAsync.props().dataDatetime.period).toEqual(DEFAULT_STATS_PERIOD); expect(orgAsync.props().dataCategory).toEqual(DataCategory.ERRORS); expect(orgAsync.props().chartTransform).toEqual(undefined); expect(orgAsync.text()).toContain('Total Errors64'); expect(orgAsync.text()).toContain('Accepted28'); expect(orgAsync.text()).toContain('Filtered7'); expect(orgAsync.text()).toContain('Dropped29'); const orgChart = wrapper.find('UsageChart'); expect(orgChart.props().dataCategory).toEqual(DataCategory.ERRORS); expect(orgChart.props().dataTransform).toEqual(CHART_OPTIONS_DATA_TRANSFORM[1].value); const minAsync = wrapper.find('UsageStatsPerMin'); expect(minAsync.props().dataCategory).toEqual(DataCategory.ERRORS); expect(minAsync.text()).toContain('6'); // Display 2nd last value in series const projectAsync = wrapper.find('UsageStatsProjects'); expect(projectAsync.props().dataDatetime.period).toEqual(DEFAULT_STATS_PERIOD); expect(projectAsync.props().dataCategory).toEqual(DataCategory.ERRORS); expect(projectAsync.props().tableSort).toEqual(undefined); const projectTable = wrapper.find('UsageTable'); expect(projectTable.props().dataCategory).toEqual(DataCategory.ERRORS); // API calls with defaults expect(mock).toHaveBeenCalledTimes(3); // From UsageStatsOrg expect(mock).toHaveBeenNthCalledWith( 1, '/organizations/org-slug/stats_v2/', expect.objectContaining({ query: { statsPeriod: DEFAULT_STATS_PERIOD, interval: '1h', groupBy: ['category', 'outcome'], field: ['sum(quantity)'], }, }) ); // From UsageStatsPerMin expect(mock).toHaveBeenNthCalledWith( 2, '/organizations/org-slug/stats_v2/', expect.objectContaining({ query: { statsPeriod: '5m', interval: '1m', groupBy: ['category', 'outcome'], field: ['sum(quantity)'], }, }) ); // From UsageStatsProjects expect(mock).toHaveBeenNthCalledWith( 3, '/organizations/org-slug/stats_v2/', expect.objectContaining({ query: { statsPeriod: DEFAULT_STATS_PERIOD, interval: '1h', groupBy: ['outcome', 'project'], project: '-1', field: ['sum(quantity)'], category: 'error', }, }) ); }); it('renders with error on organization stats endpoint', async function () { MockApiClient.addMockResponse({ url: statsUrl, statusCode: 500, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect(wrapper.text()).toContain('Organization Usage Stats'); expect(wrapper.find('UsageChart')).toHaveLength(1); expect(wrapper.find('UsageTable')).toHaveLength(1); expect(wrapper.find('IconWarning')).toHaveLength(2); }); it('renders with error when user has no access to projects', async function () { MockApiClient.addMockResponse({ url: statsUrl, statusCode: 400, body: { detail: 'No projects available', }, }); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); expect(wrapper.text()).toContain('Organization Usage Stats'); expect(wrapper.find('UsageTable')).toHaveLength(1); expect(wrapper.find('IconWarning')).toHaveLength(2); expect(wrapper.find('UsageTable').text()).toContain('no projects'); }); it('passes state from router down to components', async function () { const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); const orgAsync = wrapper.find('UsageStatsOrganization'); expect(orgAsync.props().dataDatetime.period).toEqual(ninetyDays); expect(orgAsync.props().dataCategory).toEqual(DataCategory.TRANSACTIONS); expect(orgAsync.props().chartTransform).toEqual( CHART_OPTIONS_DATA_TRANSFORM[1].value ); const orgChart = wrapper.find('UsageChart'); expect(orgChart.props().dataCategory).toEqual(DataCategory.TRANSACTIONS); expect(orgChart.props().dataTransform).toEqual(CHART_OPTIONS_DATA_TRANSFORM[1].value); const projectAsync = wrapper.find('UsageStatsProjects'); expect(projectAsync.props().dataDatetime.period).toEqual(ninetyDays); expect(projectAsync.props().dataCategory).toEqual(DataCategory.TRANSACTIONS); expect(projectAsync.props().tableSort).toEqual('-project'); const projectTable = wrapper.find('UsageTable'); expect(projectTable.props().dataCategory).toEqual(DataCategory.TRANSACTIONS); expect(mock).toHaveBeenCalledTimes(3); expect(mock).toHaveBeenNthCalledWith( 1, '/organizations/org-slug/stats_v2/', expect.objectContaining({ query: { statsPeriod: ninetyDays, interval: '1d', groupBy: ['category', 'outcome'], field: ['sum(quantity)'], }, }) ); expect(mock).toHaveBeenNthCalledWith( 2, '/organizations/org-slug/stats_v2/', expect.objectContaining({ query: { statsPeriod: '5m', interval: '1m', groupBy: ['category', 'outcome'], field: ['sum(quantity)'], }, }) ); expect(mock).toHaveBeenNthCalledWith( 3, '/organizations/org-slug/stats_v2/', expect.objectContaining({ query: { statsPeriod: ninetyDays, interval: '1d', groupBy: ['outcome', 'project'], project: '-1', category: 'transaction', field: ['sum(quantity)'], }, }) ); }); it('pushes state to router', async function () { // Add another 30 projects to allow us to test pagination const moreProjects = Array.from(Array(30).keys()).map(id => TestStubs.Project({id, slug: `myProjectSlug-${id}`}) ); act(() => ProjectsStore.loadInitialData(moreProjects)); const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); const optionpagePeriod = wrapper.find(`TimeRangeSelector`); optionpagePeriod.props().onUpdate({relative: '30d'}); expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ pageStatsPeriod: '30d', }), }); optionpagePeriod .props() .onUpdate({start: '2021-01-01', end: '2021-01-31', utc: true}); expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ pageStart: '2021-01-01T00:00:00Z', pageEnd: '2021-01-31T00:00:00Z', pageUtc: true, }), }); const optionDataCategory = wrapper.find('DropdownDataCategory'); optionDataCategory.props().onChange({value: DataCategory.ATTACHMENTS}); expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({dataCategory: DataCategory.ATTACHMENTS}), }); const optionChartTransform = wrapper.find('OptionSelector[title="Type"]'); optionChartTransform.props().onChange(CHART_OPTIONS_DATA_TRANSFORM[1].value); expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ transform: CHART_OPTIONS_DATA_TRANSFORM[1].value, }), }); const inputQuery = wrapper.find('SearchBar'); inputQuery.props().onSearch('someSearchQuery'); expect(router.push).toHaveBeenCalledWith({ query: expect.objectContaining({ query: 'someSearchQuery', }), }); wrapper.find('Pagination Button').last().simulate('click'); expect(browserHistory.push).toHaveBeenCalledWith( expect.objectContaining({ query: expect.objectContaining({cursor: '0:25:0'}), }) ); }); it('removes page query parameters during outbound navigation', async () => { const wrapper = mountWithTheme( , routerContext ); await tick(); wrapper.update(); const outboundLinks = wrapper.instance().getNextLocations({id: 1, slug: 'project'}); expect(outboundLinks).toEqual({ performance: { query: {project: 1, notAPageKey: 'hello'}, pathname: '/organizations/org-slug/performance/', }, projectDetail: { query: {project: 1, notAPageKey: 'hello'}, pathname: '/organizations/org-slug/projects/project/', }, issueList: { query: {project: 1, notAPageKey: 'hello'}, pathname: '/organizations/org-slug/issues/', }, settings: { pathname: '/settings/org-slug/projects/project/', }, }); }); it('displays sampling alert', function () { const {organization: orgWithSampling} = initializeOrg({ organization: {features: ['server-side-sampling', 'server-side-sampling-ui']}, router, }); const wrapper = mountWithTheme( , routerContext ); expect(wrapper.text()).toContain( 'Manage your transaction usage in Server-Side Sampling' ); }); }); function getMockResponse() { return { mockOrgStats: { start: '2021-01-01T00:00:00Z', end: '2021-01-07T00:00:00Z', intervals: [ '2021-01-01T00:00:00Z', '2021-01-02T00:00:00Z', '2021-01-03T00:00:00Z', '2021-01-04T00:00:00Z', '2021-01-05T00:00:00Z', '2021-01-06T00:00:00Z', '2021-01-07T00:00:00Z', ], groups: [ { by: { category: 'attachment', outcome: 'accepted', }, totals: { 'sum(quantity)': 28000, }, series: { 'sum(quantity)': [1000, 2000, 3000, 4000, 5000, 6000, 7000], }, }, { by: { outcome: 'accepted', category: 'transaction', }, totals: { 'sum(quantity)': 28, }, series: { 'sum(quantity)': [1, 2, 3, 4, 5, 6, 7], }, }, { by: { category: 'error', outcome: 'accepted', }, totals: { 'sum(quantity)': 28, }, series: { 'sum(quantity)': [1, 2, 3, 4, 5, 6, 7], }, }, { by: { category: 'error', outcome: 'filtered', }, totals: { 'sum(quantity)': 7, }, series: { 'sum(quantity)': [1, 1, 1, 1, 1, 1, 1], }, }, { by: { category: 'error', outcome: 'rate_limited', }, totals: { 'sum(quantity)': 14, }, series: { 'sum(quantity)': [2, 2, 2, 2, 2, 2, 2], }, }, { by: { category: 'error', outcome: 'invalid', }, totals: { 'sum(quantity)': 15, }, series: { 'sum(quantity)': [2, 2, 2, 2, 2, 2, 3], }, }, { by: { category: 'error', outcome: 'client_discard', }, totals: { 'sum(quantity)': 15, }, series: { 'sum(quantity)': [2, 2, 2, 2, 2, 2, 3], }, }, ], }, }; }