widgetQueries.spec.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  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 {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 {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. 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. 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. 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. 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. expect(child).toHaveBeenLastCalledWith(
  569. expect.objectContaining({
  570. timeseriesResults: [
  571. {data: [{name: 1000000, value: 200}], seriesName: 'errors : count()'},
  572. {data: [{name: 1000000, value: 100}], seriesName: 'default : count()'},
  573. ],
  574. })
  575. );
  576. });
  577. it('calls events-stats with 4h interval when interval buckets would exceed 66', async function () {
  578. const eventsStatsMock = MockApiClient.addMockResponse({
  579. url: '/organizations/org-slug/events-stats/',
  580. body: [],
  581. });
  582. const areaWidget = {
  583. ...singleQueryWidget,
  584. displayType: DisplayType.AREA,
  585. interval: '5m',
  586. };
  587. renderWithProviders(
  588. <WidgetQueries
  589. api={new MockApiClient()}
  590. widget={areaWidget}
  591. organization={initialData.organization}
  592. selection={{
  593. ...selection,
  594. datetime: {
  595. period: '90d',
  596. start: null,
  597. end: null,
  598. utc: false,
  599. },
  600. }}
  601. >
  602. {() => <div data-test-id="child" />}
  603. </WidgetQueries>
  604. );
  605. // Child should be rendered and 1 requests should be sent.
  606. await screen.findByTestId('child');
  607. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  608. expect(eventsStatsMock).toHaveBeenCalledWith(
  609. '/organizations/org-slug/events-stats/',
  610. expect.objectContaining({query: expect.objectContaining({interval: '4h'})})
  611. );
  612. });
  613. it('does not re-query events and sets name in widgets', async function () {
  614. const eventsStatsMock = MockApiClient.addMockResponse({
  615. url: '/organizations/org-slug/events-stats/',
  616. body: EventsStatsFixture(),
  617. });
  618. const lineWidget = {
  619. ...singleQueryWidget,
  620. displayType: DisplayType.LINE,
  621. interval: '5m',
  622. };
  623. let childProps;
  624. const {rerender} = renderWithProviders(
  625. <WidgetQueries
  626. api={new MockApiClient()}
  627. widget={lineWidget}
  628. organization={initialData.organization}
  629. selection={selection}
  630. >
  631. {props => {
  632. childProps = props;
  633. return <div data-test-id="child" />;
  634. }}
  635. </WidgetQueries>
  636. );
  637. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  638. await waitFor(() => expect(childProps.loading).toEqual(false));
  639. // Simulate a re-render with a new query alias
  640. rerender(
  641. <MetricsResultsMetaProvider>
  642. <MEPSettingProvider forceTransactions={false}>
  643. <WidgetQueries
  644. api={new MockApiClient()}
  645. widget={{
  646. ...lineWidget,
  647. queries: [
  648. {
  649. conditions: 'event.type:error',
  650. fields: ['count()'],
  651. aggregates: ['count()'],
  652. columns: [],
  653. name: 'this query alias changed',
  654. orderby: '',
  655. },
  656. ],
  657. }}
  658. organization={initialData.organization}
  659. selection={selection}
  660. >
  661. {props => {
  662. childProps = props;
  663. return <div data-test-id="child" />;
  664. }}
  665. </WidgetQueries>
  666. </MEPSettingProvider>
  667. </MetricsResultsMetaProvider>
  668. );
  669. // Did not re-query
  670. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  671. expect(childProps.timeseriesResults[0].seriesName).toEqual(
  672. 'this query alias changed : count()'
  673. );
  674. });
  675. describe('multi-series grouped data', () => {
  676. const [START, END] = [1647399900, 1647399901];
  677. let mockCountData, mockCountUniqueData, mockRawResultData;
  678. beforeEach(() => {
  679. mockCountData = {
  680. start: START,
  681. end: END,
  682. data: [
  683. [START, [{'count()': 0}]],
  684. [END, [{'count()': 0}]],
  685. ],
  686. };
  687. mockCountUniqueData = {
  688. start: START,
  689. end: END,
  690. data: [
  691. [START, [{'count_unique()': 0}]],
  692. [END, [{'count_unique()': 0}]],
  693. ],
  694. };
  695. mockRawResultData = {
  696. local: {
  697. 'count()': mockCountData,
  698. 'count_unique()': mockCountUniqueData,
  699. order: 0,
  700. },
  701. prod: {
  702. 'count()': mockCountData,
  703. 'count_unique()': mockCountUniqueData,
  704. order: 1,
  705. },
  706. };
  707. });
  708. it('combines group name and aggregate names in grouped multi series data', () => {
  709. const actual = flattenMultiSeriesDataWithGrouping(mockRawResultData, '');
  710. expect(actual).toEqual([
  711. [
  712. 0,
  713. expect.objectContaining({
  714. seriesName: 'local : count()',
  715. data: expect.anything(),
  716. }),
  717. ],
  718. [
  719. 0,
  720. expect.objectContaining({
  721. seriesName: 'local : count_unique()',
  722. data: expect.anything(),
  723. }),
  724. ],
  725. [
  726. 1,
  727. expect.objectContaining({
  728. seriesName: 'prod : count()',
  729. data: expect.anything(),
  730. }),
  731. ],
  732. [
  733. 1,
  734. expect.objectContaining({
  735. seriesName: 'prod : count_unique()',
  736. data: expect.anything(),
  737. }),
  738. ],
  739. ]);
  740. });
  741. it('prefixes with a query alias when provided', () => {
  742. const actual = flattenMultiSeriesDataWithGrouping(mockRawResultData, 'Query 1');
  743. expect(actual).toEqual([
  744. [
  745. 0,
  746. expect.objectContaining({
  747. seriesName: 'Query 1 > local : count()',
  748. data: expect.anything(),
  749. }),
  750. ],
  751. [
  752. 0,
  753. expect.objectContaining({
  754. seriesName: 'Query 1 > local : count_unique()',
  755. data: expect.anything(),
  756. }),
  757. ],
  758. [
  759. 1,
  760. expect.objectContaining({
  761. seriesName: 'Query 1 > prod : count()',
  762. data: expect.anything(),
  763. }),
  764. ],
  765. [
  766. 1,
  767. expect.objectContaining({
  768. seriesName: 'Query 1 > prod : count_unique()',
  769. data: expect.anything(),
  770. }),
  771. ],
  772. ]);
  773. });
  774. });
  775. it('charts send metricsEnhanced requests', async function () {
  776. const {organization} = initialData;
  777. const mock = MockApiClient.addMockResponse({
  778. url: '/organizations/org-slug/events-stats/',
  779. body: {
  780. data: [
  781. [
  782. 1000,
  783. [
  784. {
  785. count: 100,
  786. },
  787. ],
  788. ],
  789. ],
  790. isMetricsData: false,
  791. start: 1000,
  792. end: 2000,
  793. },
  794. });
  795. const setIsMetricsMock = jest.fn();
  796. const children = jest.fn(() => <div />);
  797. renderWithProviders(
  798. <DashboardsMEPContext.Provider
  799. value={{
  800. isMetricsData: undefined,
  801. setIsMetricsData: setIsMetricsMock,
  802. }}
  803. >
  804. <WidgetQueries
  805. api={new MockApiClient()}
  806. widget={singleQueryWidget}
  807. organization={{
  808. ...organization,
  809. features: [...organization.features, 'dashboards-mep'],
  810. }}
  811. selection={selection}
  812. >
  813. {children}
  814. </WidgetQueries>
  815. </DashboardsMEPContext.Provider>
  816. );
  817. expect(mock).toHaveBeenCalledWith(
  818. '/organizations/org-slug/events-stats/',
  819. expect.objectContaining({
  820. query: expect.objectContaining({dataset: 'metricsEnhanced'}),
  821. })
  822. );
  823. await waitFor(() => {
  824. expect(setIsMetricsMock).toHaveBeenCalledWith(false);
  825. });
  826. });
  827. it('tables send metricsEnhanced requests', async function () {
  828. const {organization} = initialData;
  829. const mock = MockApiClient.addMockResponse({
  830. url: '/organizations/org-slug/events/',
  831. body: {
  832. meta: {title: 'string', isMetricsData: true},
  833. data: [{title: 'ValueError'}],
  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={new MockApiClient()}
  847. widget={{...singleQueryWidget, displayType: DisplayType.TABLE}}
  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/',
  860. expect.objectContaining({
  861. query: expect.objectContaining({dataset: 'metricsEnhanced'}),
  862. })
  863. );
  864. await waitFor(() => {
  865. expect(setIsMetricsMock).toHaveBeenCalledWith(true);
  866. });
  867. });
  868. it('does not inject equation aliases for top N requests', async function () {
  869. const testData = initializeOrg({
  870. organization: {
  871. ...OrganizationFixture(),
  872. },
  873. });
  874. const eventsStatsMock = MockApiClient.addMockResponse({
  875. url: '/organizations/org-slug/events-stats/',
  876. body: [],
  877. });
  878. const areaWidget = {
  879. title: 'Errors',
  880. displayType: DisplayType.AREA,
  881. interval: '5m',
  882. queries: [
  883. {
  884. conditions: 'event.type:error',
  885. fields: [],
  886. aggregates: ['count()', 'equation|count() * 2'],
  887. columns: ['project'],
  888. orderby: 'equation[0]',
  889. name: '',
  890. },
  891. ],
  892. };
  893. renderWithProviders(
  894. <WidgetQueries
  895. api={new MockApiClient()}
  896. widget={areaWidget}
  897. organization={testData.organization}
  898. selection={selection}
  899. >
  900. {() => <div data-test-id="child" />}
  901. </WidgetQueries>
  902. );
  903. // Child should be rendered and 1 requests should be sent.
  904. await screen.findByTestId('child');
  905. expect(eventsStatsMock).toHaveBeenCalledTimes(1);
  906. expect(eventsStatsMock).toHaveBeenCalledWith(
  907. '/organizations/org-slug/events-stats/',
  908. expect.objectContaining({
  909. query: expect.objectContaining({
  910. field: ['project', 'count()', 'equation|count() * 2'],
  911. orderby: 'equation[0]',
  912. }),
  913. })
  914. );
  915. });
  916. });