index.spec.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  3. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  4. import OrganizationStore from 'sentry/stores/organizationStore';
  5. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  6. import ProjectsStore from 'sentry/stores/projectsStore';
  7. import {DataCategory, PageFilters} from 'sentry/types';
  8. import {OrganizationStats, PAGE_QUERY_PARAMS} from 'sentry/views/organizationStats';
  9. import {ChartDataTransform} from './usageChart';
  10. describe('OrganizationStats', function () {
  11. const defaultSelection: PageFilters = {
  12. projects: [],
  13. environments: [],
  14. datetime: {
  15. start: null,
  16. end: null,
  17. period: '24h',
  18. utc: false,
  19. },
  20. };
  21. const projects = ['1', '2', '3'].map(id => TestStubs.Project({id, slug: `proj-${id}`}));
  22. const {organization, router, routerContext} = initializeOrg({
  23. organization: {features: ['global-views', 'team-insights']},
  24. projects,
  25. project: undefined,
  26. router: undefined,
  27. });
  28. const endpoint = `/organizations/${organization.slug}/stats_v2/`;
  29. const defaultProps: OrganizationStats['props'] = {
  30. router,
  31. organization,
  32. ...router,
  33. route: {},
  34. params: {orgId: organization.slug as string},
  35. routeParams: {},
  36. };
  37. let mockRequest;
  38. beforeEach(() => {
  39. MockApiClient.clearMockResponses();
  40. PageFiltersStore.init();
  41. PageFiltersStore.onInitializeUrlState(defaultSelection, new Set());
  42. OrganizationStore.onUpdate(organization, {replace: true});
  43. ProjectsStore.loadInitialData(projects);
  44. mockRequest = MockApiClient.addMockResponse({
  45. method: 'GET',
  46. url: endpoint,
  47. body: mockStatsResponse,
  48. });
  49. });
  50. afterEach(() => {
  51. PageFiltersStore.reset();
  52. });
  53. /**
  54. * Features and Alerts
  55. */
  56. it('renders header state wihout tabs', () => {
  57. const newOrg = initializeOrg();
  58. const newProps = {
  59. ...defaultProps,
  60. organization: newOrg.organization,
  61. };
  62. render(<OrganizationStats {...newProps} />, {context: newOrg.routerContext});
  63. expect(screen.getByText('Organization Usage Stats')).toBeInTheDocument();
  64. });
  65. it('renders header state with tabs', () => {
  66. render(<OrganizationStats {...defaultProps} />, {context: routerContext});
  67. expect(screen.getByText('Stats')).toBeInTheDocument();
  68. expect(screen.getByText('Usage')).toBeInTheDocument();
  69. expect(screen.getByText('Issues')).toBeInTheDocument();
  70. expect(screen.getByText('Health')).toBeInTheDocument();
  71. });
  72. /**
  73. * Base + Error Handling
  74. */
  75. it('renders the base view', () => {
  76. render(<OrganizationStats {...defaultProps} />, {context: routerContext});
  77. // Default to Errors category
  78. expect(screen.getAllByText('Errors')[0]).toBeInTheDocument();
  79. // Render the chart and project table
  80. expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
  81. expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
  82. // Render the cards
  83. expect(screen.getAllByText('Total')[0]).toBeInTheDocument();
  84. expect(screen.getByText('64')).toBeInTheDocument();
  85. expect(screen.getAllByText('Accepted')[0]).toBeInTheDocument();
  86. expect(screen.getByText('28')).toBeInTheDocument();
  87. expect(screen.getByText('6 in last min')).toBeInTheDocument();
  88. expect(screen.getAllByText('Filtered')[0]).toBeInTheDocument();
  89. expect(screen.getAllByText('7')[0]).toBeInTheDocument();
  90. expect(screen.getAllByText('Dropped')[0]).toBeInTheDocument();
  91. expect(screen.getAllByText('29')[0]).toBeInTheDocument();
  92. // Correct API Calls
  93. const mockExpectations = {
  94. UsageStatsOrg: {
  95. statsPeriod: DEFAULT_STATS_PERIOD,
  96. interval: '1h',
  97. groupBy: ['category', 'outcome'],
  98. field: ['sum(quantity)'],
  99. },
  100. UsageStatsPerMin: {
  101. statsPeriod: '5m',
  102. interval: '1m',
  103. groupBy: ['category', 'outcome'],
  104. field: ['sum(quantity)'],
  105. },
  106. UsageStatsProjects: {
  107. statsPeriod: DEFAULT_STATS_PERIOD,
  108. interval: '1h',
  109. groupBy: ['outcome', 'project'],
  110. project: '-1',
  111. field: ['sum(quantity)'],
  112. category: 'error',
  113. },
  114. };
  115. for (const query of Object.values(mockExpectations)) {
  116. expect(mockRequest).toHaveBeenCalledWith(
  117. endpoint,
  118. expect.objectContaining({query})
  119. );
  120. }
  121. });
  122. it('renders with an error on stats endpoint', () => {
  123. MockApiClient.clearMockResponses();
  124. MockApiClient.addMockResponse({
  125. url: endpoint,
  126. statusCode: 500,
  127. });
  128. render(<OrganizationStats {...defaultProps} />, {context: routerContext});
  129. expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
  130. expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
  131. expect(screen.getByTestId('error-messages')).toBeInTheDocument();
  132. });
  133. it('renders with an error when user has no projects', () => {
  134. MockApiClient.clearMockResponses();
  135. MockApiClient.addMockResponse({
  136. url: endpoint,
  137. statusCode: 400,
  138. body: {detail: 'No projects available'},
  139. });
  140. render(<OrganizationStats {...defaultProps} />, {context: routerContext});
  141. expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
  142. expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
  143. expect(screen.getByTestId('empty-message')).toBeInTheDocument();
  144. });
  145. /**
  146. * Router Handling
  147. */
  148. it('pushes state changes to the route', () => {
  149. render(<OrganizationStats {...defaultProps} />, {context: routerContext});
  150. userEvent.click(screen.getByText('Category'));
  151. userEvent.click(screen.getByText('Attachments'));
  152. expect(router.push).toHaveBeenCalledWith(
  153. expect.objectContaining({
  154. query: {dataCategory: DataCategory.ATTACHMENTS},
  155. })
  156. );
  157. userEvent.click(screen.getByText('Periodic'));
  158. userEvent.click(screen.getByText('Cumulative'));
  159. expect(router.push).toHaveBeenCalledWith(
  160. expect.objectContaining({
  161. query: {transform: ChartDataTransform.CUMULATIVE},
  162. })
  163. );
  164. const inputQuery = 'proj-1';
  165. userEvent.type(
  166. screen.getByPlaceholderText('Filter your projects'),
  167. `${inputQuery}{enter}`
  168. );
  169. expect(router.push).toHaveBeenCalledWith(
  170. expect.objectContaining({
  171. query: {query: inputQuery},
  172. })
  173. );
  174. });
  175. it('does not leak query params onto next page links', () => {
  176. const dummyLocation = PAGE_QUERY_PARAMS.reduce(
  177. (location, param) => {
  178. location.query[param] = '';
  179. return location;
  180. },
  181. {query: {}}
  182. );
  183. const newProps = {
  184. ...defaultProps,
  185. location: dummyLocation as any,
  186. };
  187. render(<OrganizationStats {...newProps} />, {context: routerContext});
  188. const projectLinks = screen.getAllByTestId('badge-display-name');
  189. expect(projectLinks.length).toBeGreaterThan(0);
  190. const leakingRegex = PAGE_QUERY_PARAMS.join('|');
  191. for (const projectLink of projectLinks) {
  192. expect(projectLink.closest('a')).toHaveAttribute(
  193. 'href',
  194. expect.not.stringMatching(leakingRegex)
  195. );
  196. }
  197. });
  198. });
  199. const mockStatsResponse = {
  200. start: '2021-01-01T00:00:00Z',
  201. end: '2021-01-07T00:00:00Z',
  202. intervals: [
  203. '2021-01-01T00:00:00Z',
  204. '2021-01-02T00:00:00Z',
  205. '2021-01-03T00:00:00Z',
  206. '2021-01-04T00:00:00Z',
  207. '2021-01-05T00:00:00Z',
  208. '2021-01-06T00:00:00Z',
  209. '2021-01-07T00:00:00Z',
  210. ],
  211. groups: [
  212. {
  213. by: {
  214. project: 1,
  215. category: 'attachment',
  216. outcome: 'accepted',
  217. },
  218. totals: {
  219. 'sum(quantity)': 28000,
  220. },
  221. series: {
  222. 'sum(quantity)': [1000, 2000, 3000, 4000, 5000, 6000, 7000],
  223. },
  224. },
  225. {
  226. by: {
  227. project: 1,
  228. outcome: 'accepted',
  229. category: 'transaction',
  230. },
  231. totals: {
  232. 'sum(quantity)': 28,
  233. },
  234. series: {
  235. 'sum(quantity)': [1, 2, 3, 4, 5, 6, 7],
  236. },
  237. },
  238. {
  239. by: {
  240. project: 1,
  241. category: 'error',
  242. outcome: 'accepted',
  243. },
  244. totals: {
  245. 'sum(quantity)': 28,
  246. },
  247. series: {
  248. 'sum(quantity)': [1, 2, 3, 4, 5, 6, 7],
  249. },
  250. },
  251. {
  252. by: {
  253. project: 1,
  254. category: 'error',
  255. outcome: 'filtered',
  256. },
  257. totals: {
  258. 'sum(quantity)': 7,
  259. },
  260. series: {
  261. 'sum(quantity)': [1, 1, 1, 1, 1, 1, 1],
  262. },
  263. },
  264. {
  265. by: {
  266. project: 1,
  267. category: 'error',
  268. outcome: 'rate_limited',
  269. },
  270. totals: {
  271. 'sum(quantity)': 14,
  272. },
  273. series: {
  274. 'sum(quantity)': [2, 2, 2, 2, 2, 2, 2],
  275. },
  276. },
  277. {
  278. by: {
  279. project: 1,
  280. category: 'error',
  281. outcome: 'invalid',
  282. },
  283. totals: {
  284. 'sum(quantity)': 15,
  285. },
  286. series: {
  287. 'sum(quantity)': [2, 2, 2, 2, 2, 2, 3],
  288. },
  289. },
  290. {
  291. by: {
  292. project: 1,
  293. category: 'error',
  294. outcome: 'client_discard',
  295. },
  296. totals: {
  297. 'sum(quantity)': 15,
  298. },
  299. series: {
  300. 'sum(quantity)': [2, 2, 2, 2, 2, 2, 3],
  301. },
  302. },
  303. ],
  304. };