virtualizedViewManager.spec.tsx 20 KB

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