trace.spec.tsx 58 KB

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