widgetQueries.spec.tsx 27 KB

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