widgetQueries.spec.tsx 28 KB

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