virtualizedViewManager.spec.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
  3. import {EntryType, type Event} from 'sentry/types';
  4. import type {
  5. TraceFullDetailed,
  6. TraceSplitResults,
  7. } from 'sentry/utils/performance/quickTrace/types';
  8. import {
  9. type VirtualizedList,
  10. VirtualizedViewManager,
  11. } from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager';
  12. import {TraceTree} from '../traceModels/traceTree';
  13. function makeEvent(overrides: Partial<Event> = {}, spans: RawSpanType[] = []): Event {
  14. return {
  15. entries: [{type: EntryType.SPANS, data: spans}],
  16. ...overrides,
  17. } as Event;
  18. }
  19. function makeTrace(
  20. overrides: Partial<TraceSplitResults<TraceFullDetailed>>
  21. ): TraceSplitResults<TraceFullDetailed> {
  22. return {
  23. transactions: [],
  24. orphan_errors: [],
  25. ...overrides,
  26. } as TraceSplitResults<TraceFullDetailed>;
  27. }
  28. function makeTransaction(overrides: Partial<TraceFullDetailed> = {}): TraceFullDetailed {
  29. return {
  30. children: [],
  31. start_timestamp: 0,
  32. timestamp: 1,
  33. transaction: 'transaction',
  34. 'transaction.op': '',
  35. 'transaction.status': '',
  36. errors: [],
  37. performance_issues: [],
  38. ...overrides,
  39. } as TraceFullDetailed;
  40. }
  41. function makeSpan(overrides: Partial<RawSpanType> = {}): RawSpanType {
  42. return {
  43. op: '',
  44. description: '',
  45. span_id: '',
  46. start_timestamp: 0,
  47. timestamp: 10,
  48. ...overrides,
  49. } as RawSpanType;
  50. }
  51. function makeParentAutogroupSpans(): RawSpanType[] {
  52. return [
  53. makeSpan({description: 'span', op: 'db', span_id: 'head_span'}),
  54. makeSpan({
  55. description: 'span',
  56. op: 'db',
  57. span_id: 'middle_span',
  58. parent_span_id: 'head_span',
  59. }),
  60. makeSpan({
  61. description: 'span',
  62. op: 'db',
  63. span_id: 'tail_span',
  64. parent_span_id: 'middle_span',
  65. }),
  66. ];
  67. }
  68. function makeSiblingAutogroupedSpans(): RawSpanType[] {
  69. return [
  70. makeSpan({description: 'span', op: 'db', span_id: 'first_span'}),
  71. makeSpan({description: 'span', op: 'db', span_id: 'middle_span'}),
  72. makeSpan({description: 'span', op: 'db', span_id: 'other_middle_span'}),
  73. makeSpan({description: 'span', op: 'db', span_id: 'another_middle_span'}),
  74. makeSpan({description: 'span', op: 'db', span_id: 'last_span'}),
  75. ];
  76. }
  77. function makeSingleTransactionTree(): TraceTree {
  78. return TraceTree.FromTrace(
  79. makeTrace({
  80. transactions: [
  81. makeTransaction({
  82. transaction: 'transaction',
  83. project_slug: 'project',
  84. event_id: 'event_id',
  85. }),
  86. ],
  87. })
  88. );
  89. }
  90. function makeList(): VirtualizedList {
  91. return {
  92. scrollToRow: jest.fn(),
  93. } as unknown as VirtualizedList;
  94. }
  95. const EVENT_REQUEST_URL =
  96. '/organizations/org-slug/events/project:event_id/?averageColumn=span.self_time&averageColumn=span.duration';
  97. describe('VirtualizedViewManger', () => {
  98. it('initializes space', () => {
  99. const manager = new VirtualizedViewManager({
  100. list: {width: 0.5},
  101. span_list: {width: 0.5},
  102. });
  103. manager.initializeTraceSpace([10_000, 0, 1000, 1]);
  104. expect(manager.trace_space.serialize()).toEqual([0, 0, 1000, 1]);
  105. expect(manager.trace_view.serialize()).toEqual([0, 0, 1000, 1]);
  106. });
  107. it('initializes physical space', () => {
  108. const manager = new VirtualizedViewManager({
  109. list: {width: 0.5},
  110. span_list: {width: 0.5},
  111. });
  112. manager.initializePhysicalSpace(1000, 1);
  113. expect(manager.container_physical_space.serialize()).toEqual([0, 0, 1000, 1]);
  114. expect(manager.trace_physical_space.serialize()).toEqual([0, 0, 500, 1]);
  115. });
  116. describe('computeSpanCSSMatrixTransform', () => {
  117. it('enforces min scaling', () => {
  118. const manager = new VirtualizedViewManager({
  119. list: {width: 0},
  120. span_list: {width: 1},
  121. });
  122. manager.initializeTraceSpace([0, 0, 1000, 1]);
  123. manager.initializePhysicalSpace(1000, 1);
  124. expect(manager.computeSpanCSSMatrixTransform([0, 0.1])).toEqual([
  125. 0.001, 0, 0, 1, 0, 0,
  126. ]);
  127. });
  128. it('computes width scaling correctly', () => {
  129. const manager = new VirtualizedViewManager({
  130. list: {width: 0},
  131. span_list: {width: 1},
  132. });
  133. manager.initializeTraceSpace([0, 0, 100, 1]);
  134. manager.initializePhysicalSpace(1000, 1);
  135. expect(manager.computeSpanCSSMatrixTransform([0, 100])).toEqual([1, 0, 0, 1, 0, 0]);
  136. });
  137. it('computes x position correctly', () => {
  138. const manager = new VirtualizedViewManager({
  139. list: {width: 0},
  140. span_list: {width: 1},
  141. });
  142. manager.initializeTraceSpace([0, 0, 1000, 1]);
  143. manager.initializePhysicalSpace(1000, 1);
  144. expect(manager.computeSpanCSSMatrixTransform([50, 1000])).toEqual([
  145. 1, 0, 0, 1, 50, 0,
  146. ]);
  147. });
  148. it('computes span x position correctly', () => {
  149. const manager = new VirtualizedViewManager({
  150. list: {width: 0},
  151. span_list: {width: 1},
  152. });
  153. manager.initializeTraceSpace([0, 0, 1000, 1]);
  154. manager.initializePhysicalSpace(1000, 1);
  155. expect(manager.computeSpanCSSMatrixTransform([50, 1000])).toEqual([
  156. 1, 0, 0, 1, 50, 0,
  157. ]);
  158. });
  159. describe('when start is not 0', () => {
  160. it('computes width scaling correctly', () => {
  161. const manager = new VirtualizedViewManager({
  162. list: {width: 0},
  163. span_list: {width: 1},
  164. });
  165. manager.initializeTraceSpace([100, 0, 100, 1]);
  166. manager.initializePhysicalSpace(1000, 1);
  167. expect(manager.computeSpanCSSMatrixTransform([100, 100])).toEqual([
  168. 1, 0, 0, 1, 0, 0,
  169. ]);
  170. });
  171. it('computes x position correctly when view is offset', () => {
  172. const manager = new VirtualizedViewManager({
  173. list: {width: 0},
  174. span_list: {width: 1},
  175. });
  176. manager.initializeTraceSpace([100, 0, 100, 1]);
  177. manager.initializePhysicalSpace(1000, 1);
  178. expect(manager.computeSpanCSSMatrixTransform([100, 100])).toEqual([
  179. 1, 0, 0, 1, 0, 0,
  180. ]);
  181. });
  182. });
  183. });
  184. describe('computeTransformXFromTimestamp', () => {
  185. it('computes x position correctly', () => {
  186. const manager = new VirtualizedViewManager({
  187. list: {width: 0},
  188. span_list: {width: 1},
  189. });
  190. manager.initializeTraceSpace([0, 0, 1000, 1]);
  191. manager.initializePhysicalSpace(1000, 1);
  192. expect(manager.computeTransformXFromTimestamp(50)).toEqual(50);
  193. });
  194. it('computes x position correctly when view is offset', () => {
  195. const manager = new VirtualizedViewManager({
  196. list: {width: 0},
  197. span_list: {width: 1},
  198. });
  199. manager.initializeTraceSpace([50, 0, 1000, 1]);
  200. manager.initializePhysicalSpace(1000, 1);
  201. manager.trace_view.x = 50;
  202. expect(manager.computeTransformXFromTimestamp(-50)).toEqual(-150);
  203. });
  204. it('when view is offset and scaled', () => {
  205. const manager = new VirtualizedViewManager({
  206. list: {width: 0},
  207. span_list: {width: 1},
  208. });
  209. manager.initializeTraceSpace([50, 0, 100, 1]);
  210. manager.initializePhysicalSpace(1000, 1);
  211. manager.trace_view.width = 50;
  212. manager.trace_view.x = 50;
  213. expect(Math.round(manager.computeTransformXFromTimestamp(75))).toEqual(-250);
  214. });
  215. });
  216. describe('getConfigSpaceCursor', () => {
  217. it('returns the correct x position', () => {
  218. const manager = new VirtualizedViewManager({
  219. list: {width: 0},
  220. span_list: {width: 1},
  221. });
  222. manager.initializeTraceSpace([0, 0, 100, 1]);
  223. manager.initializePhysicalSpace(1000, 1);
  224. expect(manager.getConfigSpaceCursor({x: 500, y: 0})).toEqual([50, 0]);
  225. });
  226. it('returns the correct x position when view scaled', () => {
  227. const manager = new VirtualizedViewManager({
  228. list: {width: 0},
  229. span_list: {width: 1},
  230. });
  231. manager.initializeTraceSpace([0, 0, 100, 1]);
  232. manager.initializePhysicalSpace(1000, 1);
  233. manager.trace_view.x = 50;
  234. manager.trace_view.width = 50;
  235. expect(manager.getConfigSpaceCursor({x: 500, y: 0})).toEqual([75, 0]);
  236. });
  237. it('returns the correct x position when view is offset', () => {
  238. const manager = new VirtualizedViewManager({
  239. list: {width: 0},
  240. span_list: {width: 1},
  241. });
  242. manager.initializeTraceSpace([0, 0, 100, 1]);
  243. manager.initializePhysicalSpace(1000, 1);
  244. manager.trace_view.x = 50;
  245. expect(manager.getConfigSpaceCursor({x: 500, y: 0})).toEqual([100, 0]);
  246. });
  247. });
  248. describe('expandToPath', () => {
  249. const organization = OrganizationFixture();
  250. const api = new MockApiClient();
  251. const manager = new VirtualizedViewManager({
  252. list: {width: 0.5},
  253. span_list: {width: 0.5},
  254. });
  255. it('scrolls to root node', async () => {
  256. const tree = TraceTree.FromTrace(
  257. makeTrace({
  258. transactions: [makeTransaction()],
  259. orphan_errors: [],
  260. })
  261. );
  262. manager.list = makeList();
  263. const result = await TraceTree.ExpandToPath(tree, tree.list[0].path, () => void 0, {
  264. api: api,
  265. organization,
  266. });
  267. expect(result?.node).toBe(tree.list[0]);
  268. });
  269. it('scrolls to transaction', async () => {
  270. const tree = TraceTree.FromTrace(
  271. makeTrace({
  272. transactions: [
  273. makeTransaction(),
  274. makeTransaction({
  275. event_id: 'event_id',
  276. children: [],
  277. }),
  278. ],
  279. })
  280. );
  281. manager.list = makeList();
  282. const result = await TraceTree.ExpandToPath(tree, ['txn-event_id'], () => void 0, {
  283. api: api,
  284. organization,
  285. });
  286. expect(result?.node).toBe(tree.list[2]);
  287. });
  288. it('scrolls to nested transaction', async () => {
  289. const tree = TraceTree.FromTrace(
  290. makeTrace({
  291. transactions: [
  292. makeTransaction({
  293. event_id: 'root',
  294. children: [
  295. makeTransaction({
  296. event_id: 'child',
  297. children: [
  298. makeTransaction({
  299. event_id: 'event_id',
  300. children: [],
  301. }),
  302. ],
  303. }),
  304. ],
  305. }),
  306. ],
  307. })
  308. );
  309. manager.list = makeList();
  310. expect(tree.list[tree.list.length - 1].path).toEqual([
  311. 'txn-event_id',
  312. 'txn-child',
  313. 'txn-root',
  314. ]);
  315. const result = await TraceTree.ExpandToPath(
  316. tree,
  317. ['txn-event_id', 'txn-child', 'txn-root'],
  318. () => void 0,
  319. {
  320. api: api,
  321. organization,
  322. }
  323. );
  324. expect(result?.node).toBe(tree.list[tree.list.length - 1]);
  325. });
  326. it('scrolls to spans of expanded transaction', async () => {
  327. manager.list = makeList();
  328. const tree = TraceTree.FromTrace(
  329. makeTrace({
  330. transactions: [
  331. makeTransaction({
  332. event_id: 'event_id',
  333. project_slug: 'project',
  334. children: [],
  335. }),
  336. ],
  337. })
  338. );
  339. MockApiClient.addMockResponse({
  340. url: EVENT_REQUEST_URL,
  341. method: 'GET',
  342. body: makeEvent(undefined, [makeSpan({span_id: 'span_id'})]),
  343. });
  344. const result = await TraceTree.ExpandToPath(
  345. tree,
  346. ['span-span_id', 'txn-event_id'],
  347. () => void 0,
  348. {
  349. api: api,
  350. organization,
  351. }
  352. );
  353. expect(tree.list[1].zoomedIn).toBe(true);
  354. expect(result?.node).toBe(tree.list[2]);
  355. });
  356. it('scrolls to empty data node of expanded transaction', async () => {
  357. manager.list = makeList();
  358. const tree = TraceTree.FromTrace(
  359. makeTrace({
  360. transactions: [
  361. makeTransaction({
  362. event_id: 'event_id',
  363. project_slug: 'project',
  364. children: [],
  365. }),
  366. ],
  367. })
  368. );
  369. MockApiClient.addMockResponse({
  370. url: EVENT_REQUEST_URL,
  371. method: 'GET',
  372. body: makeEvent(undefined, []),
  373. });
  374. const result = await TraceTree.ExpandToPath(
  375. tree,
  376. ['empty-node', 'txn-event_id'],
  377. () => void 0,
  378. {
  379. api: api,
  380. organization,
  381. }
  382. );
  383. expect(tree.list[1].zoomedIn).toBe(true);
  384. expect(result?.node).toBe(tree.list[2]);
  385. });
  386. it('scrolls to span -> transaction -> span -> transaction', async () => {
  387. manager.list = makeList();
  388. const tree = TraceTree.FromTrace(
  389. makeTrace({
  390. transactions: [
  391. makeTransaction({
  392. event_id: 'event_id',
  393. project_slug: 'project_slug',
  394. children: [
  395. makeTransaction({
  396. parent_span_id: 'child_span',
  397. event_id: 'child_event_id',
  398. project_slug: 'project_slug',
  399. }),
  400. ],
  401. }),
  402. ],
  403. })
  404. );
  405. MockApiClient.addMockResponse({
  406. url: EVENT_REQUEST_URL,
  407. method: 'GET',
  408. body: makeEvent(undefined, [
  409. makeSpan({span_id: 'other_child_span'}),
  410. makeSpan({span_id: 'child_span'}),
  411. ]),
  412. });
  413. MockApiClient.addMockResponse({
  414. url: '/organizations/org-slug/events/project_slug:child_event_id/?averageColumn=span.self_time&averageColumn=span.duration',
  415. method: 'GET',
  416. body: makeEvent(undefined, [makeSpan({span_id: 'other_child_span'})]),
  417. });
  418. const result = await TraceTree.ExpandToPath(
  419. tree,
  420. ['span-other_child_span', 'txn-child_event_id', 'txn-event_id'],
  421. () => void 0,
  422. {
  423. api: api,
  424. organization,
  425. }
  426. );
  427. expect(result).toBeTruthy();
  428. });
  429. describe('scrolls to directly autogrouped node', () => {
  430. for (const headOrTailId of ['head_span', 'tail_span']) {
  431. it('scrolls to directly autogrouped node head', async () => {
  432. manager.list = makeList();
  433. const tree = makeSingleTransactionTree();
  434. MockApiClient.addMockResponse({
  435. url: EVENT_REQUEST_URL,
  436. method: 'GET',
  437. body: makeEvent({}, makeParentAutogroupSpans()),
  438. });
  439. const result = await TraceTree.ExpandToPath(
  440. tree,
  441. [`ag-${headOrTailId}`, 'txn-event_id'],
  442. () => void 0,
  443. {
  444. api: api,
  445. organization,
  446. }
  447. );
  448. expect(result).toBeTruthy();
  449. });
  450. }
  451. for (const headOrTailId of ['head_span', 'tail_span']) {
  452. it('scrolls to child of autogrouped node head or tail', async () => {
  453. manager.list = makeList();
  454. const tree = makeSingleTransactionTree();
  455. MockApiClient.addMockResponse({
  456. url: EVENT_REQUEST_URL,
  457. method: 'GET',
  458. body: makeEvent({}, makeParentAutogroupSpans()),
  459. });
  460. const result = await TraceTree.ExpandToPath(
  461. tree,
  462. ['span-middle_span', `ag-${headOrTailId}`, 'txn-event_id'],
  463. () => void 0,
  464. {
  465. api: api,
  466. organization,
  467. }
  468. );
  469. expect(result).toBeTruthy();
  470. });
  471. }
  472. });
  473. describe('sibling autogrouping', () => {
  474. it('scrolls to child span of sibling autogrouped node', async () => {
  475. manager.list = makeList();
  476. const tree = makeSingleTransactionTree();
  477. MockApiClient.addMockResponse({
  478. url: EVENT_REQUEST_URL,
  479. method: 'GET',
  480. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  481. });
  482. const result = await TraceTree.ExpandToPath(
  483. tree,
  484. ['span-middle_span', `ag-first_span`, 'txn-event_id'],
  485. () => void 0,
  486. {
  487. api: api,
  488. organization,
  489. }
  490. );
  491. expect(result).toBeTruthy();
  492. });
  493. });
  494. describe('missing instrumentation', () => {
  495. it('scrolls to missing instrumentation via previous span_id', async () => {
  496. manager.list = makeList();
  497. const tree = makeSingleTransactionTree();
  498. MockApiClient.addMockResponse({
  499. url: EVENT_REQUEST_URL,
  500. method: 'GET',
  501. body: makeEvent({}, [
  502. makeSpan({
  503. description: 'span',
  504. op: 'db',
  505. start_timestamp: 0,
  506. timestamp: 0.5,
  507. span_id: 'first_span',
  508. }),
  509. makeSpan({
  510. description: 'span',
  511. op: 'db',
  512. start_timestamp: 0.7,
  513. timestamp: 1,
  514. span_id: 'middle_span',
  515. }),
  516. ]),
  517. });
  518. const result = await TraceTree.ExpandToPath(
  519. tree,
  520. ['ms-first_span', 'txn-event_id'],
  521. () => void 0,
  522. {
  523. api: api,
  524. organization,
  525. }
  526. );
  527. expect(result).toBeTruthy();
  528. });
  529. it('scrolls to missing instrumentation via next span_id', async () => {
  530. manager.list = makeList();
  531. const tree = makeSingleTransactionTree();
  532. MockApiClient.addMockResponse({
  533. url: EVENT_REQUEST_URL,
  534. method: 'GET',
  535. body: makeEvent({}, [
  536. makeSpan({
  537. description: 'span',
  538. op: 'db',
  539. start_timestamp: 0,
  540. timestamp: 0.5,
  541. span_id: 'first_span',
  542. }),
  543. makeSpan({
  544. description: 'span',
  545. op: 'db',
  546. start_timestamp: 0.7,
  547. timestamp: 1,
  548. span_id: 'second_span',
  549. }),
  550. ]),
  551. });
  552. const result = await TraceTree.ExpandToPath(
  553. tree,
  554. ['ms-second_span', 'txn-event_id'],
  555. () => void 0,
  556. {
  557. api: api,
  558. organization,
  559. }
  560. );
  561. expect(result).toBeTruthy();
  562. });
  563. });
  564. it('scrolls to orphan error', async () => {
  565. manager.list = makeList();
  566. const tree = TraceTree.FromTrace(
  567. makeTrace({
  568. transactions: [makeTransaction()],
  569. orphan_errors: [
  570. {
  571. event_id: 'ded',
  572. project_slug: 'project_slug',
  573. project_id: 1,
  574. issue: 'whoa rusty',
  575. issue_id: 0,
  576. span: '',
  577. level: 'error',
  578. title: 'ded fo good',
  579. message: 'ded fo good',
  580. timestamp: 1,
  581. },
  582. ],
  583. })
  584. );
  585. const result = await TraceTree.ExpandToPath(tree, ['error-ded'], () => void 0, {
  586. api: api,
  587. organization,
  588. });
  589. expect(result?.node).toBe(tree.list[2]);
  590. });
  591. describe('error handling', () => {
  592. it('scrolls to child span of sibling autogrouped node when path is missing autogrouped node', async () => {
  593. manager.list = makeList();
  594. const tree = makeSingleTransactionTree();
  595. MockApiClient.addMockResponse({
  596. url: EVENT_REQUEST_URL,
  597. method: 'GET',
  598. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  599. });
  600. const result = await TraceTree.ExpandToPath(
  601. tree,
  602. ['span-middle_span', 'txn-event_id'],
  603. () => void 0,
  604. {
  605. api: api,
  606. organization,
  607. }
  608. );
  609. expect(result).toBeTruthy();
  610. });
  611. it('scrolls to child span of parent autogrouped node when path is missing autogrouped node', async () => {
  612. manager.list = makeList();
  613. const tree = makeSingleTransactionTree();
  614. MockApiClient.addMockResponse({
  615. url: EVENT_REQUEST_URL,
  616. method: 'GET',
  617. body: makeEvent({}, makeParentAutogroupSpans()),
  618. });
  619. const result = await TraceTree.ExpandToPath(
  620. tree,
  621. ['span-middle_span', 'txn-event_id'],
  622. () => void 0,
  623. {
  624. api: api,
  625. organization,
  626. }
  627. );
  628. expect(result).toBeTruthy();
  629. });
  630. });
  631. });
  632. });