eventsRequest.spec.jsx 18 KB

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