index.spec.jsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. import {browserHistory} from 'react-router';
  2. import {mountWithTheme} from 'sentry-test/enzyme';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {act} from 'sentry-test/reactTestingLibrary';
  5. import {DEFAULT_RELATIVE_PERIODS, DEFAULT_STATS_PERIOD} from 'sentry/constants';
  6. import ProjectsStore from 'sentry/stores/projectsStore';
  7. import {DataCategory} from 'sentry/types';
  8. import {OrganizationStats} from 'sentry/views/organizationStats';
  9. import {CHART_OPTIONS_DATA_TRANSFORM} from 'sentry/views/organizationStats/usageChart';
  10. describe('OrganizationStats', function () {
  11. const router = TestStubs.router();
  12. const {organization, routerContext} = initializeOrg({router});
  13. const statsUrl = `/organizations/${organization.slug}/stats_v2/`;
  14. const ninetyDays = Object.keys(DEFAULT_RELATIVE_PERIODS)[5];
  15. const {mockOrgStats} = getMockResponse();
  16. let mock;
  17. beforeEach(() => {
  18. MockApiClient.clearMockResponses();
  19. mock = MockApiClient.addMockResponse({
  20. url: statsUrl,
  21. body: mockOrgStats,
  22. });
  23. });
  24. it('renders with default state', async function () {
  25. const wrapper = mountWithTheme(
  26. <OrganizationStats organization={organization} />,
  27. routerContext
  28. );
  29. await tick();
  30. wrapper.update();
  31. expect(wrapper.text()).toContain('Organization Usage Stats');
  32. expect(wrapper.find('UsageChart')).toHaveLength(1);
  33. expect(wrapper.find('UsageTable')).toHaveLength(1);
  34. expect(wrapper.find('IconWarning')).toHaveLength(0);
  35. const orgAsync = wrapper.find('UsageStatsOrganization');
  36. expect(orgAsync.props().dataDatetime.period).toEqual(DEFAULT_STATS_PERIOD);
  37. expect(orgAsync.props().dataCategory).toEqual(DataCategory.ERRORS);
  38. expect(orgAsync.props().chartTransform).toEqual(undefined);
  39. expect(orgAsync.text()).toContain('Total Errors64');
  40. expect(orgAsync.text()).toContain('Accepted28');
  41. expect(orgAsync.text()).toContain('Filtered7');
  42. expect(orgAsync.text()).toContain('Dropped29');
  43. const orgChart = wrapper.find('UsageChart');
  44. expect(orgChart.props().dataCategory).toEqual(DataCategory.ERRORS);
  45. expect(orgChart.props().dataTransform).toEqual(CHART_OPTIONS_DATA_TRANSFORM[1].value);
  46. const minAsync = wrapper.find('UsageStatsPerMin');
  47. expect(minAsync.props().dataCategory).toEqual(DataCategory.ERRORS);
  48. expect(minAsync.text()).toContain('6'); // Display 2nd last value in series
  49. const projectAsync = wrapper.find('UsageStatsProjects');
  50. expect(projectAsync.props().dataDatetime.period).toEqual(DEFAULT_STATS_PERIOD);
  51. expect(projectAsync.props().dataCategory).toEqual(DataCategory.ERRORS);
  52. expect(projectAsync.props().tableSort).toEqual(undefined);
  53. const projectTable = wrapper.find('UsageTable');
  54. expect(projectTable.props().dataCategory).toEqual(DataCategory.ERRORS);
  55. // API calls with defaults
  56. expect(mock).toHaveBeenCalledTimes(3);
  57. // From UsageStatsOrg
  58. expect(mock).toHaveBeenNthCalledWith(
  59. 1,
  60. '/organizations/org-slug/stats_v2/',
  61. expect.objectContaining({
  62. query: {
  63. statsPeriod: DEFAULT_STATS_PERIOD,
  64. interval: '1h',
  65. groupBy: ['category', 'outcome'],
  66. field: ['sum(quantity)'],
  67. },
  68. })
  69. );
  70. // From UsageStatsPerMin
  71. expect(mock).toHaveBeenNthCalledWith(
  72. 2,
  73. '/organizations/org-slug/stats_v2/',
  74. expect.objectContaining({
  75. query: {
  76. statsPeriod: '5m',
  77. interval: '1m',
  78. groupBy: ['category', 'outcome'],
  79. field: ['sum(quantity)'],
  80. },
  81. })
  82. );
  83. // From UsageStatsProjects
  84. expect(mock).toHaveBeenNthCalledWith(
  85. 3,
  86. '/organizations/org-slug/stats_v2/',
  87. expect.objectContaining({
  88. query: {
  89. statsPeriod: DEFAULT_STATS_PERIOD,
  90. interval: '1h',
  91. groupBy: ['outcome', 'project'],
  92. project: '-1',
  93. field: ['sum(quantity)'],
  94. category: 'error',
  95. },
  96. })
  97. );
  98. });
  99. it('renders with error on organization stats endpoint', async function () {
  100. MockApiClient.addMockResponse({
  101. url: statsUrl,
  102. statusCode: 500,
  103. });
  104. const wrapper = mountWithTheme(
  105. <OrganizationStats organization={organization} />,
  106. routerContext
  107. );
  108. await tick();
  109. wrapper.update();
  110. expect(wrapper.text()).toContain('Organization Usage Stats');
  111. expect(wrapper.find('UsageChart')).toHaveLength(1);
  112. expect(wrapper.find('UsageTable')).toHaveLength(1);
  113. expect(wrapper.find('IconWarning')).toHaveLength(2);
  114. });
  115. it('renders with error when user has no access to projects', async function () {
  116. MockApiClient.addMockResponse({
  117. url: statsUrl,
  118. statusCode: 400,
  119. body: {
  120. detail: 'No projects available',
  121. },
  122. });
  123. const wrapper = mountWithTheme(
  124. <OrganizationStats organization={organization} />,
  125. routerContext
  126. );
  127. await tick();
  128. wrapper.update();
  129. expect(wrapper.text()).toContain('Organization Usage Stats');
  130. expect(wrapper.find('UsageTable')).toHaveLength(1);
  131. expect(wrapper.find('IconWarning')).toHaveLength(2);
  132. expect(wrapper.find('UsageTable').text()).toContain('no projects');
  133. });
  134. it('passes state from router down to components', async function () {
  135. const wrapper = mountWithTheme(
  136. <OrganizationStats
  137. organization={organization}
  138. location={{
  139. query: {
  140. pageStatsPeriod: ninetyDays,
  141. dataCategory: DataCategory.TRANSACTIONS,
  142. transform: CHART_OPTIONS_DATA_TRANSFORM[1].value,
  143. sort: '-project',
  144. query: 'myProjectSlug',
  145. cursor: '0:1:0',
  146. },
  147. }}
  148. />,
  149. routerContext
  150. );
  151. await tick();
  152. wrapper.update();
  153. const orgAsync = wrapper.find('UsageStatsOrganization');
  154. expect(orgAsync.props().dataDatetime.period).toEqual(ninetyDays);
  155. expect(orgAsync.props().dataCategory).toEqual(DataCategory.TRANSACTIONS);
  156. expect(orgAsync.props().chartTransform).toEqual(
  157. CHART_OPTIONS_DATA_TRANSFORM[1].value
  158. );
  159. const orgChart = wrapper.find('UsageChart');
  160. expect(orgChart.props().dataCategory).toEqual(DataCategory.TRANSACTIONS);
  161. expect(orgChart.props().dataTransform).toEqual(CHART_OPTIONS_DATA_TRANSFORM[1].value);
  162. const projectAsync = wrapper.find('UsageStatsProjects');
  163. expect(projectAsync.props().dataDatetime.period).toEqual(ninetyDays);
  164. expect(projectAsync.props().dataCategory).toEqual(DataCategory.TRANSACTIONS);
  165. expect(projectAsync.props().tableSort).toEqual('-project');
  166. const projectTable = wrapper.find('UsageTable');
  167. expect(projectTable.props().dataCategory).toEqual(DataCategory.TRANSACTIONS);
  168. expect(mock).toHaveBeenCalledTimes(3);
  169. expect(mock).toHaveBeenNthCalledWith(
  170. 1,
  171. '/organizations/org-slug/stats_v2/',
  172. expect.objectContaining({
  173. query: {
  174. statsPeriod: ninetyDays,
  175. interval: '1d',
  176. groupBy: ['category', 'outcome'],
  177. field: ['sum(quantity)'],
  178. },
  179. })
  180. );
  181. expect(mock).toHaveBeenNthCalledWith(
  182. 2,
  183. '/organizations/org-slug/stats_v2/',
  184. expect.objectContaining({
  185. query: {
  186. statsPeriod: '5m',
  187. interval: '1m',
  188. groupBy: ['category', 'outcome'],
  189. field: ['sum(quantity)'],
  190. },
  191. })
  192. );
  193. expect(mock).toHaveBeenNthCalledWith(
  194. 3,
  195. '/organizations/org-slug/stats_v2/',
  196. expect.objectContaining({
  197. query: {
  198. statsPeriod: ninetyDays,
  199. interval: '1d',
  200. groupBy: ['outcome', 'project'],
  201. project: '-1',
  202. category: 'transaction',
  203. field: ['sum(quantity)'],
  204. },
  205. })
  206. );
  207. });
  208. it('pushes state to router', async function () {
  209. // Add another 30 projects to allow us to test pagination
  210. const moreProjects = Array.from(Array(30).keys()).map(id =>
  211. TestStubs.Project({id, slug: `myProjectSlug-${id}`})
  212. );
  213. act(() => ProjectsStore.loadInitialData(moreProjects));
  214. const wrapper = mountWithTheme(
  215. <OrganizationStats
  216. organization={organization}
  217. location={{
  218. query: {
  219. pageStatsPeriod: ninetyDays,
  220. dataCategory: DataCategory.ERRORS,
  221. transform: CHART_OPTIONS_DATA_TRANSFORM[0].value,
  222. sort: '-project',
  223. query: 'myProjectSlug',
  224. cursor: '0:0:0',
  225. },
  226. }}
  227. router={router}
  228. />,
  229. routerContext
  230. );
  231. await tick();
  232. wrapper.update();
  233. const optionpagePeriod = wrapper.find(`TimeRangeSelector`);
  234. optionpagePeriod.props().onUpdate({relative: '30d'});
  235. expect(router.push).toHaveBeenCalledWith({
  236. query: expect.objectContaining({
  237. pageStatsPeriod: '30d',
  238. }),
  239. });
  240. optionpagePeriod
  241. .props()
  242. .onUpdate({start: '2021-01-01', end: '2021-01-31', utc: true});
  243. expect(router.push).toHaveBeenCalledWith({
  244. query: expect.objectContaining({
  245. pageStart: '2021-01-01T00:00:00Z',
  246. pageEnd: '2021-01-31T00:00:00Z',
  247. pageUtc: true,
  248. }),
  249. });
  250. const optionDataCategory = wrapper.find('DropdownDataCategory');
  251. optionDataCategory.props().onChange({value: DataCategory.ATTACHMENTS});
  252. expect(router.push).toHaveBeenCalledWith({
  253. query: expect.objectContaining({dataCategory: DataCategory.ATTACHMENTS}),
  254. });
  255. const optionChartTransform = wrapper.find('OptionSelector[title="Type"]');
  256. optionChartTransform.props().onChange(CHART_OPTIONS_DATA_TRANSFORM[1].value);
  257. expect(router.push).toHaveBeenCalledWith({
  258. query: expect.objectContaining({
  259. transform: CHART_OPTIONS_DATA_TRANSFORM[1].value,
  260. }),
  261. });
  262. const inputQuery = wrapper.find('SearchBar');
  263. inputQuery.props().onSearch('someSearchQuery');
  264. expect(router.push).toHaveBeenCalledWith({
  265. query: expect.objectContaining({
  266. query: 'someSearchQuery',
  267. }),
  268. });
  269. wrapper.find('Pagination Button').last().simulate('click');
  270. expect(browserHistory.push).toHaveBeenCalledWith(
  271. expect.objectContaining({
  272. query: expect.objectContaining({cursor: '0:25:0'}),
  273. })
  274. );
  275. });
  276. it('removes page query parameters during outbound navigation', async () => {
  277. const wrapper = mountWithTheme(
  278. <OrganizationStats
  279. organization={organization}
  280. location={{
  281. query: {
  282. pageStart: '2021-01-01T00:00:00Z',
  283. pageEnd: '2021-01-07T00:00:00Z',
  284. pageStatsPeriod: ninetyDays,
  285. pageUtc: true,
  286. dataCategory: DataCategory.TRANSACTIONS,
  287. transform: CHART_OPTIONS_DATA_TRANSFORM[1].value,
  288. sort: '-project',
  289. query: 'myProjectSlug',
  290. cursor: '0:1:0',
  291. notAPageKey: 'hello', // Should not be removed
  292. },
  293. }}
  294. router={router}
  295. />,
  296. routerContext
  297. );
  298. await tick();
  299. wrapper.update();
  300. const outboundLinks = wrapper.instance().getNextLocations({id: 1, slug: 'project'});
  301. expect(outboundLinks).toEqual({
  302. performance: {
  303. query: {project: 1, notAPageKey: 'hello'},
  304. pathname: '/organizations/org-slug/performance/',
  305. },
  306. projectDetail: {
  307. query: {project: 1, notAPageKey: 'hello'},
  308. pathname: '/organizations/org-slug/projects/project/',
  309. },
  310. issueList: {
  311. query: {project: 1, notAPageKey: 'hello'},
  312. pathname: '/organizations/org-slug/issues/',
  313. },
  314. settings: {
  315. pathname: '/settings/org-slug/projects/project/',
  316. },
  317. });
  318. });
  319. it('displays sampling alert', function () {
  320. const {organization: orgWithSampling} = initializeOrg({
  321. organization: {features: ['server-side-sampling', 'server-side-sampling-ui']},
  322. router,
  323. });
  324. const wrapper = mountWithTheme(
  325. <OrganizationStats
  326. organization={orgWithSampling}
  327. location={{
  328. query: {
  329. dataCategory: DataCategory.TRANSACTIONS,
  330. },
  331. }}
  332. router={router}
  333. />,
  334. routerContext
  335. );
  336. expect(wrapper.text()).toContain(
  337. 'Manage your transaction usage in Server-Side Sampling'
  338. );
  339. });
  340. });
  341. function getMockResponse() {
  342. return {
  343. mockOrgStats: {
  344. start: '2021-01-01T00:00:00Z',
  345. end: '2021-01-07T00:00:00Z',
  346. intervals: [
  347. '2021-01-01T00:00:00Z',
  348. '2021-01-02T00:00:00Z',
  349. '2021-01-03T00:00:00Z',
  350. '2021-01-04T00:00:00Z',
  351. '2021-01-05T00:00:00Z',
  352. '2021-01-06T00:00:00Z',
  353. '2021-01-07T00:00:00Z',
  354. ],
  355. groups: [
  356. {
  357. by: {
  358. category: 'attachment',
  359. outcome: 'accepted',
  360. },
  361. totals: {
  362. 'sum(quantity)': 28000,
  363. },
  364. series: {
  365. 'sum(quantity)': [1000, 2000, 3000, 4000, 5000, 6000, 7000],
  366. },
  367. },
  368. {
  369. by: {
  370. outcome: 'accepted',
  371. category: 'transaction',
  372. },
  373. totals: {
  374. 'sum(quantity)': 28,
  375. },
  376. series: {
  377. 'sum(quantity)': [1, 2, 3, 4, 5, 6, 7],
  378. },
  379. },
  380. {
  381. by: {
  382. category: 'error',
  383. outcome: 'accepted',
  384. },
  385. totals: {
  386. 'sum(quantity)': 28,
  387. },
  388. series: {
  389. 'sum(quantity)': [1, 2, 3, 4, 5, 6, 7],
  390. },
  391. },
  392. {
  393. by: {
  394. category: 'error',
  395. outcome: 'filtered',
  396. },
  397. totals: {
  398. 'sum(quantity)': 7,
  399. },
  400. series: {
  401. 'sum(quantity)': [1, 1, 1, 1, 1, 1, 1],
  402. },
  403. },
  404. {
  405. by: {
  406. category: 'error',
  407. outcome: 'rate_limited',
  408. },
  409. totals: {
  410. 'sum(quantity)': 14,
  411. },
  412. series: {
  413. 'sum(quantity)': [2, 2, 2, 2, 2, 2, 2],
  414. },
  415. },
  416. {
  417. by: {
  418. category: 'error',
  419. outcome: 'invalid',
  420. },
  421. totals: {
  422. 'sum(quantity)': 15,
  423. },
  424. series: {
  425. 'sum(quantity)': [2, 2, 2, 2, 2, 2, 3],
  426. },
  427. },
  428. {
  429. by: {
  430. category: 'error',
  431. outcome: 'client_discard',
  432. },
  433. totals: {
  434. 'sum(quantity)': 15,
  435. },
  436. series: {
  437. 'sum(quantity)': [2, 2, 2, 2, 2, 2, 3],
  438. },
  439. },
  440. ],
  441. },
  442. };
  443. }