eventsRequest.spec.tsx 19 KB

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