eventsRequest.spec.tsx 19 KB

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