traceView.spec.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. import {
  2. generateSampleEvent,
  3. generateSampleSpan,
  4. initializeData as _initializeData,
  5. } from 'sentry-test/performance/initializePerformanceData';
  6. import {MockSpan, TransactionEventBuilder} from 'sentry-test/performance/utils';
  7. import {
  8. render,
  9. screen,
  10. userEvent,
  11. waitFor,
  12. within,
  13. } from 'sentry-test/reactTestingLibrary';
  14. import * as AnchorLinkManager from 'sentry/components/events/interfaces/spans/spanContext';
  15. import TraceView from 'sentry/components/events/interfaces/spans/traceView';
  16. import {spanTargetHash} from 'sentry/components/events/interfaces/spans/utils';
  17. import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
  18. import ProjectsStore from 'sentry/stores/projectsStore';
  19. import {QuickTraceContext} from 'sentry/utils/performance/quickTrace/quickTraceContext';
  20. import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery';
  21. function initializeData(settings) {
  22. const data = _initializeData(settings);
  23. ProjectsStore.loadInitialData(data.projects);
  24. return data;
  25. }
  26. describe('TraceView', () => {
  27. let data;
  28. beforeEach(() => {
  29. data = initializeData({});
  30. });
  31. afterEach(() => {
  32. MockApiClient.clearMockResponses();
  33. });
  34. describe('Autogrouped spans tests', () => {
  35. it('should render siblings with the same op and description as a grouped span in the minimap and span tree', async () => {
  36. const builder = new TransactionEventBuilder();
  37. builder.addSpan(
  38. new MockSpan({
  39. startTimestamp: 0,
  40. endTimestamp: 100,
  41. op: 'http',
  42. description: 'group me',
  43. }),
  44. 5
  45. );
  46. const waterfallModel = new WaterfallModel(builder.getEventFixture());
  47. render(
  48. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  49. );
  50. expect(await screen.findByTestId('minimap-sibling-group-bar')).toBeInTheDocument();
  51. expect(await screen.findByTestId('span-row-2')).toHaveTextContent('Autogrouped');
  52. expect(screen.queryByTestId('span-row-3')).not.toBeInTheDocument();
  53. });
  54. it('should expand grouped siblings when clicked, async and then regroup when clicked again', async () => {
  55. const builder = new TransactionEventBuilder();
  56. builder.addSpan(
  57. new MockSpan({
  58. startTimestamp: 0,
  59. endTimestamp: 100,
  60. op: 'http',
  61. description: 'group me',
  62. }),
  63. 5
  64. );
  65. const waterfallModel = new WaterfallModel(builder.getEventFixture());
  66. render(
  67. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  68. );
  69. const groupedSiblingsSpan = await screen.findByText('Autogrouped — http —');
  70. await userEvent.click(groupedSiblingsSpan);
  71. await waitFor(() =>
  72. expect(screen.queryByText('Autogrouped — http —')).not.toBeInTheDocument()
  73. );
  74. for (let i = 1; i < 7; i++) {
  75. expect(await screen.findByTestId(`span-row-${i}`)).toBeInTheDocument();
  76. }
  77. const regroupButton = await screen.findByText('Regroup');
  78. expect(regroupButton).toBeInTheDocument();
  79. await userEvent.click(regroupButton);
  80. await waitFor(() =>
  81. expect(screen.queryByTestId('span-row-6')).not.toBeInTheDocument()
  82. );
  83. expect(await screen.findByText('Autogrouped — http —')).toBeInTheDocument();
  84. });
  85. it("should not group sibling spans that don't have the same op or description", async () => {
  86. const builder = new TransactionEventBuilder();
  87. builder.addSpan(
  88. new MockSpan({
  89. startTimestamp: 10,
  90. endTimestamp: 100,
  91. op: 'http',
  92. description: 'test',
  93. })
  94. );
  95. builder.addSpan(
  96. new MockSpan({
  97. startTimestamp: 100,
  98. endTimestamp: 200,
  99. op: 'http',
  100. description: 'group me',
  101. }),
  102. 5
  103. );
  104. builder.addSpan(
  105. new MockSpan({
  106. startTimestamp: 200,
  107. endTimestamp: 300,
  108. op: 'http',
  109. description: 'test',
  110. })
  111. );
  112. const waterfallModel = new WaterfallModel(builder.getEventFixture());
  113. render(
  114. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  115. );
  116. expect(await screen.findByText('group me')).toBeInTheDocument();
  117. expect(await screen.findAllByText('test')).toHaveLength(2);
  118. });
  119. it('should autogroup similar nested spans', async () => {
  120. const builder = new TransactionEventBuilder();
  121. const span = new MockSpan({
  122. startTimestamp: 50,
  123. endTimestamp: 100,
  124. op: 'http',
  125. description: 'group me',
  126. }).addDuplicateNestedChildren(5);
  127. builder.addSpan(span);
  128. const waterfallModel = new WaterfallModel(builder.getEventFixture());
  129. render(
  130. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  131. );
  132. const grouped = await screen.findByText('group me');
  133. expect(grouped).toBeInTheDocument();
  134. });
  135. it('should expand/collapse only the sibling group that is clicked, async even if multiple groups have the same op and description', async () => {
  136. const builder = new TransactionEventBuilder();
  137. builder.addSpan(
  138. new MockSpan({
  139. startTimestamp: 100,
  140. endTimestamp: 200,
  141. op: 'http',
  142. description: 'group me',
  143. }),
  144. 5
  145. );
  146. builder.addSpan(
  147. new MockSpan({
  148. startTimestamp: 200,
  149. endTimestamp: 300,
  150. op: 'http',
  151. description: 'not me',
  152. })
  153. );
  154. builder.addSpan(
  155. new MockSpan({
  156. startTimestamp: 300,
  157. endTimestamp: 400,
  158. op: 'http',
  159. description: 'group me',
  160. }),
  161. 5
  162. );
  163. const waterfallModel = new WaterfallModel(builder.getEventFixture());
  164. render(
  165. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  166. );
  167. expect(screen.queryAllByText('group me')).toHaveLength(2);
  168. const firstGroup = screen.queryAllByText('Autogrouped — http —')[0];
  169. await userEvent.click(firstGroup);
  170. expect(await screen.findAllByText('group me')).toHaveLength(6);
  171. const secondGroup = await screen.findByText('Autogrouped — http —');
  172. await userEvent.click(secondGroup);
  173. expect(await screen.findAllByText('group me')).toHaveLength(10);
  174. const firstRegroup = screen.queryAllByText('Regroup')[0];
  175. await userEvent.click(firstRegroup);
  176. expect(await screen.findAllByText('group me')).toHaveLength(6);
  177. const secondRegroup = await screen.findByText('Regroup');
  178. await userEvent.click(secondRegroup);
  179. expect(await screen.findAllByText('group me')).toHaveLength(2);
  180. });
  181. // TODO: This test can be converted later to use the TransactionEventBuilder instead
  182. it('should allow expanding of embedded transactions', async () => {
  183. const {organization, project, location} = initializeData({});
  184. const event = generateSampleEvent();
  185. generateSampleSpan(
  186. 'parent span',
  187. 'db',
  188. 'b000000000000000',
  189. 'a000000000000000',
  190. event
  191. );
  192. const waterfallModel = new WaterfallModel(event);
  193. const mockResponse = {
  194. method: 'GET',
  195. statusCode: 200,
  196. body: {
  197. transactions: [
  198. event,
  199. {
  200. errors: [],
  201. event_id: '998d7e2c304c45729545e4434e2967cb',
  202. generation: 1,
  203. parent_event_id: '2b658a829a21496b87fd1f14a61abf65',
  204. parent_span_id: 'b000000000000000',
  205. project_id: project.id,
  206. project_slug: project.slug,
  207. span_id: '8596e2795f88471d',
  208. transaction:
  209. '/api/0/organizations/{organization_slug}/events/{project_slug}:{event_id}/',
  210. 'transaction.duration': 159,
  211. 'transaction.op': 'http.server',
  212. },
  213. ],
  214. orphan_errors: [],
  215. },
  216. };
  217. const eventsTraceMock = MockApiClient.addMockResponse({
  218. url: `/organizations/${organization.slug}/events-trace/${event.contexts.trace?.trace_id}/`,
  219. ...mockResponse,
  220. });
  221. const eventsTraceLightMock = MockApiClient.addMockResponse({
  222. url: `/organizations/${organization.slug}/events-trace-light/${event.contexts.trace?.trace_id}/`,
  223. ...mockResponse,
  224. });
  225. const embeddedEvent = {
  226. ...generateSampleEvent(),
  227. id: '998d7e2c304c45729545e4434e2967cb',
  228. eventID: '998d7e2c304c45729545e4434e2967cb',
  229. };
  230. embeddedEvent.contexts.trace!.span_id = 'a111111111111111';
  231. const embeddedSpan = generateSampleSpan(
  232. 'i am embedded :)',
  233. 'test',
  234. 'b111111111111111',
  235. 'b000000000000000',
  236. embeddedEvent
  237. );
  238. embeddedSpan.trace_id = '8cbbc19c0f54447ab702f00263262726';
  239. const fetchEmbeddedTransactionMock = MockApiClient.addMockResponse({
  240. url: `/organizations/${organization.slug}/events/${project.slug}:998d7e2c304c45729545e4434e2967cb/`,
  241. method: 'GET',
  242. statusCode: 200,
  243. body: embeddedEvent,
  244. });
  245. render(
  246. <QuickTraceQuery event={event} location={location} orgSlug={organization.slug}>
  247. {results => (
  248. <QuickTraceContext.Provider value={results}>
  249. <TraceView organization={organization} waterfallModel={waterfallModel} />
  250. </QuickTraceContext.Provider>
  251. )}
  252. </QuickTraceQuery>
  253. );
  254. expect(eventsTraceMock).toHaveBeenCalled();
  255. expect(eventsTraceLightMock).toHaveBeenCalled();
  256. const embeddedTransactionBadge = await screen.findByTestId(
  257. 'embedded-transaction-badge'
  258. );
  259. expect(embeddedTransactionBadge).toBeInTheDocument();
  260. await userEvent.click(embeddedTransactionBadge);
  261. expect(fetchEmbeddedTransactionMock).toHaveBeenCalled();
  262. expect(await screen.findByText(/i am embedded :\)/i)).toBeInTheDocument();
  263. });
  264. it('should allow expanding of multiple embedded transactions with the same parent span', async () => {
  265. const {organization, project, location} = initializeData({});
  266. const event = generateSampleEvent();
  267. generateSampleSpan(
  268. 'GET /api/transitive-edge',
  269. 'http.client',
  270. 'b000000000000000',
  271. 'a000000000000000',
  272. event
  273. );
  274. const waterfallModel = new WaterfallModel(event);
  275. const mockResponse = {
  276. method: 'GET',
  277. statusCode: 200,
  278. body: {
  279. transactions: [
  280. event,
  281. {
  282. errors: [],
  283. event_id: '998d7e2c304c45729545e4434e2967cb',
  284. generation: 1,
  285. parent_event_id: '2b658a829a21496b87fd1f14a61abf65',
  286. parent_span_id: 'b000000000000000',
  287. project_id: project.id,
  288. project_slug: project.slug,
  289. span_id: '8596e2795f88471d',
  290. transaction:
  291. '/api/0/organizations/{organization_slug}/events/{project_slug}:{event_id}/',
  292. 'transaction.duration': 159,
  293. 'transaction.op': 'http.server',
  294. },
  295. {
  296. errors: [],
  297. event_id: '59e1fe369528499b87dab7221ce6b8a9',
  298. generation: 1,
  299. parent_event_id: '2b658a829a21496b87fd1f14a61abf65',
  300. parent_span_id: 'b000000000000000',
  301. project_id: project.id,
  302. project_slug: project.slug,
  303. span_id: 'aa5abb302ad5b9e1',
  304. transaction:
  305. '/api/0/organizations/{organization_slug}/events/{project_slug}:{event_id}/',
  306. 'transaction.duration': 159,
  307. 'transaction.op': 'middleware.nextjs',
  308. },
  309. ],
  310. orphan_errors: [],
  311. },
  312. };
  313. const eventsTraceMock = MockApiClient.addMockResponse({
  314. url: `/organizations/${organization.slug}/events-trace/${event.contexts.trace?.trace_id}/`,
  315. ...mockResponse,
  316. });
  317. const eventsTraceLightMock = MockApiClient.addMockResponse({
  318. url: `/organizations/${organization.slug}/events-trace-light/${event.contexts.trace?.trace_id}/`,
  319. ...mockResponse,
  320. });
  321. const embeddedEvent1 = {
  322. ...generateSampleEvent(),
  323. id: '998d7e2c304c45729545e4434e2967cb',
  324. eventID: '998d7e2c304c45729545e4434e2967cb',
  325. };
  326. embeddedEvent1.contexts.trace!.span_id = 'a111111111111111';
  327. const embeddedSpan1 = generateSampleSpan(
  328. 'i am embedded :)',
  329. 'test',
  330. 'b111111111111111',
  331. 'b000000000000000',
  332. embeddedEvent1
  333. );
  334. embeddedSpan1.trace_id = '8cbbc19c0f54447ab702f00263262726';
  335. const embeddedEvent2 = {
  336. ...generateSampleEvent(),
  337. id: '59e1fe369528499b87dab7221ce6b8a9',
  338. eventID: '59e1fe369528499b87dab7221ce6b8a9',
  339. };
  340. embeddedEvent2.contexts.trace!.span_id = 'a222222222222222';
  341. const embeddedSpan2 = generateSampleSpan(
  342. 'i am also embedded :o',
  343. 'middleware.nextjs',
  344. 'c111111111111111',
  345. 'b000000000000000',
  346. embeddedEvent2
  347. );
  348. embeddedSpan2.trace_id = '8cbbc19c0f54447ab702f00263262726';
  349. const fetchEmbeddedTransactionMock1 = MockApiClient.addMockResponse({
  350. url: `/organizations/${organization.slug}/events/${project.slug}:998d7e2c304c45729545e4434e2967cb/`,
  351. method: 'GET',
  352. statusCode: 200,
  353. body: embeddedEvent1,
  354. });
  355. const fetchEmbeddedTransactionMock2 = MockApiClient.addMockResponse({
  356. url: `/organizations/${organization.slug}/events/${project.slug}:59e1fe369528499b87dab7221ce6b8a9/`,
  357. method: 'GET',
  358. statusCode: 200,
  359. body: embeddedEvent2,
  360. });
  361. render(
  362. <QuickTraceQuery event={event} location={location} orgSlug={organization.slug}>
  363. {results => (
  364. <QuickTraceContext.Provider value={results}>
  365. <TraceView organization={organization} waterfallModel={waterfallModel} />
  366. </QuickTraceContext.Provider>
  367. )}
  368. </QuickTraceQuery>
  369. );
  370. expect(eventsTraceMock).toHaveBeenCalled();
  371. expect(eventsTraceLightMock).toHaveBeenCalled();
  372. const embeddedTransactionBadge = await screen.findByTestId(
  373. 'embedded-transaction-badge'
  374. );
  375. expect(embeddedTransactionBadge).toBeInTheDocument();
  376. await userEvent.click(embeddedTransactionBadge);
  377. expect(fetchEmbeddedTransactionMock1).toHaveBeenCalled();
  378. expect(fetchEmbeddedTransactionMock2).toHaveBeenCalled();
  379. expect(await screen.findByText(/i am embedded :\)/i)).toBeInTheDocument();
  380. expect(await screen.findByText(/i am also embedded :o/i)).toBeInTheDocument();
  381. });
  382. it('should correctly render sibling autogroup text when op and/or description is not provided', async () => {
  383. data = initializeData({});
  384. const builder1 = new TransactionEventBuilder();
  385. // Autogroup without span ops
  386. builder1.addSpan(
  387. new MockSpan({
  388. startTimestamp: 50,
  389. endTimestamp: 100,
  390. description: 'group me',
  391. }),
  392. 5
  393. );
  394. const {rerender} = render(
  395. <TraceView
  396. organization={data.organization}
  397. waterfallModel={new WaterfallModel(builder1.getEventFixture())}
  398. />
  399. );
  400. expect(await screen.findByTestId('span-row-2')).toHaveTextContent(
  401. /Autogrouped — group me/
  402. );
  403. // Autogroup without span descriptions
  404. const builder2 = new TransactionEventBuilder();
  405. builder2.addSpan(
  406. new MockSpan({
  407. startTimestamp: 100,
  408. endTimestamp: 200,
  409. op: 'http',
  410. }),
  411. 5
  412. );
  413. rerender(
  414. <TraceView
  415. organization={data.organization}
  416. waterfallModel={new WaterfallModel(builder2.getEventFixture())}
  417. />
  418. );
  419. expect(await screen.findByTestId('span-row-2')).toHaveTextContent(
  420. /Autogrouped — http/
  421. );
  422. // Autogroup without span ops or descriptions
  423. const builder3 = new TransactionEventBuilder();
  424. builder3.addSpan(
  425. new MockSpan({
  426. startTimestamp: 200,
  427. endTimestamp: 300,
  428. }),
  429. 5
  430. );
  431. rerender(
  432. <TraceView
  433. organization={data.organization}
  434. waterfallModel={new WaterfallModel(builder3.getEventFixture())}
  435. />
  436. );
  437. expect(await screen.findByTestId('span-row-2')).toHaveTextContent(
  438. /Autogrouped — siblings/
  439. );
  440. });
  441. it('should automatically expand a sibling span group and select a span if it is anchored', async () => {
  442. data = initializeData({});
  443. const builder = new TransactionEventBuilder();
  444. builder.addSpan(
  445. new MockSpan({
  446. startTimestamp: 100,
  447. endTimestamp: 200,
  448. op: 'http',
  449. description: 'group me',
  450. }),
  451. 5
  452. );
  453. // Manually set the hash here, the AnchorLinkManager is expected to automatically expand the group and scroll to the span with this id
  454. location.hash = spanTargetHash('0000000000000003');
  455. const waterfallModel = new WaterfallModel(builder.getEventFixture());
  456. render(
  457. <AnchorLinkManager.Provider>
  458. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  459. </AnchorLinkManager.Provider>
  460. );
  461. expect(await screen.findByText(/0000000000000003/i)).toBeInTheDocument();
  462. location.hash = '';
  463. });
  464. it('should automatically expand a descendant span group and select a span if it is anchored', async () => {
  465. data = initializeData({});
  466. const builder = new TransactionEventBuilder();
  467. const span = new MockSpan({
  468. startTimestamp: 50,
  469. endTimestamp: 100,
  470. op: 'http',
  471. description: 'group me',
  472. }).addDuplicateNestedChildren(5);
  473. builder.addSpan(span);
  474. location.hash = spanTargetHash('0000000000000003');
  475. const waterfallModel = new WaterfallModel(builder.getEventFixture());
  476. render(
  477. <AnchorLinkManager.Provider>
  478. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  479. </AnchorLinkManager.Provider>
  480. );
  481. expect(await screen.findByText(/0000000000000003/i)).toBeInTheDocument();
  482. location.hash = '';
  483. });
  484. });
  485. it('should merge web vitals labels if they are too close together', () => {
  486. data = initializeData({});
  487. const event = generateSampleEvent();
  488. generateSampleSpan('browser', 'test1', 'b000000000000000', 'a000000000000000', event);
  489. generateSampleSpan('browser', 'test2', 'c000000000000000', 'a000000000000000', event);
  490. generateSampleSpan('browser', 'test3', 'd000000000000000', 'a000000000000000', event);
  491. generateSampleSpan('browser', 'test4', 'e000000000000000', 'a000000000000000', event);
  492. generateSampleSpan('browser', 'test5', 'f000000000000000', 'a000000000000000', event);
  493. event.measurements = {
  494. fcp: {value: 1000},
  495. fp: {value: 1050},
  496. lcp: {value: 1100},
  497. };
  498. const waterfallModel = new WaterfallModel(event);
  499. render(
  500. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  501. );
  502. const labelContainer = screen.getByText(/fcp/i).parentElement?.parentElement;
  503. expect(labelContainer).toBeInTheDocument();
  504. expect(within(labelContainer!).getByText(/fcp/i)).toBeInTheDocument();
  505. expect(within(labelContainer!).getByText(/fp/i)).toBeInTheDocument();
  506. expect(within(labelContainer!).getByText(/lcp/i)).toBeInTheDocument();
  507. });
  508. it('should not merge web vitals labels if they are spaced away from each other', () => {
  509. data = initializeData({});
  510. const event = generateSampleEvent();
  511. generateSampleSpan('browser', 'test1', 'b000000000000000', 'a000000000000000', event);
  512. event.startTimestamp = 1;
  513. event.endTimestamp = 100;
  514. event.measurements = {
  515. fcp: {value: 858.3002090454102, unit: 'millisecond'},
  516. lcp: {value: 1000363.800048828125, unit: 'millisecond'},
  517. };
  518. const waterfallModel = new WaterfallModel(event);
  519. render(
  520. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  521. );
  522. const fcpLabelContainer = screen.getByText(/fcp/i).parentElement?.parentElement;
  523. expect(fcpLabelContainer).toBeInTheDocument();
  524. // LCP should not be merged along with FCP. We expect it to be in a separate element
  525. expect(within(fcpLabelContainer!).queryByText(/lcp/i)).not.toBeInTheDocument();
  526. const lcpLabelContainer = screen.getByText(/lcp/i).parentElement?.parentElement;
  527. expect(lcpLabelContainer).toBeInTheDocument();
  528. });
  529. it('should have all focused spans visible', async () => {
  530. data = initializeData({});
  531. const event = generateSampleEvent();
  532. for (let i = 0; i < 10; i++) {
  533. generateSampleSpan(`desc${i}`, 'db', `id${i}`, 'c000000000000000', event);
  534. }
  535. const waterfallModel = new WaterfallModel(event, ['id3'], ['id3', 'id4']);
  536. render(
  537. <TraceView organization={data.organization} waterfallModel={waterfallModel} />
  538. );
  539. expect(await screen.findByTestId('span-row-1')).toHaveTextContent('desc3');
  540. expect(await screen.findByTestId('span-row-2')).toHaveTextContent('desc4');
  541. expect(screen.queryByTestId('span-row-3')).not.toBeInTheDocument();
  542. });
  543. });