widgetQueries.spec.jsx 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. import {initializeOrg} from 'sentry-test/initializeOrg';
  2. import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  3. import {Client} from 'sentry/api';
  4. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  5. import {DashboardFilterKeys} from 'sentry/views/dashboardsV2/types';
  6. import {DashboardsMEPContext} from 'sentry/views/dashboardsV2/widgetCard/dashboardsMEPContext';
  7. import WidgetQueries, {
  8. flattenMultiSeriesDataWithGrouping,
  9. } from 'sentry/views/dashboardsV2/widgetCard/widgetQueries';
  10. import {OrganizationContext} from 'sentry/views/organizationContext';
  11. describe('Dashboards > WidgetQueries', function () {
  12. const initialData = initializeOrg({
  13. organization: TestStubs.Organization(),
  14. });
  15. const renderWithProviders = (component, context) =>
  16. render(
  17. <OrganizationContext.Provider value={initialData.organization}>
  18. <MEPSettingProvider forceTransactions={false}>{component}</MEPSettingProvider>
  19. </OrganizationContext.Provider>,
  20. context
  21. );
  22. const multipleQueryWidget = {
  23. title: 'Errors',
  24. interval: '5m',
  25. displayType: 'line',
  26. queries: [
  27. {
  28. conditions: 'event.type:error',
  29. fields: ['count()'],
  30. aggregates: ['count()'],
  31. columns: [],
  32. name: 'errors',
  33. orderby: '',
  34. },
  35. {
  36. conditions: 'event.type:default',
  37. fields: ['count()'],
  38. aggregates: ['count()'],
  39. columns: [],
  40. name: 'default',
  41. orderby: '',
  42. },
  43. ],
  44. };
  45. const singleQueryWidget = {
  46. title: 'Errors',
  47. interval: '5m',
  48. displayType: 'line',
  49. queries: [
  50. {
  51. conditions: 'event.type:error',
  52. fields: ['count()'],
  53. aggregates: ['count()'],
  54. columns: [],
  55. name: 'errors',
  56. orderby: '',
  57. },
  58. ],
  59. };
  60. const tableWidget = {
  61. title: 'SDK',
  62. interval: '5m',
  63. displayType: 'table',
  64. queries: [
  65. {
  66. conditions: 'event.type:error',
  67. fields: ['sdk.name'],
  68. aggregates: [],
  69. columns: ['sdk.name'],
  70. name: 'sdk',
  71. orderby: '',
  72. },
  73. ],
  74. };
  75. const selection = {
  76. projects: [1],
  77. environments: ['prod'],
  78. datetime: {
  79. period: '14d',
  80. orderby: '',
  81. },
  82. };
  83. const api = new Client();
  84. afterEach(function () {
  85. MockApiClient.clearMockResponses();
  86. });
  87. it('can send multiple API requests', async function () {
  88. const errorMock = MockApiClient.addMockResponse({
  89. url: '/organizations/org-slug/events-stats/',
  90. body: [],
  91. match: [MockApiClient.matchQuery({query: 'event.type:error'})],
  92. });
  93. const defaultMock = MockApiClient.addMockResponse({
  94. url: '/organizations/org-slug/events-stats/',
  95. body: [],
  96. match: [MockApiClient.matchQuery({query: 'event.type:default'})],
  97. });
  98. renderWithProviders(
  99. <WidgetQueries
  100. api={api}
  101. widget={multipleQueryWidget}
  102. organization={initialData.organization}
  103. selection={selection}
  104. >
  105. {() => <div data-test-id="child" />}
  106. </WidgetQueries>
  107. );
  108. // Child should be rendered and 2 requests should be sent.
  109. await screen.findByTestId('child');
  110. expect(errorMock).toHaveBeenCalledTimes(1);
  111. expect(defaultMock).toHaveBeenCalledTimes(1);
  112. });
  113. it('appends dashboard filters to events series request', async function () {
  114. const mock = MockApiClient.addMockResponse({
  115. url: '/organizations/org-slug/events-stats/',
  116. body: [],
  117. });
  118. renderWithProviders(
  119. <WidgetQueries
  120. api={api}
  121. widget={singleQueryWidget}
  122. organization={initialData.organization}
  123. selection={selection}
  124. dashboardFilters={{[DashboardFilterKeys.RELEASE]: ['abc@1.2.0', 'abc@1.3.0']}}
  125. >
  126. {() => <div data-test-id="child" />}
  127. </WidgetQueries>
  128. );
  129. await screen.findByTestId('child');
  130. expect(mock).toHaveBeenCalledWith(
  131. '/organizations/org-slug/events-stats/',
  132. expect.objectContaining({
  133. query: expect.objectContaining({
  134. query: 'event.type:error release:[abc@1.2.0,abc@1.3.0] ',
  135. }),
  136. })
  137. );
  138. });
  139. it('appends dashboard filters to events table request', async function () {
  140. const mock = MockApiClient.addMockResponse({
  141. url: '/organizations/org-slug/eventsv2/',
  142. body: [],
  143. });
  144. renderWithProviders(
  145. <WidgetQueries
  146. api={api}
  147. widget={tableWidget}
  148. organization={initialData.organization}
  149. selection={selection}
  150. dashboardFilters={{[DashboardFilterKeys.RELEASE]: ['abc@1.3.0']}}
  151. >
  152. {() => <div data-test-id="child" />}
  153. </WidgetQueries>
  154. );
  155. await screen.findByTestId('child');
  156. expect(mock).toHaveBeenCalledWith(
  157. '/organizations/org-slug/eventsv2/',
  158. expect.objectContaining({
  159. query: expect.objectContaining({
  160. query: 'event.type:error release:abc@1.3.0 ',
  161. }),
  162. })
  163. );
  164. });
  165. it('sets errorMessage when the first request fails', async function () {
  166. const okMock = MockApiClient.addMockResponse({
  167. url: '/organizations/org-slug/events-stats/',
  168. match: [MockApiClient.matchQuery({query: 'event.type:error'})],
  169. body: [],
  170. });
  171. const failMock = MockApiClient.addMockResponse({
  172. url: '/organizations/org-slug/events-stats/',
  173. statusCode: 400,
  174. body: {detail: 'Bad request data'},
  175. match: [MockApiClient.matchQuery({query: 'event.type:default'})],
  176. });
  177. let error = '';
  178. renderWithProviders(
  179. <WidgetQueries
  180. api={api}
  181. widget={multipleQueryWidget}
  182. organization={initialData.organization}
  183. selection={selection}
  184. >
  185. {({errorMessage}) => {
  186. error = errorMessage;
  187. return <div data-test-id="child" />;
  188. }}
  189. </WidgetQueries>
  190. );
  191. // Child should be rendered and 2 requests should be sent.
  192. await screen.findByTestId('child');
  193. expect(okMock).toHaveBeenCalledTimes(1);
  194. expect(failMock).toHaveBeenCalledTimes(1);
  195. expect(error).toEqual('Bad request data');
  196. });
  197. it('adjusts interval based on date window', async function () {
  198. const errorMock = MockApiClient.addMockResponse({
  199. url: '/organizations/org-slug/events-stats/',
  200. body: [],
  201. });
  202. const widget = {...singleQueryWidget, interval: '1m'};
  203. const longSelection = {
  204. projects: [1],
  205. environments: ['prod', 'dev'],
  206. datetime: {
  207. period: '90d',
  208. },
  209. };
  210. renderWithProviders(
  211. <WidgetQueries
  212. api={api}
  213. widget={widget}
  214. organization={initialData.organization}
  215. selection={longSelection}
  216. >
  217. {() => <div data-test-id="child" />}
  218. </WidgetQueries>
  219. );
  220. // Child should be rendered and interval bumped up.
  221. await screen.findByTestId('child');
  222. expect(errorMock).toHaveBeenCalledTimes(1);
  223. expect(errorMock).toHaveBeenCalledWith(
  224. '/organizations/org-slug/events-stats/',
  225. expect.objectContaining({
  226. query: expect.objectContaining({
  227. interval: '4h',
  228. statsPeriod: '90d',
  229. environment: ['prod', 'dev'],
  230. project: [1],
  231. }),
  232. })
  233. );
  234. });
  235. it('adjusts interval based on date window 14d', async function () {
  236. const errorMock = MockApiClient.addMockResponse({
  237. url: '/organizations/org-slug/events-stats/',
  238. body: [],
  239. });
  240. const widget = {...singleQueryWidget, interval: '1m'};
  241. renderWithProviders(
  242. <WidgetQueries
  243. api={api}
  244. widget={widget}
  245. organization={initialData.organization}
  246. selection={selection}
  247. >
  248. {() => <div data-test-id="child" />}
  249. </WidgetQueries>
  250. );
  251. // Child should be rendered and interval bumped up.
  252. await screen.findByTestId('child');
  253. expect(errorMock).toHaveBeenCalledTimes(1);
  254. expect(errorMock).toHaveBeenCalledWith(
  255. '/organizations/org-slug/events-stats/',
  256. expect.objectContaining({
  257. query: expect.objectContaining({interval: '30m'}),
  258. })
  259. );
  260. });
  261. it('can send table result queries', async function () {
  262. const tableMock = MockApiClient.addMockResponse({
  263. url: '/organizations/org-slug/eventsv2/',
  264. body: {
  265. meta: {'sdk.name': 'string'},
  266. data: [{'sdk.name': 'python'}],
  267. },
  268. });
  269. let childProps = undefined;
  270. renderWithProviders(
  271. <WidgetQueries
  272. api={api}
  273. widget={tableWidget}
  274. organization={initialData.organization}
  275. selection={selection}
  276. >
  277. {props => {
  278. childProps = props;
  279. return <div data-test-id="child" />;
  280. }}
  281. </WidgetQueries>
  282. );
  283. // Child should be rendered and 1 requests should be sent.
  284. await screen.findByTestId('child');
  285. expect(tableMock).toHaveBeenCalledTimes(1);
  286. expect(tableMock).toHaveBeenCalledWith(
  287. '/organizations/org-slug/eventsv2/',
  288. expect.objectContaining({
  289. query: expect.objectContaining({
  290. query: 'event.type:error',
  291. field: ['sdk.name'],
  292. statsPeriod: '14d',
  293. environment: ['prod'],
  294. project: [1],
  295. }),
  296. })
  297. );
  298. expect(childProps.timeseriesResults).toBeUndefined();
  299. expect(childProps.tableResults[0].data).toHaveLength(1);
  300. expect(childProps.tableResults[0].meta).toBeDefined();
  301. });
  302. it('can send multiple table queries', async function () {
  303. const firstQuery = MockApiClient.addMockResponse({
  304. url: '/organizations/org-slug/eventsv2/',
  305. body: {
  306. meta: {'sdk.name': 'string'},
  307. data: [{'sdk.name': 'python'}],
  308. },
  309. match: [MockApiClient.matchQuery({query: 'event.type:error'})],
  310. });
  311. const secondQuery = MockApiClient.addMockResponse({
  312. url: '/organizations/org-slug/eventsv2/',
  313. body: {
  314. meta: {title: 'string'},
  315. data: [{title: 'ValueError'}],
  316. },
  317. match: [MockApiClient.matchQuery({query: 'title:ValueError'})],
  318. });
  319. const widget = {
  320. title: 'SDK',
  321. interval: '5m',
  322. displayType: 'table',
  323. queries: [
  324. {
  325. conditions: 'event.type:error',
  326. fields: ['sdk.name'],
  327. aggregates: [],
  328. columns: ['sdk.name'],
  329. name: 'sdk',
  330. orderby: '',
  331. },
  332. {
  333. conditions: 'title:ValueError',
  334. fields: ['title'],
  335. aggregates: [],
  336. columns: ['sdk.name'],
  337. name: 'title',
  338. orderby: '',
  339. },
  340. ],
  341. };
  342. let childProps = undefined;
  343. renderWithProviders(
  344. <WidgetQueries
  345. api={api}
  346. widget={widget}
  347. organization={initialData.organization}
  348. selection={selection}
  349. >
  350. {props => {
  351. childProps = props;
  352. return <div data-test-id="child" />;
  353. }}
  354. </WidgetQueries>
  355. );
  356. // Child should be rendered and 2 requests should be sent.
  357. await screen.findByTestId('child');
  358. expect(firstQuery).toHaveBeenCalledTimes(1);
  359. expect(secondQuery).toHaveBeenCalledTimes(1);
  360. expect(childProps.tableResults).toHaveLength(2);
  361. expect(childProps.tableResults[0].data[0]['sdk.name']).toBeDefined();
  362. expect(childProps.tableResults[1].data[0].title).toBeDefined();
  363. });
  364. it('can send big number result queries', async function () {
  365. const tableMock = MockApiClient.addMockResponse({
  366. url: '/organizations/org-slug/eventsv2/',
  367. body: {
  368. meta: {'sdk.name': 'string'},
  369. data: [{'sdk.name': 'python'}],
  370. },
  371. });
  372. let childProps = undefined;
  373. renderWithProviders(
  374. <WidgetQueries
  375. api={api}
  376. widget={{
  377. title: 'SDK',
  378. interval: '5m',
  379. displayType: 'big_number',
  380. queries: [
  381. {
  382. conditions: 'event.type:error',
  383. fields: ['sdk.name'],
  384. aggregates: [],
  385. columns: ['sdk.name'],
  386. name: 'sdk',
  387. orderby: '',
  388. },
  389. ],
  390. }}
  391. organization={initialData.organization}
  392. selection={selection}
  393. >
  394. {props => {
  395. childProps = props;
  396. return <div data-test-id="child" />;
  397. }}
  398. </WidgetQueries>
  399. );
  400. // Child should be rendered and 1 requests should be sent.
  401. await screen.findByTestId('child');
  402. expect(tableMock).toHaveBeenCalledTimes(1);
  403. expect(tableMock).toHaveBeenCalledWith(
  404. '/organizations/org-slug/eventsv2/',
  405. expect.objectContaining({
  406. query: expect.objectContaining({
  407. referrer: 'api.dashboards.bignumberwidget',
  408. query: 'event.type:error',
  409. field: ['sdk.name'],
  410. statsPeriod: '14d',
  411. environment: ['prod'],
  412. project: [1],
  413. }),
  414. })
  415. );
  416. expect(childProps.timeseriesResults).toBeUndefined();
  417. expect(childProps.tableResults[0].data).toHaveLength(1);
  418. expect(childProps.tableResults[0].meta).toBeDefined();
  419. });
  420. it('can send world map result queries', async function () {
  421. const tableMock = MockApiClient.addMockResponse({
  422. url: '/organizations/org-slug/events-geo/',
  423. body: {
  424. meta: {'sdk.name': 'string'},
  425. data: [{'sdk.name': 'python'}],
  426. },
  427. });
  428. let childProps = undefined;
  429. renderWithProviders(
  430. <WidgetQueries
  431. api={api}
  432. widget={{
  433. title: 'SDK',
  434. interval: '5m',
  435. displayType: 'world_map',
  436. queries: [
  437. {
  438. conditions: 'event.type:error',
  439. fields: ['count()'],
  440. aggregates: [],
  441. columns: ['count()'],
  442. name: 'sdk',
  443. orderby: '',
  444. },
  445. ],
  446. }}
  447. organization={initialData.organization}
  448. selection={selection}
  449. >
  450. {props => {
  451. childProps = props;
  452. return <div data-test-id="child" />;
  453. }}
  454. </WidgetQueries>
  455. );
  456. // Child should be rendered and 1 requests should be sent.
  457. await screen.findByTestId('child');
  458. expect(tableMock).toHaveBeenCalledTimes(1);
  459. expect(tableMock).toHaveBeenCalledWith(
  460. '/organizations/org-slug/events-geo/',
  461. expect.objectContaining({
  462. query: expect.objectContaining({
  463. referrer: 'api.dashboards.worldmapwidget',
  464. query: 'event.type:error',
  465. field: ['count()'],
  466. statsPeriod: '14d',
  467. environment: ['prod'],
  468. project: [1],
  469. }),
  470. })
  471. );
  472. expect(childProps.timeseriesResults).toBeUndefined();
  473. expect(childProps.tableResults[0].data).toHaveLength(1);
  474. expect(childProps.tableResults[0].meta).toBeDefined();
  475. });
  476. it('stops loading state once all queries finish even if some fail', async function () {
  477. const firstQuery = MockApiClient.addMockResponse({
  478. statusCode: 500,
  479. url: '/organizations/org-slug/eventsv2/',
  480. body: {detail: 'it didnt work'},
  481. match: [MockApiClient.matchQuery({query: 'event.type:error'})],
  482. });
  483. const secondQuery = MockApiClient.addMockResponse({
  484. url: '/organizations/org-slug/eventsv2/',
  485. body: {
  486. meta: {title: 'string'},
  487. data: [{title: 'ValueError'}],
  488. },
  489. match: [MockApiClient.matchQuery({query: 'title:ValueError'})],
  490. });
  491. const widget = {
  492. title: 'SDK',
  493. interval: '5m',
  494. displayType: 'table',
  495. queries: [
  496. {
  497. conditions: 'event.type:error',
  498. fields: ['sdk.name'],
  499. aggregates: [],
  500. columns: ['sdk.name'],
  501. name: 'sdk',
  502. orderby: '',
  503. },
  504. {
  505. conditions: 'title:ValueError',
  506. fields: ['sdk.name'],
  507. aggregates: [],
  508. columns: ['sdk.name'],
  509. name: 'title',
  510. orderby: '',
  511. },
  512. ],
  513. };
  514. let childProps = undefined;
  515. renderWithProviders(
  516. <WidgetQueries
  517. api={api}
  518. widget={widget}
  519. organization={initialData.organization}
  520. selection={selection}
  521. >
  522. {props => {
  523. childProps = props;
  524. return <div data-test-id="child" />;
  525. }}
  526. </WidgetQueries>
  527. );
  528. // Child should be rendered and 2 requests should be sent.
  529. await screen.findByTestId('child');
  530. expect(firstQuery).toHaveBeenCalledTimes(1);
  531. expect(secondQuery).toHaveBeenCalledTimes(1);
  532. expect(childProps.loading).toEqual(false);
  533. });
  534. it('sets bar charts to 1d interval', async function () {
  535. const errorMock = MockApiClient.addMockResponse({
  536. url: '/organizations/org-slug/events-stats/',
  537. body: [],
  538. match: [MockApiClient.matchQuery({interval: '1d'})],
  539. });
  540. const barWidget = {
  541. ...singleQueryWidget,
  542. displayType: 'bar',
  543. // Should be ignored for bars.
  544. interval: '5m',
  545. };
  546. renderWithProviders(
  547. <WidgetQueries
  548. api={api}
  549. widget={barWidget}
  550. organization={initialData.organization}
  551. selection={selection}
  552. >
  553. {() => <div data-test-id="child" />}
  554. </WidgetQueries>
  555. );
  556. // Child should be rendered and 1 requests should be sent.
  557. await screen.findByTestId('child');
  558. expect(errorMock).toHaveBeenCalledTimes(1);
  559. });
  560. it('returns timeseriesResults in the same order as widgetQuery', async function () {
  561. MockApiClient.clearMockResponses();
  562. const defaultMock = MockApiClient.addMockResponse({
  563. url: '/organizations/org-slug/events-stats/',
  564. method: 'GET',
  565. body: {
  566. data: [
  567. [
  568. 1000,
  569. [
  570. {
  571. count: 100,
  572. },
  573. ],
  574. ],
  575. ],
  576. start: 1000,
  577. end: 2000,
  578. },
  579. match: [MockApiClient.matchQuery({query: 'event.type:default'})],
  580. });
  581. const errorMock = MockApiClient.addMockResponse({
  582. url: '/organizations/org-slug/events-stats/',
  583. method: 'GET',
  584. body: {
  585. data: [
  586. [
  587. 1000,
  588. [
  589. {
  590. count: 200,
  591. },
  592. ],
  593. ],
  594. ],
  595. start: 1000,
  596. end: 2000,
  597. },
  598. match: [MockApiClient.matchQuery({query: 'event.type:error'})],
  599. });
  600. const barWidget = {
  601. ...multipleQueryWidget,
  602. displayType: 'bar',
  603. // Should be ignored for bars.
  604. interval: '5m',
  605. };
  606. const child = jest.fn(() => <div data-test-id="child" />);
  607. renderWithProviders(
  608. <WidgetQueries
  609. api={api}
  610. widget={barWidget}
  611. organization={initialData.organization}
  612. selection={selection}
  613. >
  614. {child}
  615. </WidgetQueries>
  616. );
  617. await screen.findByTestId('child');
  618. expect(defaultMock).toHaveBeenCalledTimes(1);
  619. expect(errorMock).toHaveBeenCalledTimes(1);
  620. expect(child).toHaveBeenLastCalledWith(
  621. expect.objectContaining({
  622. timeseriesResults: [
  623. {data: [{name: 1000000, value: 200}], seriesName: 'errors : count()'},
  624. {data: [{name: 1000000, value: 100}], seriesName: 'default : count()'},
  625. ],
  626. })
  627. );
  628. });
  629. it('calls events-stats with 4h interval when interval buckets would exceed 66', async function () {
  630. const eventsStatsMock = MockApiClient.addMockResponse({
  631. url: '/organizations/org-slug/events-stats/',
  632. body: [],
  633. });
  634. const areaWidget = {
  635. ...singleQueryWidget,
  636. displayType: 'area',
  637. interval: '5m',
  638. };
  639. renderWithProviders(
  640. <WidgetQueries
  641. api={api}
  642. widget={areaWidget}
  643. organization={initialData.organization}
  644. selection={{
  645. ...selection,
  646. datetime: {
  647. period: '90d',
  648. },
  649. }}
  650. >
  651. {() => <div data-test-id="child" />}
  652. </WidgetQueries>
  653. );
  654. // Child should be rendered and 1 requests should be sent.
  655. await screen.findByTestId('child');
  656. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  657. expect(eventsStatsMock).toHaveBeenCalledWith(
  658. '/organizations/org-slug/events-stats/',
  659. expect.objectContaining({query: expect.objectContaining({interval: '4h'})})
  660. );
  661. });
  662. it('does not re-query events and sets name in widgets', async function () {
  663. const eventsStatsMock = MockApiClient.addMockResponse({
  664. url: '/organizations/org-slug/events-stats/',
  665. body: TestStubs.EventsStats(),
  666. });
  667. const lineWidget = {
  668. ...singleQueryWidget,
  669. displayType: 'line',
  670. interval: '5m',
  671. };
  672. let childProps;
  673. const {rerender} = renderWithProviders(
  674. <WidgetQueries
  675. api={api}
  676. widget={lineWidget}
  677. organization={initialData.organization}
  678. selection={selection}
  679. >
  680. {props => {
  681. childProps = props;
  682. return <div data-test-id="child" />;
  683. }}
  684. </WidgetQueries>
  685. );
  686. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  687. await waitFor(() => expect(childProps.loading).toEqual(false));
  688. // Simulate a re-render with a new query alias
  689. rerender(
  690. <OrganizationContext.Provider value={initialData.organization}>
  691. <MEPSettingProvider forceTransactions={false}>
  692. <WidgetQueries
  693. api={api}
  694. widget={{
  695. ...lineWidget,
  696. queries: [
  697. {
  698. conditions: 'event.type:error',
  699. fields: ['count()'],
  700. aggregates: ['count()'],
  701. columns: [],
  702. name: 'this query alias changed',
  703. orderby: '',
  704. },
  705. ],
  706. }}
  707. organization={initialData.organization}
  708. selection={selection}
  709. >
  710. {props => {
  711. childProps = props;
  712. return <div data-test-id="child" />;
  713. }}
  714. </WidgetQueries>
  715. </MEPSettingProvider>
  716. </OrganizationContext.Provider>
  717. );
  718. // Did not re-query
  719. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  720. expect(childProps.timeseriesResults[0].seriesName).toEqual(
  721. 'this query alias changed : count()'
  722. );
  723. });
  724. describe('multi-series grouped data', () => {
  725. const [START, END] = [1647399900, 1647399901];
  726. let mockCountData, mockCountUniqueData, mockRawResultData;
  727. beforeEach(() => {
  728. mockCountData = {
  729. start: START,
  730. end: END,
  731. data: [
  732. [START, [{'count()': 0}]],
  733. [END, [{'count()': 0}]],
  734. ],
  735. };
  736. mockCountUniqueData = {
  737. start: START,
  738. end: END,
  739. data: [
  740. [START, [{'count_unique()': 0}]],
  741. [END, [{'count_unique()': 0}]],
  742. ],
  743. };
  744. mockRawResultData = {
  745. local: {
  746. 'count()': mockCountData,
  747. 'count_unique()': mockCountUniqueData,
  748. order: 0,
  749. },
  750. prod: {
  751. 'count()': mockCountData,
  752. 'count_unique()': mockCountUniqueData,
  753. order: 1,
  754. },
  755. };
  756. });
  757. it('combines group name and aggregate names in grouped multi series data', () => {
  758. const actual = flattenMultiSeriesDataWithGrouping(mockRawResultData, '');
  759. expect(actual).toEqual([
  760. [
  761. 0,
  762. expect.objectContaining({
  763. seriesName: 'local : count()',
  764. data: expect.anything(),
  765. }),
  766. ],
  767. [
  768. 0,
  769. expect.objectContaining({
  770. seriesName: 'local : count_unique()',
  771. data: expect.anything(),
  772. }),
  773. ],
  774. [
  775. 1,
  776. expect.objectContaining({
  777. seriesName: 'prod : count()',
  778. data: expect.anything(),
  779. }),
  780. ],
  781. [
  782. 1,
  783. expect.objectContaining({
  784. seriesName: 'prod : count_unique()',
  785. data: expect.anything(),
  786. }),
  787. ],
  788. ]);
  789. });
  790. it('prefixes with a query alias when provided', () => {
  791. const actual = flattenMultiSeriesDataWithGrouping(mockRawResultData, 'Query 1');
  792. expect(actual).toEqual([
  793. [
  794. 0,
  795. expect.objectContaining({
  796. seriesName: 'Query 1 > local : count()',
  797. data: expect.anything(),
  798. }),
  799. ],
  800. [
  801. 0,
  802. expect.objectContaining({
  803. seriesName: 'Query 1 > local : count_unique()',
  804. data: expect.anything(),
  805. }),
  806. ],
  807. [
  808. 1,
  809. expect.objectContaining({
  810. seriesName: 'Query 1 > prod : count()',
  811. data: expect.anything(),
  812. }),
  813. ],
  814. [
  815. 1,
  816. expect.objectContaining({
  817. seriesName: 'Query 1 > prod : count_unique()',
  818. data: expect.anything(),
  819. }),
  820. ],
  821. ]);
  822. });
  823. });
  824. it('charts send metricsEnhanced requests', async function () {
  825. const {organization} = initialData;
  826. const mock = MockApiClient.addMockResponse({
  827. url: '/organizations/org-slug/events-stats/',
  828. body: {
  829. data: [
  830. [
  831. 1000,
  832. [
  833. {
  834. count: 100,
  835. },
  836. ],
  837. ],
  838. ],
  839. isMetricsData: false,
  840. start: 1000,
  841. end: 2000,
  842. },
  843. });
  844. const setIsMetricsMock = jest.fn();
  845. const children = jest.fn(() => <div />);
  846. renderWithProviders(
  847. <DashboardsMEPContext.Provider
  848. value={{
  849. isMetricsData: undefined,
  850. setIsMetricsData: setIsMetricsMock,
  851. }}
  852. >
  853. <WidgetQueries
  854. api={api}
  855. widget={singleQueryWidget}
  856. organization={{
  857. ...organization,
  858. features: [...organization.features, 'dashboards-mep'],
  859. }}
  860. selection={selection}
  861. >
  862. {children}
  863. </WidgetQueries>
  864. </DashboardsMEPContext.Provider>
  865. );
  866. expect(mock).toHaveBeenCalledWith(
  867. '/organizations/org-slug/events-stats/',
  868. expect.objectContaining({
  869. query: expect.objectContaining({dataset: 'metricsEnhanced'}),
  870. })
  871. );
  872. await waitFor(() => {
  873. expect(setIsMetricsMock).toHaveBeenCalledWith(false);
  874. });
  875. });
  876. it('tables send metricsEnhanced requests', async function () {
  877. const {organization} = initialData;
  878. const mock = MockApiClient.addMockResponse({
  879. url: '/organizations/org-slug/eventsv2/',
  880. body: {
  881. meta: {title: 'string', isMetricsData: true},
  882. data: [{title: 'ValueError'}],
  883. },
  884. });
  885. const setIsMetricsMock = jest.fn();
  886. const children = jest.fn(() => <div />);
  887. renderWithProviders(
  888. <DashboardsMEPContext.Provider
  889. value={{
  890. isMetricsData: undefined,
  891. setIsMetricsData: setIsMetricsMock,
  892. }}
  893. >
  894. <WidgetQueries
  895. api={api}
  896. widget={{...singleQueryWidget, displayType: 'table'}}
  897. organization={{
  898. ...organization,
  899. features: [...organization.features, 'dashboards-mep'],
  900. }}
  901. selection={selection}
  902. >
  903. {children}
  904. </WidgetQueries>
  905. </DashboardsMEPContext.Provider>
  906. );
  907. expect(mock).toHaveBeenCalledWith(
  908. '/organizations/org-slug/eventsv2/',
  909. expect.objectContaining({
  910. query: expect.objectContaining({dataset: 'metricsEnhanced'}),
  911. })
  912. );
  913. await waitFor(() => {
  914. expect(setIsMetricsMock).toHaveBeenCalledWith(true);
  915. });
  916. });
  917. it('does not inject equation aliases for top N requests', async function () {
  918. const testData = initializeOrg({
  919. organization: {
  920. ...TestStubs.Organization(),
  921. },
  922. });
  923. const eventsStatsMock = MockApiClient.addMockResponse({
  924. url: '/organizations/org-slug/events-stats/',
  925. body: [],
  926. });
  927. const areaWidget = {
  928. displayType: 'area',
  929. interval: '5m',
  930. queries: [
  931. {
  932. conditions: 'event.type:error',
  933. fields: [],
  934. aggregates: ['count()', 'equation|count() * 2'],
  935. columns: ['project'],
  936. orderby: 'equation[0]',
  937. name: '',
  938. },
  939. ],
  940. };
  941. renderWithProviders(
  942. <WidgetQueries
  943. api={api}
  944. widget={areaWidget}
  945. organization={testData.organization}
  946. selection={selection}
  947. >
  948. {() => <div data-test-id="child" />}
  949. </WidgetQueries>
  950. );
  951. // Child should be rendered and 1 requests should be sent.
  952. await screen.findByTestId('child');
  953. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  954. expect(eventsStatsMock).toHaveBeenCalledWith(
  955. '/organizations/org-slug/events-stats/',
  956. expect.objectContaining({
  957. query: expect.objectContaining({
  958. field: ['project', 'count()', 'equation|count() * 2'],
  959. orderby: 'equation[0]',
  960. }),
  961. })
  962. );
  963. });
  964. });