virtualizedViewManager.spec.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761
  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([1, 0, 0, 1, 0, 0]);
  156. });
  157. it('computes x position correctly', () => {
  158. const manager = new VirtualizedViewManager(
  159. {
  160. list: {width: 0},
  161. span_list: {width: 1},
  162. },
  163. new TraceScheduler(),
  164. new TraceView()
  165. );
  166. manager.view.setTraceSpace([0, 0, 1000, 1]);
  167. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  168. expect(manager.computeSpanCSSMatrixTransform([50, 1000])).toEqual([
  169. 1, 0, 0, 1, 50, 0,
  170. ]);
  171. });
  172. it('computes span x position correctly', () => {
  173. const manager = new VirtualizedViewManager(
  174. {
  175. list: {width: 0},
  176. span_list: {width: 1},
  177. },
  178. new TraceScheduler(),
  179. new TraceView()
  180. );
  181. manager.view.setTraceSpace([0, 0, 1000, 1]);
  182. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  183. expect(manager.computeSpanCSSMatrixTransform([50, 1000])).toEqual([
  184. 1, 0, 0, 1, 50, 0,
  185. ]);
  186. });
  187. describe('when start is not 0', () => {
  188. it('computes width scaling correctly', () => {
  189. const manager = new VirtualizedViewManager(
  190. {
  191. list: {width: 0},
  192. span_list: {width: 1},
  193. },
  194. new TraceScheduler(),
  195. new TraceView()
  196. );
  197. manager.view.setTraceSpace([100, 0, 100, 1]);
  198. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  199. expect(manager.computeSpanCSSMatrixTransform([100, 100])).toEqual([
  200. 1, 0, 0, 1, 0, 0,
  201. ]);
  202. });
  203. it('computes x position correctly when view is offset', () => {
  204. const manager = new VirtualizedViewManager(
  205. {
  206. list: {width: 0},
  207. span_list: {width: 1},
  208. },
  209. new TraceScheduler(),
  210. new TraceView()
  211. );
  212. manager.view.setTraceSpace([100, 0, 100, 1]);
  213. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  214. expect(manager.computeSpanCSSMatrixTransform([100, 100])).toEqual([
  215. 1, 0, 0, 1, 0, 0,
  216. ]);
  217. });
  218. });
  219. });
  220. describe('transformXFromTimestamp', () => {
  221. it('computes x position correctly', () => {
  222. const manager = new VirtualizedViewManager(
  223. {
  224. list: {width: 0},
  225. span_list: {width: 1},
  226. },
  227. new TraceScheduler(),
  228. new TraceView()
  229. );
  230. manager.view.setTraceSpace([0, 0, 1000, 1]);
  231. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  232. expect(manager.transformXFromTimestamp(50)).toEqual(50);
  233. });
  234. it('computes x position correctly when view is offset', () => {
  235. const manager = new VirtualizedViewManager(
  236. {
  237. list: {width: 0},
  238. span_list: {width: 1},
  239. },
  240. new TraceScheduler(),
  241. new TraceView()
  242. );
  243. manager.view.setTraceSpace([50, 0, 1000, 1]);
  244. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  245. manager.view.trace_view.x = 50;
  246. expect(manager.transformXFromTimestamp(-50)).toEqual(-150);
  247. });
  248. it('when view is offset and scaled', () => {
  249. const manager = new VirtualizedViewManager(
  250. {
  251. list: {width: 0},
  252. span_list: {width: 1},
  253. },
  254. new TraceScheduler(),
  255. new TraceView()
  256. );
  257. manager.view.setTraceSpace([100, 0, 1000, 1]);
  258. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  259. manager.view.setTraceView({width: 500, x: 500});
  260. expect(Math.round(manager.transformXFromTimestamp(100))).toEqual(-500);
  261. });
  262. });
  263. describe('expandToPath', () => {
  264. const organization = OrganizationFixture();
  265. const api = new MockApiClient();
  266. const manager = new VirtualizedViewManager(
  267. {
  268. list: {width: 0.5},
  269. span_list: {width: 0.5},
  270. },
  271. new TraceScheduler(),
  272. new TraceView()
  273. );
  274. it('scrolls to root node', async () => {
  275. const tree = TraceTree.FromTrace(
  276. makeTrace({
  277. transactions: [makeTransaction()],
  278. orphan_errors: [],
  279. }),
  280. null
  281. );
  282. manager.list = makeList();
  283. const result = await TraceTree.ExpandToPath(tree, tree.list[0].path, () => void 0, {
  284. api: api,
  285. organization,
  286. });
  287. expect(result?.node).toBe(tree.list[0]);
  288. });
  289. it('scrolls to transaction', async () => {
  290. const tree = TraceTree.FromTrace(
  291. makeTrace({
  292. transactions: [
  293. makeTransaction(),
  294. makeTransaction({
  295. event_id: 'event_id',
  296. children: [],
  297. }),
  298. ],
  299. }),
  300. null
  301. );
  302. manager.list = makeList();
  303. const result = await TraceTree.ExpandToPath(tree, ['txn-event_id'], () => void 0, {
  304. api: api,
  305. organization,
  306. });
  307. expect(result?.node).toBe(tree.list[2]);
  308. });
  309. it('scrolls to nested transaction', async () => {
  310. const tree = TraceTree.FromTrace(
  311. makeTrace({
  312. transactions: [
  313. makeTransaction({
  314. event_id: 'root',
  315. children: [
  316. makeTransaction({
  317. event_id: 'child',
  318. children: [
  319. makeTransaction({
  320. event_id: 'event_id',
  321. children: [],
  322. }),
  323. ],
  324. }),
  325. ],
  326. }),
  327. ],
  328. }),
  329. null
  330. );
  331. manager.list = makeList();
  332. expect(tree.list[tree.list.length - 1].path).toEqual([
  333. 'txn-event_id',
  334. 'txn-child',
  335. 'txn-root',
  336. ]);
  337. const result = await TraceTree.ExpandToPath(
  338. tree,
  339. ['txn-event_id', 'txn-child', 'txn-root'],
  340. () => void 0,
  341. {
  342. api: api,
  343. organization,
  344. }
  345. );
  346. expect(result?.node).toBe(tree.list[tree.list.length - 1]);
  347. });
  348. it('scrolls to spans of expanded transaction', async () => {
  349. manager.list = makeList();
  350. const tree = TraceTree.FromTrace(
  351. makeTrace({
  352. transactions: [
  353. makeTransaction({
  354. event_id: 'event_id',
  355. project_slug: 'project',
  356. children: [],
  357. }),
  358. ],
  359. }),
  360. null
  361. );
  362. MockApiClient.addMockResponse({
  363. url: EVENT_REQUEST_URL,
  364. method: 'GET',
  365. body: makeEvent(undefined, [makeSpan({span_id: 'span_id'})]),
  366. });
  367. const result = await TraceTree.ExpandToPath(
  368. tree,
  369. ['span-span_id', 'txn-event_id'],
  370. () => void 0,
  371. {
  372. api: api,
  373. organization,
  374. }
  375. );
  376. expect(tree.list[1].zoomedIn).toBe(true);
  377. expect(result?.node).toBe(tree.list[2]);
  378. });
  379. it('scrolls to empty data node of expanded transaction', async () => {
  380. manager.list = makeList();
  381. const tree = TraceTree.FromTrace(
  382. makeTrace({
  383. transactions: [
  384. makeTransaction({
  385. event_id: 'event_id',
  386. project_slug: 'project',
  387. children: [],
  388. }),
  389. ],
  390. }),
  391. null
  392. );
  393. MockApiClient.addMockResponse({
  394. url: EVENT_REQUEST_URL,
  395. method: 'GET',
  396. body: makeEvent(undefined, []),
  397. });
  398. const result = await TraceTree.ExpandToPath(
  399. tree,
  400. ['empty-node', 'txn-event_id'],
  401. () => void 0,
  402. {
  403. api: api,
  404. organization,
  405. }
  406. );
  407. expect(tree.list[1].zoomedIn).toBe(true);
  408. expect(result?.node).toBe(tree.list[2]);
  409. });
  410. it('scrolls to span -> transaction -> span -> transaction', async () => {
  411. manager.list = makeList();
  412. const tree = TraceTree.FromTrace(
  413. makeTrace({
  414. transactions: [
  415. makeTransaction({
  416. event_id: 'event_id',
  417. project_slug: 'project_slug',
  418. children: [
  419. makeTransaction({
  420. parent_span_id: 'child_span',
  421. event_id: 'child_event_id',
  422. project_slug: 'project_slug',
  423. }),
  424. ],
  425. }),
  426. ],
  427. }),
  428. null
  429. );
  430. MockApiClient.addMockResponse({
  431. url: EVENT_REQUEST_URL,
  432. method: 'GET',
  433. body: makeEvent(undefined, [
  434. makeSpan({span_id: 'other_child_span'}),
  435. makeSpan({span_id: 'child_span'}),
  436. ]),
  437. });
  438. MockApiClient.addMockResponse({
  439. url: '/organizations/org-slug/events/project_slug:child_event_id/?averageColumn=span.self_time&averageColumn=span.duration',
  440. method: 'GET',
  441. body: makeEvent(undefined, [makeSpan({span_id: 'other_child_span'})]),
  442. });
  443. const result = await TraceTree.ExpandToPath(
  444. tree,
  445. ['span-other_child_span', 'txn-child_event_id', 'txn-event_id'],
  446. () => void 0,
  447. {
  448. api: api,
  449. organization,
  450. }
  451. );
  452. expect(result).toBeTruthy();
  453. });
  454. describe('scrolls to directly autogrouped node', () => {
  455. for (const headOrTailId of ['head_span', 'tail_span']) {
  456. it('scrolls to directly autogrouped node head', async () => {
  457. manager.list = makeList();
  458. const tree = makeSingleTransactionTree();
  459. MockApiClient.addMockResponse({
  460. url: EVENT_REQUEST_URL,
  461. method: 'GET',
  462. body: makeEvent({}, makeParentAutogroupSpans()),
  463. });
  464. const result = await TraceTree.ExpandToPath(
  465. tree,
  466. [`ag-${headOrTailId}`, 'txn-event_id'],
  467. () => void 0,
  468. {
  469. api: api,
  470. organization,
  471. }
  472. );
  473. expect(result).toBeTruthy();
  474. });
  475. }
  476. for (const headOrTailId of ['head_span', 'tail_span']) {
  477. it('scrolls to child of autogrouped node head or tail', async () => {
  478. manager.list = makeList();
  479. const tree = makeSingleTransactionTree();
  480. MockApiClient.addMockResponse({
  481. url: EVENT_REQUEST_URL,
  482. method: 'GET',
  483. body: makeEvent({}, makeParentAutogroupSpans()),
  484. });
  485. const result = await TraceTree.ExpandToPath(
  486. tree,
  487. ['span-middle_span', `ag-${headOrTailId}`, 'txn-event_id'],
  488. () => void 0,
  489. {
  490. api: api,
  491. organization,
  492. }
  493. );
  494. expect(result).toBeTruthy();
  495. });
  496. }
  497. });
  498. describe('sibling autogrouping', () => {
  499. it('scrolls to child span of sibling autogrouped node', async () => {
  500. manager.list = makeList();
  501. const tree = makeSingleTransactionTree();
  502. MockApiClient.addMockResponse({
  503. url: EVENT_REQUEST_URL,
  504. method: 'GET',
  505. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  506. });
  507. const result = await TraceTree.ExpandToPath(
  508. tree,
  509. ['span-middle_span', `ag-first_span`, 'txn-event_id'],
  510. () => void 0,
  511. {
  512. api: api,
  513. organization,
  514. }
  515. );
  516. expect(result).toBeTruthy();
  517. });
  518. });
  519. describe('missing instrumentation', () => {
  520. it('scrolls to missing instrumentation via previous span_id', async () => {
  521. manager.list = makeList();
  522. const tree = makeSingleTransactionTree();
  523. MockApiClient.addMockResponse({
  524. url: EVENT_REQUEST_URL,
  525. method: 'GET',
  526. body: makeEvent({}, [
  527. makeSpan({
  528. description: 'span',
  529. op: 'db',
  530. start_timestamp: 0,
  531. timestamp: 0.5,
  532. span_id: 'first_span',
  533. }),
  534. makeSpan({
  535. description: 'span',
  536. op: 'db',
  537. start_timestamp: 0.7,
  538. timestamp: 1,
  539. span_id: 'middle_span',
  540. }),
  541. ]),
  542. });
  543. const result = await TraceTree.ExpandToPath(
  544. tree,
  545. ['ms-first_span', 'txn-event_id'],
  546. () => void 0,
  547. {
  548. api: api,
  549. organization,
  550. }
  551. );
  552. expect(result).toBeTruthy();
  553. });
  554. it('scrolls to missing instrumentation via next span_id', async () => {
  555. manager.list = makeList();
  556. const tree = makeSingleTransactionTree();
  557. MockApiClient.addMockResponse({
  558. url: EVENT_REQUEST_URL,
  559. method: 'GET',
  560. body: makeEvent({}, [
  561. makeSpan({
  562. description: 'span',
  563. op: 'db',
  564. start_timestamp: 0,
  565. timestamp: 0.5,
  566. span_id: 'first_span',
  567. }),
  568. makeSpan({
  569. description: 'span',
  570. op: 'db',
  571. start_timestamp: 0.7,
  572. timestamp: 1,
  573. span_id: 'second_span',
  574. }),
  575. ]),
  576. });
  577. const result = await TraceTree.ExpandToPath(
  578. tree,
  579. ['ms-second_span', 'txn-event_id'],
  580. () => void 0,
  581. {
  582. api: api,
  583. organization,
  584. }
  585. );
  586. expect(result).toBeTruthy();
  587. });
  588. });
  589. it('scrolls to orphan error', async () => {
  590. manager.list = makeList();
  591. const tree = TraceTree.FromTrace(
  592. makeTrace({
  593. transactions: [makeTransaction()],
  594. orphan_errors: [
  595. {
  596. event_id: 'ded',
  597. project_slug: 'project_slug',
  598. project_id: 1,
  599. issue: 'whoa rusty',
  600. issue_id: 0,
  601. span: '',
  602. level: 'error',
  603. title: 'ded fo good',
  604. message: 'ded fo good',
  605. timestamp: 1,
  606. },
  607. ],
  608. }),
  609. null
  610. );
  611. const result = await TraceTree.ExpandToPath(tree, ['error-ded'], () => void 0, {
  612. api: api,
  613. organization,
  614. });
  615. expect(result?.node).toBe(tree.list[2]);
  616. });
  617. describe('error handling', () => {
  618. it('scrolls to child span of sibling autogrouped node when path is missing autogrouped node', async () => {
  619. manager.list = makeList();
  620. const tree = makeSingleTransactionTree();
  621. MockApiClient.addMockResponse({
  622. url: EVENT_REQUEST_URL,
  623. method: 'GET',
  624. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  625. });
  626. const result = await TraceTree.ExpandToPath(
  627. tree,
  628. ['span-middle_span', 'txn-event_id'],
  629. () => void 0,
  630. {
  631. api: api,
  632. organization,
  633. }
  634. );
  635. expect(result).toBeTruthy();
  636. });
  637. it('scrolls to child span of parent autogrouped node when path is missing autogrouped node', async () => {
  638. manager.list = makeList();
  639. const tree = makeSingleTransactionTree();
  640. MockApiClient.addMockResponse({
  641. url: EVENT_REQUEST_URL,
  642. method: 'GET',
  643. body: makeEvent({}, makeParentAutogroupSpans()),
  644. });
  645. const result = await TraceTree.ExpandToPath(
  646. tree,
  647. ['span-middle_span', 'txn-event_id'],
  648. () => void 0,
  649. {
  650. api: api,
  651. organization,
  652. }
  653. );
  654. expect(result).toBeTruthy();
  655. });
  656. });
  657. });
  658. });