widgetQueries.spec.jsx 28 KB

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