trace.spec.tsx 58 KB

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