trace.spec.tsx 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804
  1. import * as Sentry from '@sentry/react';
  2. import MockDate from 'mockdate';
  3. import {TransactionEventFixture} from 'sentry-fixture/event';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {
  6. findAllByText,
  7. findByText,
  8. fireEvent,
  9. render,
  10. screen,
  11. userEvent,
  12. waitFor,
  13. within,
  14. } from 'sentry-test/reactTestingLibrary';
  15. import {EntryType, type EventTransaction} from 'sentry/types/event';
  16. import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
  17. import {TraceView} from 'sentry/views/performance/newTraceDetails/index';
  18. import {
  19. makeEventTransaction,
  20. makeSpan,
  21. makeTraceError,
  22. makeTransaction,
  23. } from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeTestUtils';
  24. import type {TracePreferencesState} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
  25. import {DEFAULT_TRACE_VIEW_PREFERENCES} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
  26. class MockResizeObserver {
  27. callback: ResizeObserverCallback;
  28. constructor(callback: ResizeObserverCallback) {
  29. this.callback = callback;
  30. }
  31. unobserve(_element: HTMLElement) {
  32. return;
  33. }
  34. observe(element: HTMLElement) {
  35. // Executes in sync so we dont have to
  36. this.callback(
  37. [
  38. {
  39. target: element,
  40. // @ts-expect-error partial mock
  41. contentRect: {width: 1000, height: 24 * 20 - 1},
  42. },
  43. ],
  44. this
  45. );
  46. }
  47. disconnect() {}
  48. }
  49. type Arguments<F extends Function> = F extends (...args: infer A) => any ? A : never;
  50. type ResponseType = Arguments<typeof MockApiClient.addMockResponse>[0];
  51. function mockQueryString(queryString: string) {
  52. Object.defineProperty(window, 'location', {
  53. value: {
  54. search: queryString,
  55. },
  56. });
  57. }
  58. function mockTracePreferences(preferences: Partial<TracePreferencesState>) {
  59. const merged: TracePreferencesState = {
  60. ...DEFAULT_TRACE_VIEW_PREFERENCES,
  61. ...preferences,
  62. autogroup: {
  63. ...DEFAULT_TRACE_VIEW_PREFERENCES.autogroup,
  64. ...preferences.autogroup,
  65. },
  66. drawer: {
  67. ...DEFAULT_TRACE_VIEW_PREFERENCES.drawer,
  68. ...preferences.drawer,
  69. },
  70. list: {
  71. ...DEFAULT_TRACE_VIEW_PREFERENCES.list,
  72. ...preferences.list,
  73. },
  74. };
  75. localStorage.setItem('trace-view-preferences', JSON.stringify(merged));
  76. }
  77. function mockTraceResponse(resp?: Partial<ResponseType>) {
  78. MockApiClient.addMockResponse({
  79. url: '/organizations/org-slug/events-trace/trace-id/',
  80. method: 'GET',
  81. asyncDelay: 1,
  82. ...(resp ?? {body: {}}),
  83. });
  84. }
  85. function mockTraceMetaResponse(resp?: Partial<ResponseType>) {
  86. MockApiClient.addMockResponse({
  87. url: '/organizations/org-slug/events-trace-meta/trace-id/',
  88. method: 'GET',
  89. asyncDelay: 1,
  90. ...(resp ?? {
  91. body: {
  92. errors: 0,
  93. performance_issues: 0,
  94. projects: 0,
  95. transactions: 0,
  96. transaction_child_count_map: [],
  97. },
  98. }),
  99. });
  100. }
  101. function mockTraceTagsResponse(resp?: Partial<ResponseType>) {
  102. MockApiClient.addMockResponse({
  103. url: '/organizations/org-slug/events-facets/',
  104. method: 'GET',
  105. asyncDelay: 1,
  106. ...(resp ?? {body: []}),
  107. });
  108. }
  109. // function _mockTraceDetailsResponse(id: string, resp?: Partial<ResponseType>) {
  110. // MockApiClient.addMockResponse({
  111. // url: `/organizations/org-slug/events/project_slug:transaction-${id}`,
  112. // method: 'GET',
  113. // asyncDelay: 1,
  114. // ...(resp ?? {}),
  115. // });
  116. // }
  117. function mockTransactionDetailsResponse(id: string, resp?: Partial<ResponseType>) {
  118. MockApiClient.addMockResponse({
  119. url: `/organizations/org-slug/events/project_slug:${id}/`,
  120. method: 'GET',
  121. asyncDelay: 1,
  122. ...(resp ?? {body: TransactionEventFixture()}),
  123. });
  124. }
  125. function mockTraceRootEvent(id: string, resp?: Partial<ResponseType>) {
  126. MockApiClient.addMockResponse({
  127. url: `/organizations/org-slug/events/project_slug:${id}/`,
  128. method: 'GET',
  129. asyncDelay: 1,
  130. ...(resp ?? {body: TransactionEventFixture()}),
  131. });
  132. }
  133. function mockTraceRootFacets(resp?: Partial<ResponseType>) {
  134. MockApiClient.addMockResponse({
  135. url: `/organizations/org-slug/events-facets/`,
  136. method: 'GET',
  137. asyncDelay: 1,
  138. body: {},
  139. ...(resp ?? {}),
  140. });
  141. }
  142. function mockTraceEventDetails(resp?: Partial<ResponseType>) {
  143. MockApiClient.addMockResponse({
  144. url: `/organizations/org-slug/events/`,
  145. method: 'GET',
  146. asyncDelay: 1,
  147. body: {},
  148. ...(resp ?? {body: TransactionEventFixture()}),
  149. });
  150. }
  151. function mockSpansResponse(
  152. id: string,
  153. resp?: Partial<ResponseType>,
  154. body: Partial<EventTransaction> = {}
  155. ) {
  156. return MockApiClient.addMockResponse({
  157. url: `/organizations/org-slug/events/project_slug:${id}/?averageColumn=span.self_time&averageColumn=span.duration`,
  158. method: 'GET',
  159. asyncDelay: 1,
  160. body,
  161. ...(resp ?? {}),
  162. });
  163. }
  164. function mockTransactionSpansResponse(
  165. id: string,
  166. resp?: Partial<ResponseType>,
  167. body: Partial<EventTransaction> = {}
  168. ) {
  169. return MockApiClient.addMockResponse({
  170. url: `/organizations/org-slug/events/project_slug:${id}/`,
  171. method: 'GET',
  172. asyncDelay: 1,
  173. body,
  174. ...(resp ?? {}),
  175. });
  176. }
  177. const {router} = initializeOrg({
  178. router: {
  179. params: {orgId: 'org-slug', traceSlug: 'trace-id'},
  180. },
  181. });
  182. function mockMetricsResponse() {
  183. MockApiClient.addMockResponse({
  184. url: '/organizations/org-slug/metrics/query/',
  185. method: 'POST',
  186. body: {
  187. data: [],
  188. queries: [],
  189. },
  190. });
  191. }
  192. function getVirtualizedContainer(): HTMLElement {
  193. const virtualizedContainer = screen.queryByTestId('trace-virtualized-list');
  194. if (!virtualizedContainer) {
  195. throw new Error('Virtualized container not found');
  196. }
  197. return virtualizedContainer;
  198. }
  199. function getVirtualizedScrollContainer(): HTMLElement {
  200. const virtualizedScrollContainer = screen.queryByTestId(
  201. 'trace-virtualized-list-scroll-container'
  202. );
  203. if (!virtualizedScrollContainer) {
  204. throw new Error('Virtualized scroll container not found');
  205. }
  206. return virtualizedScrollContainer;
  207. }
  208. async function keyboardNavigationTestSetup() {
  209. const keyboard_navigation_transactions: TraceFullDetailed[] = [];
  210. for (let i = 0; i < 1e4; i++) {
  211. keyboard_navigation_transactions.push(
  212. makeTransaction({
  213. span_id: i + '',
  214. event_id: i + '',
  215. transaction: 'transaction-name-' + i,
  216. 'transaction.op': 'transaction-op-' + i,
  217. project_slug: 'project_slug',
  218. })
  219. );
  220. mockTransactionDetailsResponse(i.toString());
  221. }
  222. mockTraceResponse({
  223. body: {
  224. transactions: keyboard_navigation_transactions,
  225. orphan_errors: [],
  226. },
  227. });
  228. mockTraceMetaResponse({
  229. body: {
  230. errors: 0,
  231. performance_issues: 0,
  232. projects: 0,
  233. transactions: 0,
  234. transaction_child_count_map: keyboard_navigation_transactions.map(t => ({
  235. 'transaction.id': t.event_id,
  236. count: 5,
  237. })),
  238. },
  239. });
  240. mockTraceRootFacets();
  241. mockTraceRootEvent('0');
  242. mockTraceEventDetails();
  243. mockMetricsResponse();
  244. const value = render(<TraceView />, {router});
  245. const virtualizedContainer = getVirtualizedContainer();
  246. const virtualizedScrollContainer = getVirtualizedScrollContainer();
  247. // Awaits for the placeholder rendering rows to be removed
  248. expect(await findByText(value.container, /transaction-op-0/i)).toBeInTheDocument();
  249. return {...value, virtualizedContainer, virtualizedScrollContainer};
  250. }
  251. async function pageloadTestSetup() {
  252. const pageloadTransactions: TraceFullDetailed[] = [];
  253. for (let i = 0; i < 1e3; i++) {
  254. pageloadTransactions.push(
  255. makeTransaction({
  256. span_id: i + '',
  257. event_id: i + '',
  258. transaction: 'transaction-name-' + i,
  259. 'transaction.op': 'transaction-op-' + i,
  260. project_slug: 'project_slug',
  261. })
  262. );
  263. mockTransactionDetailsResponse(i.toString());
  264. }
  265. mockTraceResponse({
  266. body: {
  267. transactions: pageloadTransactions,
  268. orphan_errors: [],
  269. },
  270. });
  271. mockTraceMetaResponse({
  272. body: {
  273. errors: 0,
  274. performance_issues: 0,
  275. projects: 0,
  276. transactions: 0,
  277. transaction_child_count_map: pageloadTransactions.map(t => ({
  278. 'transaction.id': t.event_id,
  279. count: 5,
  280. })),
  281. },
  282. });
  283. mockTraceRootFacets();
  284. mockTraceRootEvent('0');
  285. mockTraceEventDetails();
  286. mockMetricsResponse();
  287. const value = render(<TraceView />, {router});
  288. const virtualizedContainer = getVirtualizedContainer();
  289. const virtualizedScrollContainer = getVirtualizedScrollContainer();
  290. // Awaits for the placeholder rendering rows to be removed
  291. expect((await screen.findAllByText(/transaction-op-/i)).length).toBeGreaterThan(0);
  292. return {...value, virtualizedContainer, virtualizedScrollContainer};
  293. }
  294. async function nestedTransactionsTestSetup() {
  295. const transactions: TraceFullDetailed[] = [];
  296. let txn = makeTransaction({
  297. span_id: '0',
  298. event_id: '0',
  299. transaction: 'transaction-name-0',
  300. 'transaction.op': 'transaction-op-0',
  301. project_slug: 'project_slug',
  302. });
  303. transactions.push(txn);
  304. for (let i = 0; i < 100; i++) {
  305. const next = makeTransaction({
  306. span_id: i + '',
  307. event_id: i + '',
  308. transaction: 'transaction-name-' + i,
  309. 'transaction.op': 'transaction-op-' + i,
  310. project_slug: 'project_slug',
  311. });
  312. txn.children.push(next);
  313. txn = next;
  314. transactions.push(next);
  315. mockTransactionDetailsResponse(i.toString());
  316. }
  317. mockTraceResponse({
  318. body: {
  319. transactions: transactions,
  320. orphan_errors: [],
  321. },
  322. });
  323. mockTraceMetaResponse();
  324. mockTraceRootFacets();
  325. mockTraceRootEvent('0');
  326. mockTraceEventDetails();
  327. mockMetricsResponse();
  328. const value = render(<TraceView />, {router});
  329. const virtualizedContainer = getVirtualizedContainer();
  330. const virtualizedScrollContainer = getVirtualizedScrollContainer();
  331. // Awaits for the placeholder rendering rows to be removed
  332. expect((await screen.findAllByText(/transaction-op-/i)).length).toBeGreaterThan(0);
  333. return {...value, virtualizedContainer, virtualizedScrollContainer};
  334. }
  335. async function searchTestSetup() {
  336. const transactions: TraceFullDetailed[] = [];
  337. for (let i = 0; i < 11; i++) {
  338. transactions.push(
  339. makeTransaction({
  340. span_id: i + '',
  341. event_id: i + '',
  342. transaction: 'transaction-name' + i,
  343. 'transaction.op': 'transaction-op-' + i,
  344. project_slug: 'project_slug',
  345. })
  346. );
  347. mockTransactionDetailsResponse(i.toString());
  348. }
  349. mockTraceResponse({
  350. body: {
  351. transactions: transactions,
  352. orphan_errors: [],
  353. },
  354. });
  355. mockTraceMetaResponse({
  356. body: {
  357. errors: 0,
  358. performance_issues: 0,
  359. projects: 0,
  360. transactions: 0,
  361. transaction_child_count_map: transactions.map(t => ({
  362. 'transaction.id': t.event_id,
  363. count: 5,
  364. })),
  365. },
  366. });
  367. mockTraceRootFacets();
  368. mockTraceRootEvent('0');
  369. mockTraceEventDetails();
  370. mockMetricsResponse();
  371. const value = render(<TraceView />, {router});
  372. const virtualizedContainer = getVirtualizedContainer();
  373. const virtualizedScrollContainer = getVirtualizedScrollContainer();
  374. // Awaits for the placeholder rendering rows to be removed
  375. expect(await findByText(value.container, /transaction-op-0/i)).toBeInTheDocument();
  376. return {...value, virtualizedContainer, virtualizedScrollContainer};
  377. }
  378. async function simpleTestSetup() {
  379. const transactions: TraceFullDetailed[] = [];
  380. let parent: any;
  381. for (let i = 0; i < 1e3; i++) {
  382. const next = makeTransaction({
  383. span_id: i + '',
  384. event_id: i + '',
  385. transaction: 'transaction-name' + i,
  386. 'transaction.op': 'transaction-op-' + i,
  387. project_slug: 'project_slug',
  388. });
  389. if (parent) {
  390. parent.children.push(next);
  391. } else {
  392. transactions.push(next);
  393. }
  394. parent = next;
  395. mockTransactionDetailsResponse(i.toString());
  396. }
  397. mockTraceResponse({
  398. body: {
  399. transactions: transactions,
  400. orphan_errors: [],
  401. },
  402. });
  403. mockTraceMetaResponse({
  404. body: {
  405. errors: 0,
  406. performance_issues: 0,
  407. projects: 0,
  408. transactions: 0,
  409. transaction_child_count_map: transactions.map(t => ({
  410. 'transaction.id': t.event_id,
  411. count: 5,
  412. })),
  413. },
  414. });
  415. mockTraceRootFacets();
  416. mockTraceRootEvent('0');
  417. mockTraceEventDetails();
  418. mockMetricsResponse();
  419. const value = render(<TraceView />, {router});
  420. const virtualizedContainer = getVirtualizedContainer();
  421. const virtualizedScrollContainer = getVirtualizedScrollContainer();
  422. // Awaits for the placeholder rendering rows to be removed
  423. expect(await findByText(value.container, /transaction-op-0/i)).toBeInTheDocument();
  424. return {...value, virtualizedContainer, virtualizedScrollContainer};
  425. }
  426. async function completeTestSetup() {
  427. const start = Date.now() / 1e3;
  428. mockTraceResponse({
  429. body: {
  430. transactions: [
  431. makeTransaction({
  432. event_id: '0',
  433. transaction: 'transaction-name-0',
  434. 'transaction.op': 'transaction-op-0',
  435. project_slug: 'project_slug',
  436. start_timestamp: start,
  437. timestamp: start + 2,
  438. children: [
  439. makeTransaction({
  440. event_id: '1',
  441. transaction: 'transaction-name-1',
  442. 'transaction.op': 'transaction-op-1',
  443. project_slug: 'project_slug',
  444. start_timestamp: start,
  445. timestamp: start + 2,
  446. }),
  447. ],
  448. }),
  449. makeTransaction({
  450. event_id: '2',
  451. transaction: 'transaction-name-2',
  452. 'transaction.op': 'transaction-op-2',
  453. project_slug: 'project_slug',
  454. start_timestamp: start,
  455. timestamp: start + 2,
  456. }),
  457. makeTransaction({
  458. event_id: '3',
  459. transaction: 'transaction-name-3',
  460. 'transaction.op': 'transaction-op-3',
  461. project_slug: 'project_slug',
  462. start_timestamp: start,
  463. timestamp: start + 2,
  464. }),
  465. ],
  466. orphan_errors: [
  467. makeTraceError({
  468. event_id: 'error0',
  469. issue: 'error-issue',
  470. project_id: 0,
  471. project_slug: 'project_slug',
  472. issue_id: 0,
  473. title: 'error-title',
  474. level: 'fatal',
  475. timestamp: start + 2,
  476. }),
  477. ],
  478. },
  479. });
  480. mockTraceMetaResponse({
  481. body: {
  482. errors: 0,
  483. performance_issues: 0,
  484. projects: 0,
  485. transactions: 0,
  486. transaction_child_count_map: [
  487. {
  488. 'transaction.id': '0',
  489. count: 2,
  490. },
  491. {
  492. 'transaction.id': '1',
  493. count: 2,
  494. },
  495. {
  496. 'transaction.id': '2',
  497. count: 2,
  498. },
  499. {
  500. 'transaction.id': '3',
  501. count: 2,
  502. },
  503. ],
  504. },
  505. });
  506. mockTraceRootFacets();
  507. mockTraceRootEvent('0');
  508. mockTraceEventDetails();
  509. mockMetricsResponse();
  510. MockApiClient.addMockResponse({
  511. url: '/organizations/org-slug/events/project_slug:error0/',
  512. body: {
  513. tags: [],
  514. contexts: {},
  515. entries: [],
  516. },
  517. });
  518. const transactionWithSpans = makeEventTransaction({
  519. entries: [
  520. {
  521. type: EntryType.SPANS,
  522. data: [
  523. makeSpan({
  524. span_id: 'span0',
  525. op: 'http',
  526. description: 'request',
  527. start_timestamp: start,
  528. timestamp: start + 0.1,
  529. }),
  530. // Parent autogroup chain
  531. makeSpan({
  532. op: 'db',
  533. description: 'redis',
  534. parent_span_id: 'span0',
  535. span_id: 'redis0',
  536. start_timestamp: start + 0.1,
  537. timestamp: start + 0.2,
  538. }),
  539. makeSpan({
  540. op: 'db',
  541. description: 'redis',
  542. parent_span_id: 'redis0',
  543. span_id: 'redis1',
  544. start_timestamp: start + 0.2,
  545. timestamp: start + 0.3,
  546. }),
  547. // Sibling autogroup chain
  548. makeSpan({
  549. op: 'http',
  550. description: 'request',
  551. parent_span_id: 'span0',
  552. span_id: 'http0',
  553. start_timestamp: start + 0.3,
  554. timestamp: start + 0.4,
  555. }),
  556. makeSpan({
  557. op: 'http',
  558. description: 'request',
  559. parent_span_id: 'span0',
  560. span_id: 'http1',
  561. start_timestamp: start + 0.4,
  562. timestamp: start + 0.5,
  563. }),
  564. makeSpan({
  565. op: 'http',
  566. description: 'request',
  567. parent_span_id: 'span0',
  568. span_id: 'http2',
  569. start_timestamp: start + 0.5,
  570. timestamp: start + 0.6,
  571. }),
  572. makeSpan({
  573. op: 'http',
  574. description: 'request',
  575. parent_span_id: 'span0',
  576. span_id: 'http3',
  577. start_timestamp: start + 0.6,
  578. timestamp: start + 0.7,
  579. }),
  580. makeSpan({
  581. op: 'http',
  582. description: 'request',
  583. parent_span_id: 'span0',
  584. span_id: 'http4',
  585. start_timestamp: start + 0.7,
  586. timestamp: start + 0.8,
  587. }),
  588. // Missing instrumentation gap
  589. makeSpan({
  590. op: 'queue',
  591. description: 'process',
  592. parent_span_id: 'span0',
  593. span_id: 'queueprocess0',
  594. start_timestamp: start + 0.8,
  595. timestamp: start + 0.9,
  596. }),
  597. makeSpan({
  598. op: 'queue',
  599. description: 'process',
  600. parent_span_id: 'span0',
  601. span_id: 'queueprocess1',
  602. start_timestamp: start + 1.1,
  603. timestamp: start + 1.2,
  604. }),
  605. ],
  606. },
  607. ],
  608. });
  609. const transactionWithoutSpans = makeEventTransaction({});
  610. mockTransactionSpansResponse('1', {}, transactionWithSpans);
  611. mockSpansResponse('1', {}, transactionWithSpans);
  612. // Mock empty response for txn without spans
  613. mockTransactionSpansResponse('0', {}, transactionWithoutSpans);
  614. mockSpansResponse('0', {}, transactionWithoutSpans);
  615. const value = render(<TraceView />, {router});
  616. const virtualizedContainer = getVirtualizedContainer();
  617. const virtualizedScrollContainer = getVirtualizedScrollContainer();
  618. // Awaits for the placeholder rendering rows to be removed
  619. expect(await findByText(value.container, /transaction-op-0/i)).toBeInTheDocument();
  620. return {...value, virtualizedContainer, virtualizedScrollContainer};
  621. }
  622. const DRAWER_TABS_TEST_ID = 'trace-drawer-tab';
  623. const DRAWER_TABS_PIN_BUTTON_TEST_ID = 'trace-drawer-tab-pin-button';
  624. const VISIBLE_TRACE_ROW_SELECTOR = '.TraceRow:not(.Hidden)';
  625. const ACTIVE_SEARCH_HIGHLIGHT_ROW = '.TraceRow.SearchResult.Highlight:not(.Hidden)';
  626. const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
  627. const searchToUpdate = async (): Promise<void> => {
  628. await wait(500);
  629. };
  630. const scrollToEnd = async (): Promise<void> => {
  631. await wait(500);
  632. };
  633. // @ts-expect-error ignore this line
  634. // eslint-disable-next-line
  635. function printVirtualizedList(container: HTMLElement) {
  636. const stdout: string[] = [];
  637. const scrollContainer = screen.queryByTestId(
  638. 'trace-virtualized-list-scroll-container'
  639. )!;
  640. const rows = Array.from(container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR));
  641. const searchResultIterator = screen.queryByTestId('trace-search-result-iterator');
  642. stdout.push(
  643. 'Scroll Container: ' +
  644. 'top=' +
  645. scrollContainer.scrollTop +
  646. ' ' +
  647. 'left=' +
  648. scrollContainer.scrollLeft +
  649. ' ' +
  650. rows.length +
  651. ' ' +
  652. 'Search:' +
  653. (searchResultIterator?.textContent ?? '')
  654. );
  655. for (const r of [...rows]) {
  656. const count = (r.querySelector('.TraceChildrenCount') as HTMLElement)?.textContent;
  657. const op = (r.querySelector('.TraceOperation') as HTMLElement)?.textContent;
  658. const desc = (r.querySelector('.TraceDescription') as HTMLElement)?.textContent;
  659. let t = (count ?? '') + ' ' + (op ?? '') + ' — ' + (desc ?? '');
  660. if (r.classList.contains('SearchResult')) {
  661. t = t + ' search';
  662. }
  663. if (r.classList.contains('Highlight')) {
  664. t = t + ' highlight';
  665. }
  666. if (document.activeElement === r) {
  667. t = t + ' ⬅ focused ';
  668. }
  669. const leftColumn = r.querySelector('.TraceLeftColumnInner') as HTMLElement;
  670. const left = Math.round(parseInt(leftColumn.style.paddingLeft, 10) / 10);
  671. stdout.push(' '.repeat(left) + t);
  672. }
  673. // This is a debug fn, we need it to log
  674. // eslint-disable-next-line
  675. console.log(stdout.join('\n'));
  676. }
  677. // @ts-expect-error ignore this line
  678. // eslint-disable-next-line
  679. function printTabs() {
  680. const tabs = screen.queryAllByTestId(DRAWER_TABS_TEST_ID);
  681. const stdout: string[] = [];
  682. for (const tab of tabs) {
  683. let text = tab.textContent ?? 'empty tab??';
  684. if (tab.hasAttribute('aria-selected')) {
  685. text = 'active' + text;
  686. }
  687. stdout.push(text);
  688. }
  689. // This is a debug fn, we need it to log
  690. // eslint-disable-next-line
  691. console.log(stdout.join(' | '));
  692. }
  693. function assertHighlightedRowAtIndex(virtualizedContainer: HTMLElement, index: number) {
  694. expect(virtualizedContainer.querySelectorAll('.TraceRow.Highlight')).toHaveLength(1);
  695. const highlighted_row = virtualizedContainer.querySelector(ACTIVE_SEARCH_HIGHLIGHT_ROW);
  696. const r = Array.from(virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR));
  697. expect(r.indexOf(highlighted_row!)).toBe(index);
  698. }
  699. // biome-ignore lint/suspicious/noSkippedTests: <Flaky test>
  700. describe.skip('trace view', () => {
  701. beforeEach(() => {
  702. jest.spyOn(console, 'error').mockImplementation(() => {});
  703. globalThis.ResizeObserver = MockResizeObserver as any;
  704. mockQueryString('');
  705. MockDate.reset();
  706. });
  707. afterEach(() => {
  708. mockQueryString('');
  709. // @ts-expect-error clear mock
  710. globalThis.ResizeObserver = undefined;
  711. });
  712. it('renders loading state', async () => {
  713. mockTraceResponse();
  714. mockTraceMetaResponse();
  715. mockTraceTagsResponse();
  716. render(<TraceView />, {router});
  717. expect(await screen.findByText(/assembling the trace/i)).toBeInTheDocument();
  718. });
  719. it('renders error state if trace fails to load', async () => {
  720. mockTraceResponse({statusCode: 404});
  721. mockTraceMetaResponse({statusCode: 404});
  722. mockTraceTagsResponse({statusCode: 404});
  723. render(<TraceView />, {router});
  724. expect(await screen.findByText(/we failed to load your trace/i)).toBeInTheDocument();
  725. });
  726. it('renders error state if meta fails to load', async () => {
  727. mockTraceResponse({
  728. statusCode: 200,
  729. body: {
  730. transactions: [makeTransaction()],
  731. orphan_errors: [],
  732. },
  733. });
  734. mockTraceMetaResponse({statusCode: 404});
  735. mockTraceTagsResponse({statusCode: 404});
  736. render(<TraceView />, {router});
  737. expect(await screen.findByText(/we failed to load your trace/i)).toBeInTheDocument();
  738. });
  739. it('renders empty state', async () => {
  740. mockTraceResponse({
  741. body: {
  742. transactions: [],
  743. orphan_errors: [],
  744. },
  745. });
  746. mockTraceMetaResponse();
  747. mockTraceTagsResponse();
  748. render(<TraceView />, {router});
  749. expect(
  750. await screen.findByText(/trace does not contain any data/i)
  751. ).toBeInTheDocument();
  752. });
  753. describe('pageload', () => {
  754. it('scrolls to trace root', async () => {
  755. mockQueryString('?node=trace-root');
  756. const {virtualizedContainer} = await completeTestSetup();
  757. await waitFor(() => {
  758. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  759. expect(rows[0]).toHaveFocus();
  760. });
  761. });
  762. it('scrolls to transaction', async () => {
  763. mockQueryString('?node=txn-1');
  764. const {virtualizedContainer} = await completeTestSetup();
  765. await waitFor(() => {
  766. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  767. expect(rows[2]).toHaveFocus();
  768. });
  769. });
  770. it('scrolls to span that is a child of transaction', async () => {
  771. mockQueryString('?node=span-span0&node=txn-1');
  772. const {virtualizedContainer} = await completeTestSetup();
  773. await findAllByText(virtualizedContainer, /Autogrouped/i);
  774. await waitFor(() => {
  775. // We need to await a tick because the row is not focused until the next tick
  776. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  777. expect(rows[3]).toHaveFocus();
  778. expect(rows[3].textContent?.includes('http — request')).toBe(true);
  779. });
  780. });
  781. it('scrolls to parent autogroup node', async () => {
  782. mockQueryString('?node=ag-redis0&node=txn-1');
  783. const {virtualizedContainer} = await completeTestSetup();
  784. await findAllByText(virtualizedContainer, /Autogrouped/i);
  785. await waitFor(() => {
  786. // We need to await a tick because the row is not focused until the next tick
  787. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  788. expect(rows[4]).toHaveFocus();
  789. expect(rows[4].textContent?.includes('Autogrouped')).toBe(true);
  790. });
  791. });
  792. it('scrolls to child of parent autogroup node', async () => {
  793. mockQueryString('?node=span-redis0&node=txn-1');
  794. const {virtualizedContainer} = await completeTestSetup();
  795. await findAllByText(virtualizedContainer, /Autogrouped/i);
  796. await waitFor(() => {
  797. // We need to await a tick because the row is not focused until the next tick
  798. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  799. expect(rows[5]).toHaveFocus();
  800. expect(rows[5].textContent?.includes('db — redis')).toBe(true);
  801. });
  802. });
  803. it('scrolls to sibling autogroup node', async () => {
  804. mockQueryString('?node=ag-http0&node=txn-1');
  805. const {virtualizedContainer} = await completeTestSetup();
  806. await findAllByText(virtualizedContainer, /Autogrouped/i);
  807. await waitFor(() => {
  808. // We need to await a tick because the row is not focused until the next tick
  809. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  810. expect(rows[5]).toHaveFocus();
  811. expect(rows[5].textContent?.includes('5Autogrouped')).toBe(true);
  812. });
  813. });
  814. it('scrolls to child of sibling autogroup node', async () => {
  815. mockQueryString('?node=span-http0&node=txn-1');
  816. const {virtualizedContainer} = await completeTestSetup();
  817. await findAllByText(virtualizedContainer, /Autogrouped/i);
  818. await waitFor(() => {
  819. // We need to await a tick because the row is not focused until the next tick
  820. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  821. expect(rows[6]).toHaveFocus();
  822. expect(rows[6].textContent?.includes('http — request')).toBe(true);
  823. });
  824. });
  825. it('scrolls to missing instrumentation node', async () => {
  826. mockQueryString('?node=ms-queueprocess0&node=txn-1');
  827. const {virtualizedContainer} = await completeTestSetup();
  828. await findAllByText(virtualizedContainer, /Autogrouped/i);
  829. // We need to await a tick because the row is not focused until the next ticks
  830. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  831. await waitFor(() => {
  832. expect(rows[7]).toHaveFocus();
  833. expect(rows[7].textContent?.includes('Missing instrumentation')).toBe(true);
  834. });
  835. });
  836. it('scrolls to trace error node', async () => {
  837. mockQueryString('?node=error-error0&node=txn-1');
  838. const {virtualizedContainer} = await completeTestSetup();
  839. await findAllByText(virtualizedContainer, /Autogrouped/i);
  840. await waitFor(() => {
  841. // We need to await a tick because the row is not focused until the next ticks
  842. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  843. expect(rows[11]).toHaveFocus();
  844. expect(rows[11].textContent?.includes('error-title')).toBe(true);
  845. });
  846. });
  847. it('scrolls to event id query param', async () => {
  848. mockQueryString('?eventId=1');
  849. const {virtualizedContainer} = await completeTestSetup();
  850. await waitFor(() => {
  851. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  852. expect(rows[2]).toHaveFocus();
  853. });
  854. });
  855. it('supports expanded node path', async () => {
  856. mockQueryString('?node=span-span0&node=txn-1&span-0&node=txn-0');
  857. const {virtualizedContainer} = await completeTestSetup();
  858. await findAllByText(virtualizedContainer, /Autogrouped/i);
  859. await waitFor(() => {
  860. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  861. expect(rows[3]).toHaveFocus();
  862. expect(rows[3].textContent?.includes('http — request')).toBe(true);
  863. });
  864. });
  865. it.each([
  866. '?eventId=doesnotexist',
  867. '?node=txn-doesnotexist',
  868. // Invalid path
  869. '?node=span-does-notexist',
  870. ])('logs if path is not found: %s', async path => {
  871. mockQueryString(path);
  872. const sentryScopeMock = {
  873. setFingerprint: jest.fn(),
  874. captureMessage: jest.fn(),
  875. } as any;
  876. jest.spyOn(Sentry, 'withScope').mockImplementation((f: any) => f(sentryScopeMock));
  877. await pageloadTestSetup();
  878. await waitFor(() => {
  879. expect(sentryScopeMock.captureMessage).toHaveBeenCalledWith(
  880. 'Failed to scroll to node in trace tree'
  881. );
  882. });
  883. });
  884. it('does not autogroup if user preference is disabled', async () => {
  885. mockTracePreferences({autogroup: {parent: false, sibling: false}});
  886. mockQueryString('?node=span-span0&node=txn-1');
  887. const {virtualizedContainer} = await completeTestSetup();
  888. await findAllByText(virtualizedContainer, /process/i);
  889. expect(screen.queryByText(/Autogrouped/i)).not.toBeInTheDocument();
  890. });
  891. it('does not inject missing instrumentation if user preference is disabled', async () => {
  892. mockTracePreferences({missing_instrumentation: false});
  893. mockQueryString('?node=span-span0&node=txn-1');
  894. const {virtualizedContainer} = await completeTestSetup();
  895. await findAllByText(virtualizedContainer, /process/i);
  896. expect(screen.queryByText(/Missing instrumentation/i)).not.toBeInTheDocument();
  897. });
  898. // biome-ignore lint/suspicious/noSkippedTests: @JonasBa will fix these flakey tests soon
  899. describe.skip('preferences', () => {
  900. it('toggles autogrouping', async () => {
  901. mockTracePreferences({autogroup: {parent: true, sibling: true}});
  902. mockQueryString('?node=span-span0&node=txn-1');
  903. const {virtualizedContainer} = await completeTestSetup();
  904. await findAllByText(virtualizedContainer, /Autogrouped/i);
  905. const preferencesDropdownTrigger = screen.getByLabelText('Trace Preferences');
  906. await userEvent.click(preferencesDropdownTrigger);
  907. // Toggle autogrouping off
  908. await userEvent.click(await screen.findByText('Autogrouping'));
  909. await waitFor(() => {
  910. expect(screen.queryByText('Autogrouped')).not.toBeInTheDocument();
  911. });
  912. // Toggle autogrouping on
  913. await userEvent.click(await screen.findByText('Autogrouping'));
  914. await waitFor(() => {
  915. expect(screen.queryAllByText('Autogrouped')).toHaveLength(2);
  916. });
  917. });
  918. it('toggles missing instrumentation', async () => {
  919. mockTracePreferences({missing_instrumentation: true});
  920. mockQueryString('?node=span-span0&node=txn-1');
  921. const {virtualizedContainer} = await completeTestSetup();
  922. await findAllByText(virtualizedContainer, /Missing instrumentation/i);
  923. const preferencesDropdownTrigger = screen.getByLabelText('Trace Preferences');
  924. // Toggle missing instrumentation off
  925. await userEvent.click(preferencesDropdownTrigger);
  926. const missingInstrumentationOption = await screen.findByText(
  927. 'Missing Instrumentation'
  928. );
  929. // Toggle missing instrumentation off
  930. await userEvent.click(missingInstrumentationOption);
  931. await waitFor(() => {
  932. expect(screen.queryByText('Missing instrumentation')).not.toBeInTheDocument();
  933. });
  934. // Toggle missing instrumentation on
  935. await userEvent.click(missingInstrumentationOption);
  936. await waitFor(() => {
  937. expect(screen.queryByText('Missing instrumentation')).toBeInTheDocument();
  938. });
  939. });
  940. });
  941. it('triggers search on load but does not steal focus from node param', async () => {
  942. mockQueryString('?search=transaction-op-999&node=txn-0');
  943. const {virtualizedContainer} = await pageloadTestSetup();
  944. const searchInput = await screen.findByPlaceholderText('Search in trace');
  945. expect(searchInput).toHaveValue('transaction-op-999');
  946. await waitFor(() => {
  947. expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent(
  948. '-/1'
  949. );
  950. });
  951. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  952. expect(rows[1]).toHaveFocus();
  953. });
  954. it('if search on load does not match anything, it does not steal focus or highlight first result', async () => {
  955. mockQueryString('?search=dead&node=txn-5');
  956. const {container} = await pageloadTestSetup();
  957. const searchInput = await screen.findByPlaceholderText('Search in trace');
  958. expect(searchInput).toHaveValue('dead');
  959. await waitFor(() => {
  960. expect(screen.getByTestId('trace-search-result-iterator')).toHaveTextContent(
  961. 'no results'
  962. );
  963. });
  964. const rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  965. expect(rows[6]).toHaveFocus();
  966. });
  967. });
  968. describe('keyboard navigation', () => {
  969. it('arrow down', async () => {
  970. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  971. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  972. await userEvent.click(rows[0]);
  973. await waitFor(() => expect(rows[0]).toHaveFocus());
  974. await userEvent.keyboard('{arrowdown}');
  975. await waitFor(() => expect(rows[1]).toHaveFocus());
  976. });
  977. it('arrow up', async () => {
  978. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  979. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  980. await userEvent.click(rows[1]);
  981. await waitFor(() => expect(rows[1]).toHaveFocus());
  982. await userEvent.keyboard('{arrowup}');
  983. await waitFor(() => expect(rows[0]).toHaveFocus());
  984. });
  985. it('arrow right expands row and fetches data', async () => {
  986. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  987. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  988. mockSpansResponse(
  989. '0',
  990. {},
  991. {
  992. entries: [
  993. {type: EntryType.SPANS, data: [makeSpan({span_id: '0', op: 'special-span'})]},
  994. ],
  995. }
  996. );
  997. await userEvent.click(rows[1]);
  998. await waitFor(() => expect(rows[1]).toHaveFocus());
  999. await userEvent.keyboard('{arrowright}');
  1000. await waitFor(() => {
  1001. expect(screen.queryByText('special-span')).toBeInTheDocument();
  1002. });
  1003. });
  1004. it('arrow left collapses row', async () => {
  1005. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  1006. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1007. const request = mockSpansResponse(
  1008. '0',
  1009. {},
  1010. {
  1011. entries: [
  1012. {type: EntryType.SPANS, data: [makeSpan({span_id: '0', op: 'special-span'})]},
  1013. ],
  1014. }
  1015. );
  1016. await userEvent.click(rows[1]);
  1017. await waitFor(() => expect(rows[1]).toHaveFocus());
  1018. await userEvent.keyboard('{arrowright}');
  1019. expect(request).toHaveBeenCalledTimes(1);
  1020. expect(await screen.findByText('special-span')).toBeInTheDocument();
  1021. await userEvent.keyboard('{arrowleft}');
  1022. expect(screen.queryByText('special-span')).not.toBeInTheDocument();
  1023. });
  1024. it('arrow left does not collapse trace root row', async () => {
  1025. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  1026. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1027. await userEvent.click(rows[0]);
  1028. await waitFor(() => expect(rows[0]).toHaveFocus());
  1029. await userEvent.keyboard('{arrowleft}');
  1030. expect(await screen.findByText('transaction-name-1')).toBeInTheDocument();
  1031. });
  1032. it('arrow left on transaction row still renders transaction children', async () => {
  1033. const {virtualizedContainer} = await nestedTransactionsTestSetup();
  1034. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1035. await userEvent.click(rows[1]);
  1036. await waitFor(() => expect(rows[1]).toHaveFocus());
  1037. await userEvent.keyboard('{arrowleft}');
  1038. expect(await screen.findByText('transaction-name-2')).toBeInTheDocument();
  1039. });
  1040. it('roving updates the element in the drawer', async () => {
  1041. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  1042. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1043. mockSpansResponse(
  1044. '0',
  1045. {},
  1046. {
  1047. entries: [
  1048. {type: EntryType.SPANS, data: [makeSpan({span_id: '0', op: 'special-span'})]},
  1049. ],
  1050. }
  1051. );
  1052. await userEvent.click(rows[1]);
  1053. await waitFor(() => expect(rows[1]).toHaveFocus());
  1054. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  1055. 'transaction-op-0'
  1056. );
  1057. await userEvent.keyboard('{arrowright}');
  1058. expect(await screen.findByText('special-span')).toBeInTheDocument();
  1059. await userEvent.keyboard('{arrowdown}');
  1060. await waitFor(() => {
  1061. const updatedRows = virtualizedContainer.querySelectorAll(
  1062. VISIBLE_TRACE_ROW_SELECTOR
  1063. );
  1064. expect(updatedRows[2]).toHaveFocus();
  1065. });
  1066. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  1067. 'special-span'
  1068. );
  1069. });
  1070. it('arrowup on first node jumps to end', async () => {
  1071. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  1072. let rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1073. await userEvent.click(rows[0]);
  1074. await waitFor(() => expect(rows[0]).toHaveFocus());
  1075. await userEvent.keyboard('{arrowup}');
  1076. expect(
  1077. await findByText(virtualizedContainer, /transaction-op-9999/i)
  1078. ).toBeInTheDocument();
  1079. await waitFor(() => {
  1080. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1081. expect(rows[rows.length - 1]).toHaveFocus();
  1082. });
  1083. });
  1084. it('arrowdown on last node jumps to start', async () => {
  1085. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  1086. let rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1087. await userEvent.click(rows[0]);
  1088. await waitFor(() => expect(rows[0]).toHaveFocus());
  1089. await userEvent.keyboard('{arrowup}', {delay: null});
  1090. await waitFor(() => {
  1091. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1092. expect(rows[rows.length - 1]).toHaveFocus();
  1093. });
  1094. expect(
  1095. await within(virtualizedContainer).findByText(/transaction-op-9999/i)
  1096. ).toBeInTheDocument();
  1097. await userEvent.keyboard('{arrowdown}', {delay: null});
  1098. await waitFor(() => {
  1099. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1100. expect(rows[0]).toHaveFocus();
  1101. });
  1102. expect(
  1103. await within(virtualizedContainer).findByText(/transaction-op-0/i)
  1104. ).toBeInTheDocument();
  1105. });
  1106. it('tab scrolls to next node', async () => {
  1107. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  1108. let rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1109. await userEvent.click(rows[0]);
  1110. await waitFor(() => expect(rows[0]).toHaveFocus());
  1111. await userEvent.keyboard('{tab}');
  1112. await waitFor(() => {
  1113. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1114. expect(rows[1]).toHaveFocus();
  1115. });
  1116. });
  1117. it('shift+tab scrolls to previous node', async () => {
  1118. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  1119. let rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1120. await userEvent.click(rows[1]);
  1121. await waitFor(() => expect(rows[1]).toHaveFocus());
  1122. await userEvent.keyboard('{Shift>}{tab}{/Shift}');
  1123. await waitFor(() => {
  1124. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1125. expect(rows[0]).toHaveFocus();
  1126. });
  1127. });
  1128. it('arrowdown+shift scrolls to the end of the list', async () => {
  1129. const {container, virtualizedContainer} = await keyboardNavigationTestSetup();
  1130. let rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1131. await userEvent.click(rows[0]);
  1132. await waitFor(() => expect(rows[0]).toHaveFocus());
  1133. await userEvent.keyboard('{Shift>}{arrowdown}{/Shift}');
  1134. expect(
  1135. await findByText(virtualizedContainer, /transaction-op-9999/i)
  1136. ).toBeInTheDocument();
  1137. await waitFor(() => {
  1138. rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1139. expect(rows[rows.length - 1]).toHaveFocus();
  1140. });
  1141. });
  1142. it('arrowup+shift scrolls to the start of the list', async () => {
  1143. const {container, virtualizedContainer} = await keyboardNavigationTestSetup();
  1144. let rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1145. await userEvent.click(rows[0]);
  1146. await waitFor(() => expect(rows[0]).toHaveFocus());
  1147. await userEvent.keyboard('{Shift>}{arrowdown}{/Shift}');
  1148. expect(
  1149. await findByText(virtualizedContainer, /transaction-op-9999/i)
  1150. ).toBeInTheDocument();
  1151. await waitFor(() => {
  1152. rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1153. expect(rows[rows.length - 1]).toHaveFocus();
  1154. });
  1155. await userEvent.keyboard('{Shift>}{arrowup}{/Shift}');
  1156. expect(
  1157. await findByText(virtualizedContainer, /transaction-op-0/i)
  1158. ).toBeInTheDocument();
  1159. await scrollToEnd();
  1160. await waitFor(() => {
  1161. rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1162. expect(rows[0]).toHaveFocus();
  1163. });
  1164. });
  1165. });
  1166. describe('search', () => {
  1167. it('searches in transaction', async () => {
  1168. const {container} = await searchTestSetup();
  1169. let rows = Array.from(container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR));
  1170. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1171. await userEvent.click(searchInput);
  1172. fireEvent.change(searchInput, {target: {value: 'transaction-op'}});
  1173. await waitFor(() => {
  1174. const highlighted_row = container.querySelector(
  1175. '.TraceRow:not(.Hidden).SearchResult.Highlight'
  1176. );
  1177. rows = Array.from(container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR));
  1178. expect(rows.indexOf(highlighted_row!)).toBe(1);
  1179. });
  1180. });
  1181. it('supports roving with arrowup and arrowdown', async () => {
  1182. const {container} = await searchTestSetup();
  1183. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1184. await userEvent.click(searchInput);
  1185. // Fire change because userEvent triggers this letter by letter
  1186. fireEvent.change(searchInput, {target: {value: 'transaction-op'}});
  1187. // Wait for the search results to resolve
  1188. await searchToUpdate();
  1189. for (const action of [
  1190. // starting at the top, jumpt bottom with shift+arrowdown
  1191. ['{Shift>}{arrowdown}{/Shift}', 11],
  1192. // // move to row above with arrowup
  1193. ['{arrowup}', 10],
  1194. // // and jump back to top with shift+arrowup
  1195. ['{Shift>}{arrowup}{/Shift}', 1],
  1196. // // // and jump to next row with arrowdown
  1197. ['{arrowdown}', 2],
  1198. ] as const) {
  1199. await userEvent.keyboard(action[0] as string);
  1200. // assert that focus on search input is never lost
  1201. expect(searchInput).toHaveFocus();
  1202. await waitFor(() => {
  1203. // Only a single row is highlighted, the rest are search results
  1204. assertHighlightedRowAtIndex(container, action[1]);
  1205. });
  1206. }
  1207. });
  1208. it('search roving updates the element in the drawer', async () => {
  1209. await searchTestSetup();
  1210. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1211. await userEvent.click(searchInput);
  1212. // Fire change because userEvent triggers this letter by letter
  1213. fireEvent.change(searchInput, {target: {value: 'transaction-op'}});
  1214. // Wait for the search results to resolve
  1215. await searchToUpdate();
  1216. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  1217. 'transaction-op-0'
  1218. );
  1219. // assert that focus on search input is never lost
  1220. expect(searchInput).toHaveFocus();
  1221. await userEvent.keyboard('{arrowdown}');
  1222. await waitFor(() => {
  1223. expect(screen.getByTestId('trace-drawer-title')).toHaveTextContent(
  1224. 'transaction-op-1'
  1225. );
  1226. });
  1227. });
  1228. it('highlighted node narrows down on the first result', async () => {
  1229. const {container} = await searchTestSetup();
  1230. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1231. await userEvent.click(searchInput);
  1232. // Fire change because userEvent triggers this letter by letter
  1233. fireEvent.change(searchInput, {target: {value: 'transaction-op-1'}});
  1234. // Wait for the search results to resolve
  1235. await searchToUpdate();
  1236. assertHighlightedRowAtIndex(container, 2);
  1237. fireEvent.change(searchInput, {target: {value: 'transaction-op-10'}});
  1238. await searchToUpdate();
  1239. await waitFor(() => {
  1240. assertHighlightedRowAtIndex(container, 11);
  1241. });
  1242. });
  1243. it('highlighted is persisted on node while it is part of the search results', async () => {
  1244. const {container} = await searchTestSetup();
  1245. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1246. await userEvent.click(searchInput);
  1247. // Fire change because userEvent triggers this letter by letter
  1248. fireEvent.change(searchInput, {target: {value: 'trans'}});
  1249. // Wait for the search results to resolve
  1250. await searchToUpdate();
  1251. await userEvent.keyboard('{arrowdown}');
  1252. await searchToUpdate();
  1253. assertHighlightedRowAtIndex(container, 2);
  1254. fireEvent.change(searchInput, {target: {value: 'transa'}});
  1255. await searchToUpdate();
  1256. // Highlighting is persisted on the row
  1257. assertHighlightedRowAtIndex(container, 2);
  1258. fireEvent.change(searchInput, {target: {value: 'this wont match anything'}});
  1259. await searchToUpdate();
  1260. // When there is no match, the highlighting is removed
  1261. expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0);
  1262. });
  1263. it('auto highlights the first result when search begins', async () => {
  1264. const {container} = await searchTestSetup();
  1265. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1266. await userEvent.click(searchInput);
  1267. // Nothing is highlighted
  1268. expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0);
  1269. // Fire change because userEvent triggers this letter by letter
  1270. fireEvent.change(searchInput, {target: {value: 't'}});
  1271. // Wait for the search results to resolve
  1272. await searchToUpdate();
  1273. assertHighlightedRowAtIndex(container, 1);
  1274. });
  1275. it('clicking a row that is also a search result updates the result index', async () => {
  1276. const {container} = await searchTestSetup();
  1277. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1278. await userEvent.click(searchInput);
  1279. // Fire change because userEvent triggers this letter by letter
  1280. fireEvent.change(searchInput, {target: {value: 'transaction-op-1'}});
  1281. await searchToUpdate();
  1282. assertHighlightedRowAtIndex(container, 2);
  1283. const rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1284. // By default, we highlight the first result
  1285. expect(await screen.findByTestId('trace-search-result-iterator')).toHaveTextContent(
  1286. '1/2'
  1287. );
  1288. await scrollToEnd();
  1289. // Click on a random row in the list that is not a search result
  1290. await userEvent.click(rows[5]);
  1291. await waitFor(() => {
  1292. expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent(
  1293. '-/2'
  1294. );
  1295. });
  1296. await scrollToEnd();
  1297. // Click on a the row in the list that is a search result
  1298. await userEvent.click(rows[2]);
  1299. await waitFor(() => {
  1300. expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent(
  1301. '1/2'
  1302. );
  1303. });
  1304. });
  1305. it('during search, expanding a row retriggers search', async () => {
  1306. mockTraceRootFacets();
  1307. mockTraceRootEvent('0');
  1308. mockTraceEventDetails();
  1309. mockMetricsResponse();
  1310. mockTraceResponse({
  1311. body: {
  1312. transactions: [
  1313. makeTransaction({
  1314. span_id: '0',
  1315. event_id: '0',
  1316. transaction: 'transaction-name-0',
  1317. 'transaction.op': 'transaction-op-0',
  1318. project_slug: 'project_slug',
  1319. }),
  1320. makeTransaction({
  1321. span_id: '1',
  1322. event_id: '1',
  1323. transaction: 'transaction-name-1',
  1324. 'transaction.op': 'transaction-op-1',
  1325. project_slug: 'project_slug',
  1326. }),
  1327. makeTransaction({
  1328. span_id: '2',
  1329. event_id: '2',
  1330. transaction: 'transaction-name-2',
  1331. 'transaction.op': 'transaction-op-2',
  1332. project_slug: 'project_slug',
  1333. }),
  1334. makeTransaction({
  1335. span_id: '3',
  1336. event_id: '3',
  1337. transaction: 'transaction-name-3',
  1338. 'transaction.op': 'transaction-op-3',
  1339. project_slug: 'project_slug',
  1340. }),
  1341. ],
  1342. orphan_errors: [],
  1343. },
  1344. });
  1345. mockTraceMetaResponse({
  1346. body: {
  1347. errors: 0,
  1348. performance_issues: 0,
  1349. projects: 0,
  1350. transactions: 0,
  1351. transaction_child_count_map: [
  1352. {
  1353. 'transaction.id': '0',
  1354. count: 5,
  1355. },
  1356. {
  1357. 'transaction.id': '1',
  1358. count: 5,
  1359. },
  1360. {
  1361. 'transaction.id': '2',
  1362. count: 5,
  1363. },
  1364. {
  1365. 'transaction.id': '3',
  1366. count: 5,
  1367. },
  1368. ],
  1369. },
  1370. });
  1371. const spansRequest = mockSpansResponse(
  1372. '0',
  1373. {},
  1374. {
  1375. entries: [
  1376. {
  1377. type: EntryType.SPANS,
  1378. data: [
  1379. makeSpan({span_id: '0', description: 'span-description', op: 'op-0'}),
  1380. ],
  1381. },
  1382. ],
  1383. }
  1384. );
  1385. const {container} = render(<TraceView />, {router});
  1386. // Awaits for the placeholder rendering rows to be removed
  1387. expect(await findByText(container, /transaction-op-0/i)).toBeInTheDocument();
  1388. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1389. await userEvent.click(searchInput);
  1390. // Fire change because userEvent triggers this letter by letter
  1391. fireEvent.change(searchInput, {target: {value: 'op-0'}});
  1392. await searchToUpdate();
  1393. await waitFor(() => {
  1394. expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent(
  1395. '1/1'
  1396. );
  1397. });
  1398. const open = await screen.findAllByRole('button', {name: '+'});
  1399. await userEvent.click(open[0]);
  1400. await waitFor(() => {
  1401. expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent(
  1402. '1/1'
  1403. );
  1404. });
  1405. expect(await screen.findByText('span-description')).toBeInTheDocument();
  1406. expect(spansRequest).toHaveBeenCalled();
  1407. await waitFor(() => {
  1408. expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent(
  1409. '1/2'
  1410. );
  1411. });
  1412. });
  1413. it('during search, highlighting is persisted on the row', async () => {
  1414. const {container} = await searchTestSetup();
  1415. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1416. await userEvent.click(searchInput);
  1417. // Fire change because userEvent triggers this letter by letter
  1418. fireEvent.change(searchInput, {target: {value: 'transaction-op'}});
  1419. await searchToUpdate();
  1420. assertHighlightedRowAtIndex(container, 1);
  1421. await searchToUpdate();
  1422. // User moves down the list using keyboard navigation
  1423. for (const _ of [1, 2, 3, 4, 5]) {
  1424. const initial = screen.getByTestId('trace-search-result-iterator').textContent;
  1425. await userEvent.keyboard('{arrowDown}');
  1426. await waitFor(() => {
  1427. expect(screen.getByTestId('trace-search-result-iterator')).not.toBe(initial);
  1428. });
  1429. }
  1430. // User clicks on an entry in the list, then proceeds to search
  1431. await waitFor(() => {
  1432. expect(screen.getByTestId('trace-search-result-iterator')).toHaveTextContent(
  1433. '6/11'
  1434. );
  1435. });
  1436. // And then continues the query - the highlighting is preserved as long as the
  1437. // rwo is part of the search results
  1438. assertHighlightedRowAtIndex(container, 6);
  1439. fireEvent.change(searchInput, {target: {value: 'transaction-op-'}});
  1440. await searchToUpdate();
  1441. assertHighlightedRowAtIndex(container, 6);
  1442. fireEvent.change(searchInput, {target: {value: 'transaction-op-5'}});
  1443. await searchToUpdate();
  1444. assertHighlightedRowAtIndex(container, 6);
  1445. fireEvent.change(searchInput, {target: {value: 'transaction-op-none'}});
  1446. await searchToUpdate();
  1447. expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0);
  1448. });
  1449. });
  1450. describe('tabbing', () => {
  1451. beforeEach(() => {});
  1452. afterEach(() => {
  1453. jest.restoreAllMocks();
  1454. });
  1455. it('clicking on a node spawns a new tab when none is selected', async () => {
  1456. const {virtualizedContainer} = await simpleTestSetup();
  1457. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1458. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1459. await userEvent.click(rows[5]);
  1460. await waitFor(() => {
  1461. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1462. });
  1463. });
  1464. it('clicking on a node replaces the previously selected tab', async () => {
  1465. const {virtualizedContainer} = await simpleTestSetup();
  1466. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1467. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1468. await userEvent.click(rows[5]);
  1469. await waitFor(() => {
  1470. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1471. expect(
  1472. screen
  1473. .queryAllByTestId(DRAWER_TABS_TEST_ID)[1]
  1474. .textContent?.includes('transaction-op-4')
  1475. ).toBeTruthy();
  1476. });
  1477. await userEvent.click(rows[7]);
  1478. await waitFor(() => {
  1479. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1480. expect(
  1481. screen
  1482. .queryAllByTestId(DRAWER_TABS_TEST_ID)[1]
  1483. .textContent?.includes('transaction-op-6')
  1484. ).toBeTruthy();
  1485. });
  1486. });
  1487. it('pinning a tab and clicking on a new node spawns a new tab', async () => {
  1488. const {virtualizedContainer} = await simpleTestSetup();
  1489. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1490. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1491. await userEvent.click(rows[5]);
  1492. await waitFor(() => {
  1493. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1494. });
  1495. await userEvent.click(await screen.findByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID));
  1496. await userEvent.click(rows[7]);
  1497. await waitFor(() => {
  1498. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(3);
  1499. expect(
  1500. screen
  1501. .queryAllByTestId(DRAWER_TABS_TEST_ID)[1]
  1502. .textContent?.includes('transaction-op-4')
  1503. ).toBeTruthy();
  1504. expect(
  1505. screen
  1506. .queryAllByTestId(DRAWER_TABS_TEST_ID)[2]
  1507. .textContent?.includes('transaction-op-6')
  1508. ).toBeTruthy();
  1509. });
  1510. });
  1511. it('unpinning a tab removes it', async () => {
  1512. const {virtualizedContainer} = await simpleTestSetup();
  1513. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1514. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1515. await userEvent.click(rows[5]);
  1516. await waitFor(() => {
  1517. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1518. });
  1519. await userEvent.click(await screen.findByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID));
  1520. await userEvent.click(rows[7]);
  1521. await waitFor(() => {
  1522. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(3);
  1523. });
  1524. const tabButtons = screen.queryAllByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID);
  1525. expect(tabButtons).toHaveLength(2);
  1526. await userEvent.click(tabButtons[0]);
  1527. await waitFor(() => {
  1528. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1529. });
  1530. });
  1531. it('clicking a node that is already open in a tab switches to that tab and persists the previous node', async () => {
  1532. const {virtualizedContainer} = await simpleTestSetup();
  1533. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1534. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1535. await userEvent.click(rows[5]);
  1536. await waitFor(() => {
  1537. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1538. });
  1539. await userEvent.click(await screen.findByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID));
  1540. await userEvent.click(rows[7]);
  1541. await waitFor(() => {
  1542. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(3);
  1543. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)[2]).toHaveAttribute(
  1544. 'aria-selected',
  1545. 'true'
  1546. );
  1547. });
  1548. await userEvent.click(rows[5]);
  1549. await waitFor(() => {
  1550. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)[1]).toHaveAttribute(
  1551. 'aria-selected',
  1552. 'true'
  1553. );
  1554. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(3);
  1555. });
  1556. });
  1557. });
  1558. });