widgetQueries.spec.jsx 26 KB

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