widgetQueries.spec.tsx 27 KB

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