widgetQueries.spec.tsx 28 KB

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