content.spec.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import {RouterFixture} from 'sentry-fixture/routerFixture';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {
  4. render,
  5. screen,
  6. userEvent,
  7. waitFor,
  8. within,
  9. } from 'sentry-test/reactTestingLibrary';
  10. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  11. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  12. import {PageParamsProvider} from 'sentry/views/explore/contexts/pageParamsContext';
  13. import {SpanTagsProvider} from 'sentry/views/explore/contexts/spanTagsContext';
  14. import {MultiQueryModeContent} from 'sentry/views/explore/multiQueryMode/content';
  15. import {useReadQueriesFromLocation} from 'sentry/views/explore/multiQueryMode/locationUtils';
  16. jest.mock('sentry/components/lazyRender', () => ({
  17. LazyRender: ({children}: {children: React.ReactNode}) => children,
  18. }));
  19. describe('MultiQueryModeContent', function () {
  20. const {organization, project} = initializeOrg({
  21. organization: {
  22. features: ['visibility-explore-rpc'],
  23. },
  24. });
  25. let eventsRequest: any;
  26. let eventsStatsRequest: any;
  27. beforeEach(function () {
  28. // without this the `CompactSelect` component errors with a bunch of async updates
  29. jest.spyOn(console, 'error').mockImplementation();
  30. MockApiClient.clearMockResponses();
  31. PageFiltersStore.init();
  32. PageFiltersStore.onInitializeUrlState(
  33. {
  34. projects: [project].map(p => parseInt(p.id, 10)),
  35. environments: [],
  36. datetime: {
  37. period: '7d',
  38. start: null,
  39. end: null,
  40. utc: null,
  41. },
  42. },
  43. new Set()
  44. );
  45. MockApiClient.addMockResponse({
  46. url: `/organizations/${organization.slug}/spans/fields/`,
  47. method: 'GET',
  48. body: [{key: 'span.op', name: 'span.op'}],
  49. });
  50. eventsRequest = MockApiClient.addMockResponse({
  51. url: `/organizations/${organization.slug}/events/`,
  52. method: 'GET',
  53. body: {},
  54. });
  55. eventsStatsRequest = MockApiClient.addMockResponse({
  56. url: `/organizations/${organization.slug}/events-stats/`,
  57. method: 'GET',
  58. body: {},
  59. });
  60. });
  61. it('updates visualization and outdated sorts', async function () {
  62. let queries: any;
  63. function Component() {
  64. queries = useReadQueriesFromLocation();
  65. return <MultiQueryModeContent />;
  66. }
  67. render(
  68. <PageParamsProvider>
  69. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  70. <Component />
  71. </SpanTagsProvider>
  72. </PageParamsProvider>,
  73. {disableRouterMocks: true}
  74. );
  75. expect(queries).toEqual([
  76. {
  77. chartType: 1,
  78. yAxes: ['avg(span.duration)'],
  79. sortBys: [
  80. {
  81. field: 'span.duration',
  82. kind: 'desc',
  83. },
  84. ],
  85. fields: ['id', 'span.duration'],
  86. groupBys: [],
  87. query: '',
  88. },
  89. ]);
  90. const section = screen.getByTestId('section-visualize-0');
  91. await userEvent.click(within(section).getByRole('button', {name: 'span.duration'}));
  92. await userEvent.click(within(section).getByRole('option', {name: 'span.self_time'}));
  93. expect(queries).toEqual([
  94. {
  95. chartType: 1,
  96. yAxes: ['avg(span.self_time)'],
  97. sortBys: [
  98. {
  99. field: 'id',
  100. kind: 'desc',
  101. },
  102. ],
  103. fields: ['id', 'span.self_time'],
  104. groupBys: [],
  105. query: '',
  106. },
  107. ]);
  108. });
  109. it('updates sorts', async function () {
  110. let queries: any;
  111. function Component() {
  112. queries = useReadQueriesFromLocation();
  113. return <MultiQueryModeContent />;
  114. }
  115. render(
  116. <PageParamsProvider>
  117. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  118. <Component />
  119. </SpanTagsProvider>
  120. </PageParamsProvider>,
  121. {disableRouterMocks: true}
  122. );
  123. expect(queries).toEqual([
  124. {
  125. chartType: 1,
  126. yAxes: ['avg(span.duration)'],
  127. sortBys: [
  128. {
  129. field: 'span.duration',
  130. kind: 'desc',
  131. },
  132. ],
  133. fields: ['id', 'span.duration'],
  134. groupBys: [],
  135. query: '',
  136. },
  137. ]);
  138. const section = screen.getByTestId('section-sort-by-0');
  139. await userEvent.click(within(section).getByRole('button', {name: 'span.duration'}));
  140. await userEvent.click(within(section).getByRole('option', {name: 'id'}));
  141. expect(queries).toEqual([
  142. {
  143. chartType: 1,
  144. yAxes: ['avg(span.duration)'],
  145. sortBys: [
  146. {
  147. field: 'id',
  148. kind: 'desc',
  149. },
  150. ],
  151. fields: ['id', 'span.duration'],
  152. groupBys: [],
  153. query: '',
  154. },
  155. ]);
  156. });
  157. it('updates group bys and outdated sorts', async function () {
  158. let queries: any;
  159. function Component() {
  160. queries = useReadQueriesFromLocation();
  161. return <MultiQueryModeContent />;
  162. }
  163. render(
  164. <PageParamsProvider>
  165. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  166. <Component />
  167. </SpanTagsProvider>
  168. </PageParamsProvider>,
  169. {disableRouterMocks: true}
  170. );
  171. expect(queries).toEqual([
  172. {
  173. chartType: 1,
  174. yAxes: ['avg(span.duration)'],
  175. sortBys: [
  176. {
  177. field: 'span.duration',
  178. kind: 'desc',
  179. },
  180. ],
  181. fields: ['id', 'span.duration'],
  182. groupBys: [],
  183. query: '',
  184. },
  185. ]);
  186. const section = screen.getByTestId('section-group-by-0');
  187. await userEvent.click(within(section).getByRole('button', {name: 'None'}));
  188. await userEvent.click(within(section).getByRole('option', {name: 'span.op'}));
  189. expect(queries).toEqual([
  190. {
  191. yAxes: ['avg(span.duration)'],
  192. chartType: 1,
  193. sortBys: [
  194. {
  195. field: 'avg(span.duration)',
  196. kind: 'desc',
  197. },
  198. ],
  199. query: '',
  200. groupBys: ['span.op'],
  201. fields: ['id', 'span.duration'],
  202. },
  203. ]);
  204. });
  205. it('updates query at the correct index', async function () {
  206. let queries: any;
  207. function Component() {
  208. queries = useReadQueriesFromLocation();
  209. return <MultiQueryModeContent />;
  210. }
  211. render(
  212. <PageParamsProvider>
  213. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  214. <Component />
  215. </SpanTagsProvider>
  216. </PageParamsProvider>,
  217. {disableRouterMocks: true}
  218. );
  219. expect(queries).toEqual([
  220. {
  221. chartType: 1,
  222. yAxes: ['avg(span.duration)'],
  223. sortBys: [
  224. {
  225. field: 'span.duration',
  226. kind: 'desc',
  227. },
  228. ],
  229. fields: ['id', 'span.duration'],
  230. groupBys: [],
  231. query: '',
  232. },
  233. ]);
  234. // Add chart
  235. await userEvent.click(screen.getByRole('button', {name: 'Add Query'}));
  236. expect(queries).toEqual([
  237. {
  238. chartType: 1,
  239. yAxes: ['avg(span.duration)'],
  240. sortBys: [
  241. {
  242. field: 'span.duration',
  243. kind: 'desc',
  244. },
  245. ],
  246. fields: ['id', 'span.duration'],
  247. groupBys: [],
  248. query: '',
  249. },
  250. {
  251. chartType: 1,
  252. yAxes: ['avg(span.duration)'],
  253. sortBys: [
  254. {
  255. field: 'span.duration',
  256. kind: 'desc',
  257. },
  258. ],
  259. fields: ['id', 'span.duration'],
  260. groupBys: [],
  261. query: '',
  262. },
  263. ]);
  264. const section = screen.getByTestId('section-visualize-0');
  265. await userEvent.click(within(section).getByRole('button', {name: 'span.duration'}));
  266. await userEvent.click(within(section).getByRole('option', {name: 'span.self_time'}));
  267. expect(queries).toEqual([
  268. {
  269. chartType: 1,
  270. yAxes: ['avg(span.self_time)'],
  271. sortBys: [
  272. {
  273. field: 'id',
  274. kind: 'desc',
  275. },
  276. ],
  277. fields: ['id', 'span.self_time'],
  278. groupBys: [],
  279. query: '',
  280. },
  281. {
  282. chartType: 1,
  283. yAxes: ['avg(span.duration)'],
  284. sortBys: [
  285. {
  286. field: 'span.duration',
  287. kind: 'desc',
  288. },
  289. ],
  290. fields: ['id', 'span.duration'],
  291. groupBys: [],
  292. query: '',
  293. },
  294. ]);
  295. await userEvent.click(screen.getAllByLabelText('Delete Query')[0]!);
  296. expect(queries).toEqual([
  297. {
  298. chartType: 1,
  299. yAxes: ['avg(span.duration)'],
  300. sortBys: [
  301. {
  302. field: 'span.duration',
  303. kind: 'desc',
  304. },
  305. ],
  306. fields: ['id', 'span.duration'],
  307. groupBys: [],
  308. query: '',
  309. },
  310. ]);
  311. });
  312. it('calls events and stats APIs', async function () {
  313. let queries: any;
  314. function Component() {
  315. queries = useReadQueriesFromLocation();
  316. return <MultiQueryModeContent />;
  317. }
  318. render(
  319. <PageParamsProvider>
  320. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  321. <Component />
  322. </SpanTagsProvider>
  323. </PageParamsProvider>,
  324. {disableRouterMocks: true}
  325. );
  326. expect(queries).toEqual([
  327. {
  328. chartType: 1,
  329. yAxes: ['avg(span.duration)'],
  330. sortBys: [
  331. {
  332. field: 'span.duration',
  333. kind: 'desc',
  334. },
  335. ],
  336. fields: ['id', 'span.duration'],
  337. groupBys: [],
  338. query: '',
  339. },
  340. ]);
  341. const section = screen.getByTestId('section-group-by-0');
  342. await userEvent.click(within(section).getByRole('button', {name: 'None'}));
  343. await userEvent.click(within(section).getByRole('option', {name: 'span.op'}));
  344. await waitFor(() =>
  345. expect(eventsStatsRequest).toHaveBeenNthCalledWith(
  346. 1,
  347. `/organizations/${organization.slug}/events-stats/`,
  348. expect.objectContaining({
  349. query: expect.objectContaining({
  350. dataset: 'spans',
  351. field: [],
  352. interval: '1h',
  353. orderby: undefined,
  354. project: ['2'],
  355. query: '!transaction.span_id:00',
  356. referrer: 'api.explorer.stats',
  357. statsPeriod: '7d',
  358. topEvents: undefined,
  359. useRpc: '1',
  360. yAxis: 'avg(span.duration)',
  361. }),
  362. })
  363. )
  364. );
  365. await waitFor(() =>
  366. expect(eventsRequest).toHaveBeenNthCalledWith(
  367. 1,
  368. `/organizations/${organization.slug}/events/`,
  369. expect.objectContaining({
  370. query: expect.objectContaining({
  371. dataset: 'spans',
  372. environment: [],
  373. field: [
  374. 'id',
  375. 'span.duration',
  376. 'transaction.span_id',
  377. 'trace',
  378. 'project',
  379. 'timestamp',
  380. ],
  381. per_page: 10,
  382. project: ['2'],
  383. query: '!transaction.span_id:00',
  384. referrer: 'api.explore.multi-query-spans-table',
  385. sort: '-span.duration',
  386. statsPeriod: '7d',
  387. useRpc: '1',
  388. }),
  389. })
  390. )
  391. );
  392. // group by requests
  393. await waitFor(() =>
  394. expect(eventsStatsRequest).toHaveBeenNthCalledWith(
  395. 2,
  396. `/organizations/${organization.slug}/events-stats/`,
  397. expect.objectContaining({
  398. query: expect.objectContaining({
  399. dataset: 'spans',
  400. excludeOther: 0,
  401. field: ['span.op', 'avg(span.duration)'],
  402. interval: '1h',
  403. orderby: '-avg_span_duration',
  404. project: ['2'],
  405. query: '!transaction.span_id:00',
  406. referrer: 'api.explorer.stats',
  407. sort: '-avg_span_duration',
  408. statsPeriod: '7d',
  409. topEvents: '5',
  410. useRpc: '1',
  411. yAxis: 'avg(span.duration)',
  412. }),
  413. })
  414. )
  415. );
  416. await waitFor(() =>
  417. expect(eventsRequest).toHaveBeenNthCalledWith(
  418. 2,
  419. `/organizations/${organization.slug}/events/`,
  420. expect.objectContaining({
  421. query: expect.objectContaining({
  422. dataset: 'spans',
  423. environment: [],
  424. field: ['span.op', 'avg(span.duration)'],
  425. per_page: 10,
  426. project: ['2'],
  427. query: '!transaction.span_id:00',
  428. referrer: 'api.explore.multi-query-spans-table',
  429. sort: '-avg_span_duration',
  430. statsPeriod: '7d',
  431. useRpc: '1',
  432. }),
  433. })
  434. )
  435. );
  436. });
  437. it('unstacking group by puts you in sample mode', async function () {
  438. MockApiClient.addMockResponse({
  439. url: `/organizations/${organization.slug}/events/`,
  440. method: 'GET',
  441. body: {
  442. data: [
  443. {
  444. 'span.op': 'POST',
  445. 'avg(span.duration)': 147.02002059925093,
  446. },
  447. {
  448. 'span.op': 'GET',
  449. 'avg(span.duration)': 1.9993342331511974,
  450. },
  451. ],
  452. },
  453. match: [
  454. function (_url: string, options: Record<string, any>) {
  455. return options.query.field.includes('span.op');
  456. },
  457. ],
  458. });
  459. let queries: any;
  460. function Component() {
  461. queries = useReadQueriesFromLocation();
  462. return <MultiQueryModeContent />;
  463. }
  464. render(
  465. <PageParamsProvider>
  466. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  467. <Component />
  468. </SpanTagsProvider>
  469. </PageParamsProvider>,
  470. {disableRouterMocks: true}
  471. );
  472. expect(queries).toEqual([
  473. {
  474. chartType: 1,
  475. yAxes: ['avg(span.duration)'],
  476. sortBys: [
  477. {
  478. field: 'span.duration',
  479. kind: 'desc',
  480. },
  481. ],
  482. fields: ['id', 'span.duration'],
  483. groupBys: [],
  484. query: '',
  485. },
  486. ]);
  487. const section = screen.getByTestId('section-group-by-0');
  488. await userEvent.click(within(section).getByRole('button', {name: 'None'}));
  489. await userEvent.click(within(section).getByRole('option', {name: 'span.op'}));
  490. await userEvent.click(screen.getAllByTestId('unstack-link')[0]!);
  491. expect(queries).toEqual([
  492. {
  493. chartType: 1,
  494. yAxes: ['avg(span.duration)'],
  495. sortBys: [
  496. {
  497. field: 'id',
  498. kind: 'desc',
  499. },
  500. ],
  501. fields: ['id', 'span.duration'],
  502. groupBys: [],
  503. query: 'span.op:POST',
  504. },
  505. ]);
  506. });
  507. it('sets interval correctly', async function () {
  508. const router = RouterFixture({
  509. location: {
  510. pathname: '/traces/compare',
  511. query: {
  512. queries: [
  513. '{"groupBys":[],"query":"","sortBys":["-timestamp"],"yAxes":["avg(span.duration)"]}',
  514. ],
  515. },
  516. },
  517. });
  518. function Component() {
  519. return <MultiQueryModeContent />;
  520. }
  521. render(
  522. <PageParamsProvider>
  523. <SpanTagsProvider dataset={DiscoverDatasets.SPANS_EAP} enabled>
  524. <Component />
  525. </SpanTagsProvider>
  526. </PageParamsProvider>,
  527. {router, organization}
  528. );
  529. const section = screen.getByTestId('section-visualization-0');
  530. expect(
  531. await within(section).findByRole('button', {name: '1 hour'})
  532. ).toBeInTheDocument();
  533. await userEvent.click(within(section).getByRole('button', {name: '1 hour'}));
  534. await userEvent.click(within(section).getByRole('option', {name: '30 minutes'}));
  535. expect(router.push).toHaveBeenCalledWith({
  536. pathname: '/traces/compare',
  537. query: expect.objectContaining({
  538. interval: '30m',
  539. queries: [
  540. '{"groupBys":[],"query":"","sortBys":["-timestamp"],"yAxes":["avg(span.duration)"]}',
  541. ],
  542. }),
  543. });
  544. });
  545. });