eventsRequest.spec.jsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. import {Organization} from 'fixtures/js-stubs/organization';
  2. import {Project} from 'fixtures/js-stubs/project';
  3. import {mountWithTheme} from 'sentry-test/enzyme';
  4. import {render, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import {doEventsRequest} from 'sentry/actionCreators/events';
  6. import EventsRequest from 'sentry/components/charts/eventsRequest';
  7. const COUNT_OBJ = {
  8. count: 123,
  9. };
  10. jest.mock('sentry/actionCreators/events', () => ({
  11. doEventsRequest: jest.fn(),
  12. }));
  13. describe('EventsRequest', function () {
  14. const project = Project();
  15. const organization = Organization();
  16. const mock = jest.fn(() => null);
  17. const DEFAULTS = {
  18. api: new MockApiClient(),
  19. projects: [parseInt(project.id, 10)],
  20. environments: [],
  21. period: '24h',
  22. organization,
  23. tag: 'release',
  24. includePrevious: false,
  25. includeTimeseries: true,
  26. };
  27. let wrapper;
  28. describe('with props changes', function () {
  29. beforeAll(function () {
  30. doEventsRequest.mockImplementation(() =>
  31. Promise.resolve({
  32. data: [[new Date(), [COUNT_OBJ]]],
  33. })
  34. );
  35. wrapper = mountWithTheme(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);
  36. });
  37. it('makes requests', function () {
  38. expect(mock).toHaveBeenNthCalledWith(
  39. 1,
  40. expect.objectContaining({
  41. loading: true,
  42. })
  43. );
  44. expect(mock).toHaveBeenLastCalledWith(
  45. expect.objectContaining({
  46. loading: false,
  47. timeseriesData: [
  48. {
  49. seriesName: expect.anything(),
  50. data: [
  51. expect.objectContaining({
  52. name: expect.any(Number),
  53. value: 123,
  54. }),
  55. ],
  56. },
  57. ],
  58. originalTimeseriesData: [[expect.anything(), expect.anything()]],
  59. })
  60. );
  61. expect(doEventsRequest).toHaveBeenCalled();
  62. });
  63. it('makes a new request if projects prop changes', async function () {
  64. doEventsRequest.mockClear();
  65. wrapper.setProps({projects: [123]});
  66. await tick();
  67. wrapper.update();
  68. expect(doEventsRequest).toHaveBeenCalledWith(
  69. expect.anything(),
  70. expect.objectContaining({
  71. projects: [123],
  72. })
  73. );
  74. });
  75. it('makes a new request if environments prop changes', async function () {
  76. doEventsRequest.mockClear();
  77. wrapper.setProps({environments: ['dev']});
  78. await tick();
  79. wrapper.update();
  80. expect(doEventsRequest).toHaveBeenCalledWith(
  81. expect.anything(),
  82. expect.objectContaining({
  83. environments: ['dev'],
  84. })
  85. );
  86. });
  87. it('makes a new request if period prop changes', async function () {
  88. doEventsRequest.mockClear();
  89. wrapper.setProps({period: '7d'});
  90. await tick();
  91. wrapper.update();
  92. expect(doEventsRequest).toHaveBeenCalledWith(
  93. expect.anything(),
  94. expect.objectContaining({
  95. period: '7d',
  96. })
  97. );
  98. });
  99. });
  100. describe('transforms', function () {
  101. beforeEach(function () {
  102. doEventsRequest.mockClear();
  103. });
  104. it('expands period in query if `includePrevious`', async function () {
  105. doEventsRequest.mockImplementation(() =>
  106. Promise.resolve({
  107. data: [
  108. [
  109. new Date(),
  110. [
  111. {...COUNT_OBJ, count: 321},
  112. {...COUNT_OBJ, count: 79},
  113. ],
  114. ],
  115. [new Date(), [COUNT_OBJ]],
  116. ],
  117. })
  118. );
  119. wrapper = mountWithTheme(
  120. <EventsRequest {...DEFAULTS} includePrevious>
  121. {mock}
  122. </EventsRequest>
  123. );
  124. await tick();
  125. wrapper.update();
  126. // actionCreator handles expanding the period when calling the API
  127. expect(doEventsRequest).toHaveBeenCalledWith(
  128. expect.anything(),
  129. expect.objectContaining({
  130. period: '24h',
  131. })
  132. );
  133. expect(mock).toHaveBeenLastCalledWith(
  134. expect.objectContaining({
  135. loading: false,
  136. allTimeseriesData: [
  137. [
  138. expect.anything(),
  139. [
  140. expect.objectContaining({count: 321}),
  141. expect.objectContaining({count: 79}),
  142. ],
  143. ],
  144. [expect.anything(), [expect.objectContaining({count: 123})]],
  145. ],
  146. timeseriesData: [
  147. {
  148. seriesName: expect.anything(),
  149. data: [
  150. expect.objectContaining({
  151. name: expect.anything(),
  152. value: 123,
  153. }),
  154. ],
  155. },
  156. ],
  157. previousTimeseriesData: [
  158. expect.objectContaining({
  159. seriesName: 'Previous',
  160. data: [
  161. expect.objectContaining({
  162. name: expect.anything(),
  163. value: 400,
  164. }),
  165. ],
  166. }),
  167. ],
  168. originalTimeseriesData: [
  169. [expect.anything(), [expect.objectContaining({count: 123})]],
  170. ],
  171. originalPreviousTimeseriesData: [
  172. [
  173. expect.anything(),
  174. [
  175. expect.objectContaining({count: 321}),
  176. expect.objectContaining({count: 79}),
  177. ],
  178. ],
  179. ],
  180. })
  181. );
  182. });
  183. it('expands multiple periods in query if `includePrevious`', async function () {
  184. doEventsRequest.mockImplementation(() =>
  185. Promise.resolve({
  186. 'count()': {
  187. data: [
  188. [
  189. new Date(),
  190. [
  191. {...COUNT_OBJ, count: 321},
  192. {...COUNT_OBJ, count: 79},
  193. ],
  194. ],
  195. [new Date(), [COUNT_OBJ]],
  196. ],
  197. },
  198. 'failure_count()': {
  199. data: [
  200. [
  201. new Date(),
  202. [
  203. {...COUNT_OBJ, count: 421},
  204. {...COUNT_OBJ, count: 79},
  205. ],
  206. ],
  207. [new Date(), [COUNT_OBJ]],
  208. ],
  209. },
  210. })
  211. );
  212. const multiYOptions = {
  213. yAxis: ['count()', 'failure_count()'],
  214. previousSeriesNames: ['previous count()', 'previous failure_count()'],
  215. };
  216. wrapper = mountWithTheme(
  217. <EventsRequest {...DEFAULTS} {...multiYOptions} includePrevious>
  218. {mock}
  219. </EventsRequest>
  220. );
  221. await tick();
  222. wrapper.update();
  223. // actionCreator handles expanding the period when calling the API
  224. expect(doEventsRequest).toHaveBeenCalledWith(
  225. expect.anything(),
  226. expect.objectContaining({
  227. period: '24h',
  228. })
  229. );
  230. expect(mock).toHaveBeenLastCalledWith(
  231. expect.objectContaining({
  232. loading: false,
  233. yAxis: ['count()', 'failure_count()'],
  234. previousSeriesNames: ['previous count()', 'previous failure_count()'],
  235. results: [
  236. expect.objectContaining({
  237. data: [expect.objectContaining({name: expect.anything(), value: 123})],
  238. seriesName: 'count()',
  239. }),
  240. expect.objectContaining({
  241. data: [expect.objectContaining({name: expect.anything(), value: 123})],
  242. seriesName: 'failure_count()',
  243. }),
  244. ],
  245. previousTimeseriesData: [
  246. expect.objectContaining({
  247. data: [expect.objectContaining({name: expect.anything(), value: 400})],
  248. seriesName: 'previous count()',
  249. stack: 'previous',
  250. }),
  251. expect.objectContaining({
  252. data: [expect.objectContaining({name: expect.anything(), value: 500})],
  253. seriesName: 'previous failure_count()',
  254. stack: 'previous',
  255. }),
  256. ],
  257. })
  258. );
  259. });
  260. it('aggregates counts per timestamp only when `includeTimeAggregation` prop is true', async function () {
  261. doEventsRequest.mockImplementation(() =>
  262. Promise.resolve({
  263. data: [[new Date(), [COUNT_OBJ, {...COUNT_OBJ, count: 100}]]],
  264. })
  265. );
  266. wrapper = mountWithTheme(
  267. <EventsRequest {...DEFAULTS} includeTimeseries>
  268. {mock}
  269. </EventsRequest>
  270. );
  271. await tick();
  272. wrapper.update();
  273. expect(mock).toHaveBeenLastCalledWith(
  274. expect.objectContaining({
  275. timeAggregatedData: {},
  276. })
  277. );
  278. wrapper.setProps({
  279. includeTimeAggregation: true,
  280. timeAggregationSeriesName: 'aggregated series',
  281. });
  282. await tick();
  283. wrapper.update();
  284. expect(mock).toHaveBeenLastCalledWith(
  285. expect.objectContaining({
  286. timeAggregatedData: {
  287. seriesName: 'aggregated series',
  288. data: [{name: expect.anything(), value: 223}],
  289. },
  290. })
  291. );
  292. });
  293. it('aggregates all counts per timestamp when category name identical', async function () {
  294. doEventsRequest.mockImplementation(() =>
  295. Promise.resolve({
  296. data: [[new Date(), [COUNT_OBJ, {...COUNT_OBJ, count: 100}]]],
  297. })
  298. );
  299. wrapper = mountWithTheme(
  300. <EventsRequest {...DEFAULTS} includeTimeseries>
  301. {mock}
  302. </EventsRequest>
  303. );
  304. await tick();
  305. wrapper.update();
  306. expect(mock).toHaveBeenLastCalledWith(
  307. expect.objectContaining({
  308. timeAggregatedData: {},
  309. })
  310. );
  311. wrapper.setProps({
  312. includeTimeAggregation: true,
  313. timeAggregationSeriesName: 'aggregated series',
  314. });
  315. await tick();
  316. wrapper.update();
  317. expect(mock).toHaveBeenLastCalledWith(
  318. expect.objectContaining({
  319. timeAggregatedData: {
  320. seriesName: 'aggregated series',
  321. data: [{name: expect.anything(), value: 223}],
  322. },
  323. })
  324. );
  325. });
  326. });
  327. describe('yAxis', function () {
  328. beforeEach(function () {
  329. doEventsRequest.mockClear();
  330. });
  331. it('supports yAxis', async function () {
  332. doEventsRequest.mockImplementation(() =>
  333. Promise.resolve({
  334. data: [
  335. [
  336. new Date(),
  337. [
  338. {...COUNT_OBJ, count: 321},
  339. {...COUNT_OBJ, count: 79},
  340. ],
  341. ],
  342. [new Date(), [COUNT_OBJ]],
  343. ],
  344. })
  345. );
  346. wrapper = mountWithTheme(
  347. <EventsRequest {...DEFAULTS} includePrevious yAxis="apdex()">
  348. {mock}
  349. </EventsRequest>
  350. );
  351. await tick();
  352. wrapper.update();
  353. expect(mock).toHaveBeenLastCalledWith(
  354. expect.objectContaining({
  355. loading: false,
  356. allTimeseriesData: [
  357. [
  358. expect.anything(),
  359. [
  360. expect.objectContaining({count: 321}),
  361. expect.objectContaining({count: 79}),
  362. ],
  363. ],
  364. [expect.anything(), [expect.objectContaining({count: 123})]],
  365. ],
  366. timeseriesData: [
  367. {
  368. seriesName: expect.anything(),
  369. data: [
  370. expect.objectContaining({
  371. name: expect.anything(),
  372. value: 123,
  373. }),
  374. ],
  375. },
  376. ],
  377. previousTimeseriesData: [
  378. expect.objectContaining({
  379. seriesName: 'Previous',
  380. data: [
  381. expect.objectContaining({
  382. name: expect.anything(),
  383. value: 400,
  384. }),
  385. ],
  386. }),
  387. ],
  388. originalTimeseriesData: [
  389. [expect.anything(), [expect.objectContaining({count: 123})]],
  390. ],
  391. originalPreviousTimeseriesData: [
  392. [
  393. expect.anything(),
  394. [
  395. expect.objectContaining({count: 321}),
  396. expect.objectContaining({count: 79}),
  397. ],
  398. ],
  399. ],
  400. })
  401. );
  402. });
  403. it('supports multiple yAxis', async function () {
  404. doEventsRequest.mockImplementation(() =>
  405. Promise.resolve({
  406. 'epm()': {
  407. data: [
  408. [
  409. new Date(),
  410. [
  411. {...COUNT_OBJ, count: 321},
  412. {...COUNT_OBJ, count: 79},
  413. ],
  414. ],
  415. [new Date(), [COUNT_OBJ]],
  416. ],
  417. },
  418. 'apdex()': {
  419. data: [
  420. [
  421. new Date(),
  422. [
  423. {...COUNT_OBJ, count: 321},
  424. {...COUNT_OBJ, count: 79},
  425. ],
  426. ],
  427. [new Date(), [COUNT_OBJ]],
  428. ],
  429. },
  430. })
  431. );
  432. wrapper = mountWithTheme(
  433. <EventsRequest {...DEFAULTS} yAxis={['apdex()', 'epm()']}>
  434. {mock}
  435. </EventsRequest>
  436. );
  437. await tick();
  438. wrapper.update();
  439. const generateExpected = name => {
  440. return {
  441. seriesName: name,
  442. data: [
  443. {name: expect.anything(), value: 400},
  444. {name: expect.anything(), value: 123},
  445. ],
  446. };
  447. };
  448. expect(mock).toHaveBeenLastCalledWith(
  449. expect.objectContaining({
  450. loading: false,
  451. results: [generateExpected('epm()'), generateExpected('apdex()')],
  452. })
  453. );
  454. });
  455. });
  456. describe('topEvents', function () {
  457. beforeEach(function () {
  458. doEventsRequest.mockClear();
  459. });
  460. it('supports topEvents parameter', async function () {
  461. doEventsRequest.mockImplementation(() =>
  462. Promise.resolve({
  463. 'project1,error': {
  464. data: [
  465. [
  466. new Date(),
  467. [
  468. {...COUNT_OBJ, count: 321},
  469. {...COUNT_OBJ, count: 79},
  470. ],
  471. ],
  472. [new Date(), [COUNT_OBJ]],
  473. ],
  474. },
  475. 'project1,warning': {
  476. data: [
  477. [
  478. new Date(),
  479. [
  480. {...COUNT_OBJ, count: 321},
  481. {...COUNT_OBJ, count: 79},
  482. ],
  483. ],
  484. [new Date(), [COUNT_OBJ]],
  485. ],
  486. },
  487. })
  488. );
  489. wrapper = mountWithTheme(
  490. <EventsRequest {...DEFAULTS} field={['project', 'level']} topEvents={2}>
  491. {mock}
  492. </EventsRequest>
  493. );
  494. await tick();
  495. wrapper.update();
  496. const generateExpected = name => {
  497. return {
  498. seriesName: name,
  499. data: [
  500. {name: expect.anything(), value: 400},
  501. {name: expect.anything(), value: 123},
  502. ],
  503. };
  504. };
  505. expect(mock).toHaveBeenLastCalledWith(
  506. expect.objectContaining({
  507. loading: false,
  508. results: [
  509. generateExpected('project1,error'),
  510. generateExpected('project1,warning'),
  511. ],
  512. })
  513. );
  514. });
  515. });
  516. describe('out of retention', function () {
  517. beforeEach(function () {
  518. doEventsRequest.mockClear();
  519. });
  520. it('does not make request', function () {
  521. wrapper = mountWithTheme(
  522. <EventsRequest {...DEFAULTS} expired>
  523. {mock}
  524. </EventsRequest>
  525. );
  526. expect(doEventsRequest).not.toHaveBeenCalled();
  527. });
  528. it('errors', function () {
  529. wrapper = mountWithTheme(
  530. <EventsRequest {...DEFAULTS} expired>
  531. {mock}
  532. </EventsRequest>
  533. );
  534. expect(mock).toHaveBeenLastCalledWith(
  535. expect.objectContaining({
  536. expired: true,
  537. errored: true,
  538. })
  539. );
  540. });
  541. });
  542. describe('timeframe', function () {
  543. beforeEach(function () {
  544. doEventsRequest.mockClear();
  545. });
  546. it('passes query timeframe start and end to the child if supplied by timeseriesData', async function () {
  547. doEventsRequest.mockImplementation(() =>
  548. Promise.resolve({
  549. p95: {
  550. data: [[new Date(), [COUNT_OBJ]]],
  551. start: 1627402280,
  552. end: 1627402398,
  553. },
  554. })
  555. );
  556. wrapper = mountWithTheme(<EventsRequest {...DEFAULTS}>{mock}</EventsRequest>);
  557. await tick();
  558. wrapper.update();
  559. expect(mock).toHaveBeenLastCalledWith(
  560. expect.objectContaining({
  561. timeframe: {
  562. start: 1627402280000,
  563. end: 1627402398000,
  564. },
  565. })
  566. );
  567. });
  568. });
  569. describe('custom performance metrics', function () {
  570. beforeEach(function () {
  571. doEventsRequest.mockClear();
  572. });
  573. it('passes timeseriesResultTypes to child', async function () {
  574. doEventsRequest.mockImplementation(() =>
  575. Promise.resolve({
  576. data: [[new Date(), [COUNT_OBJ]]],
  577. start: 1627402280,
  578. end: 1627402398,
  579. meta: {
  580. fields: {
  581. p95_measurements_custom: 'size',
  582. },
  583. units: {
  584. p95_measurements_custom: 'kibibyte',
  585. },
  586. },
  587. })
  588. );
  589. render(
  590. <EventsRequest {...DEFAULTS} yAxis="p95(measurements.custom)">
  591. {mock}
  592. </EventsRequest>
  593. );
  594. await waitFor(() =>
  595. expect(mock).toHaveBeenLastCalledWith(
  596. expect.objectContaining({
  597. timeseriesResultsTypes: {'p95(measurements.custom)': 'size'},
  598. })
  599. )
  600. );
  601. });
  602. it('scales timeseries values according to unit meta', async function () {
  603. doEventsRequest.mockImplementation(() =>
  604. Promise.resolve({
  605. data: [[new Date(), [COUNT_OBJ]]],
  606. start: 1627402280,
  607. end: 1627402398,
  608. meta: {
  609. fields: {
  610. p95_measurements_custom: 'size',
  611. },
  612. units: {
  613. p95_measurements_custom: 'mebibyte',
  614. },
  615. },
  616. })
  617. );
  618. render(
  619. <EventsRequest
  620. {...DEFAULTS}
  621. yAxis="p95(measurements.custom)"
  622. currentSeriesNames={['p95(measurements.custom)']}
  623. >
  624. {mock}
  625. </EventsRequest>
  626. );
  627. await waitFor(() =>
  628. expect(mock).toHaveBeenLastCalledWith(
  629. expect.objectContaining({
  630. timeseriesData: [
  631. {
  632. data: [{name: 1508208080000000, value: 128974848}],
  633. seriesName: 'p95(measurements.custom)',
  634. },
  635. ],
  636. })
  637. )
  638. );
  639. });
  640. });
  641. });