widgetQueries.spec.jsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  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. it('does not re-query events and sets name in widgets', async function () {
  654. const eventsStatsMock = MockApiClient.addMockResponse({
  655. url: '/organizations/org-slug/events-stats/',
  656. body: TestStubs.EventsStats(),
  657. });
  658. const lineWidget = {
  659. ...singleQueryWidget,
  660. displayType: 'line',
  661. interval: '5m',
  662. };
  663. let childProps;
  664. const {rerender} = render(
  665. <WidgetQueries
  666. api={api}
  667. widget={lineWidget}
  668. organization={initialData.organization}
  669. selection={selection}
  670. >
  671. {props => {
  672. childProps = props;
  673. return <div data-test-id="child" />;
  674. }}
  675. </WidgetQueries>
  676. );
  677. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  678. await waitFor(() => expect(childProps.loading).toEqual(false));
  679. // Simulate a re-render with a new query alias
  680. rerender(
  681. <WidgetQueries
  682. api={api}
  683. widget={{
  684. ...lineWidget,
  685. queries: [
  686. {
  687. conditions: 'event.type:error',
  688. fields: ['count()'],
  689. aggregates: ['count()'],
  690. columns: [],
  691. name: 'this query alias changed',
  692. orderby: '',
  693. },
  694. ],
  695. }}
  696. organization={initialData.organization}
  697. selection={selection}
  698. >
  699. {props => {
  700. childProps = props;
  701. return <div data-test-id="child" />;
  702. }}
  703. </WidgetQueries>
  704. );
  705. // Did not re-query
  706. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  707. expect(childProps.timeseriesResults[0].seriesName).toEqual(
  708. 'this query alias changed : count()'
  709. );
  710. });
  711. describe('multi-series grouped data', () => {
  712. const [START, END] = [1647399900, 1647399901];
  713. let mockCountData, mockCountUniqueData, mockRawResultData;
  714. beforeEach(() => {
  715. mockCountData = {
  716. start: START,
  717. end: END,
  718. data: [
  719. [START, [{'count()': 0}]],
  720. [END, [{'count()': 0}]],
  721. ],
  722. };
  723. mockCountUniqueData = {
  724. start: START,
  725. end: END,
  726. data: [
  727. [START, [{'count_unique()': 0}]],
  728. [END, [{'count_unique()': 0}]],
  729. ],
  730. };
  731. mockRawResultData = {
  732. local: {
  733. 'count()': mockCountData,
  734. 'count_unique()': mockCountUniqueData,
  735. order: 0,
  736. },
  737. prod: {
  738. 'count()': mockCountData,
  739. 'count_unique()': mockCountUniqueData,
  740. order: 1,
  741. },
  742. };
  743. });
  744. it('combines group name and aggregate names in grouped multi series data', () => {
  745. const actual = flattenMultiSeriesDataWithGrouping(mockRawResultData, '');
  746. expect(actual).toEqual([
  747. [
  748. 0,
  749. expect.objectContaining({
  750. seriesName: 'local : count()',
  751. data: expect.anything(),
  752. }),
  753. ],
  754. [
  755. 0,
  756. expect.objectContaining({
  757. seriesName: 'local : count_unique()',
  758. data: expect.anything(),
  759. }),
  760. ],
  761. [
  762. 1,
  763. expect.objectContaining({
  764. seriesName: 'prod : count()',
  765. data: expect.anything(),
  766. }),
  767. ],
  768. [
  769. 1,
  770. expect.objectContaining({
  771. seriesName: 'prod : count_unique()',
  772. data: expect.anything(),
  773. }),
  774. ],
  775. ]);
  776. });
  777. it('prefixes with a query alias when provided', () => {
  778. const actual = flattenMultiSeriesDataWithGrouping(mockRawResultData, 'Query 1');
  779. expect(actual).toEqual([
  780. [
  781. 0,
  782. expect.objectContaining({
  783. seriesName: 'Query 1 > local : count()',
  784. data: expect.anything(),
  785. }),
  786. ],
  787. [
  788. 0,
  789. expect.objectContaining({
  790. seriesName: 'Query 1 > local : count_unique()',
  791. data: expect.anything(),
  792. }),
  793. ],
  794. [
  795. 1,
  796. expect.objectContaining({
  797. seriesName: 'Query 1 > prod : count()',
  798. data: expect.anything(),
  799. }),
  800. ],
  801. [
  802. 1,
  803. expect.objectContaining({
  804. seriesName: 'Query 1 > prod : count_unique()',
  805. data: expect.anything(),
  806. }),
  807. ],
  808. ]);
  809. });
  810. });
  811. it('charts send metricsEnhanced requests', async function () {
  812. const {organization} = initialData;
  813. const mock = MockApiClient.addMockResponse({
  814. url: '/organizations/org-slug/events-stats/',
  815. body: {
  816. data: [
  817. [
  818. 1000,
  819. [
  820. {
  821. count: 100,
  822. },
  823. ],
  824. ],
  825. ],
  826. isMetricsData: false,
  827. start: 1000,
  828. end: 2000,
  829. },
  830. });
  831. const setIsMetricsMock = jest.fn();
  832. const children = jest.fn(() => <div />);
  833. render(
  834. <DashboardsMEPContext.Provider
  835. value={{
  836. isMetricsData: undefined,
  837. setIsMetricsData: setIsMetricsMock,
  838. }}
  839. >
  840. <WidgetQueries
  841. api={api}
  842. widget={singleQueryWidget}
  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-stats/',
  855. expect.objectContaining({
  856. query: expect.objectContaining({dataset: 'metricsEnhanced'}),
  857. })
  858. );
  859. await waitFor(() => {
  860. expect(setIsMetricsMock).toHaveBeenCalledWith(false);
  861. });
  862. });
  863. it('tables send metricsEnhanced requests', async function () {
  864. const {organization} = initialData;
  865. const mock = MockApiClient.addMockResponse({
  866. url: '/organizations/org-slug/eventsv2/',
  867. body: {
  868. meta: {title: 'string', isMetricsData: true},
  869. data: [{title: 'ValueError'}],
  870. },
  871. });
  872. const setIsMetricsMock = jest.fn();
  873. const children = jest.fn(() => <div />);
  874. render(
  875. <DashboardsMEPContext.Provider
  876. value={{
  877. isMetricsData: undefined,
  878. setIsMetricsData: setIsMetricsMock,
  879. }}
  880. >
  881. <WidgetQueries
  882. api={api}
  883. widget={{...singleQueryWidget, displayType: 'table'}}
  884. organization={{
  885. ...organization,
  886. features: [...organization.features, 'dashboards-mep'],
  887. }}
  888. selection={selection}
  889. >
  890. {children}
  891. </WidgetQueries>
  892. </DashboardsMEPContext.Provider>
  893. );
  894. expect(mock).toHaveBeenCalledWith(
  895. '/organizations/org-slug/eventsv2/',
  896. expect.objectContaining({
  897. query: expect.objectContaining({dataset: 'metricsEnhanced'}),
  898. })
  899. );
  900. await waitFor(() => {
  901. expect(setIsMetricsMock).toHaveBeenCalledWith(true);
  902. });
  903. });
  904. it('does not inject equation aliases for top N requests', async function () {
  905. const testData = initializeOrg({
  906. organization: {
  907. ...TestStubs.Organization(),
  908. features: ['new-widget-builder-experience-design'],
  909. },
  910. });
  911. const eventsStatsMock = MockApiClient.addMockResponse({
  912. url: '/organizations/org-slug/events-stats/',
  913. body: [],
  914. });
  915. const areaWidget = {
  916. displayType: 'area',
  917. interval: '5m',
  918. queries: [
  919. {
  920. conditions: 'event.type:error',
  921. fields: [],
  922. aggregates: ['count()', 'equation|count() * 2'],
  923. columns: ['project'],
  924. orderby: 'equation[0]',
  925. name: '',
  926. },
  927. ],
  928. };
  929. render(
  930. <WidgetQueries
  931. api={api}
  932. widget={areaWidget}
  933. organization={testData.organization}
  934. selection={selection}
  935. >
  936. {() => <div data-test-id="child" />}
  937. </WidgetQueries>
  938. );
  939. // Child should be rendered and 1 requests should be sent.
  940. await screen.findByTestId('child');
  941. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  942. expect(eventsStatsMock).toHaveBeenCalledWith(
  943. '/organizations/org-slug/events-stats/',
  944. expect.objectContaining({
  945. query: expect.objectContaining({
  946. field: ['project', 'count()', 'equation|count() * 2'],
  947. orderby: 'equation[0]',
  948. }),
  949. })
  950. );
  951. });
  952. });