virtualizedViewManager.spec.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  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. null
  91. );
  92. }
  93. function makeList(): VirtualizedList {
  94. return {
  95. scrollToRow: jest.fn(),
  96. } as unknown as VirtualizedList;
  97. }
  98. const EVENT_REQUEST_URL =
  99. '/organizations/org-slug/events/project:event_id/?averageColumn=span.self_time&averageColumn=span.duration';
  100. describe('VirtualizedViewManger', () => {
  101. it('initializes space', () => {
  102. const manager = new VirtualizedViewManager(
  103. {
  104. list: {width: 0.5},
  105. span_list: {width: 0.5},
  106. },
  107. new TraceScheduler(),
  108. new TraceView()
  109. );
  110. manager.view.setTraceSpace([10_000, 0, 1000, 1]);
  111. expect(manager.view.trace_space.serialize()).toEqual([0, 0, 1000, 1]);
  112. expect(manager.view.trace_view.serialize()).toEqual([0, 0, 1000, 1]);
  113. });
  114. it('initializes physical space', () => {
  115. const manager = new VirtualizedViewManager(
  116. {
  117. list: {width: 0.5},
  118. span_list: {width: 0.5},
  119. },
  120. new TraceScheduler(),
  121. new TraceView()
  122. );
  123. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 500, 1]);
  124. expect(manager.view.trace_container_physical_space.serialize()).toEqual([
  125. 0, 0, 1000, 1,
  126. ]);
  127. expect(manager.view.trace_physical_space.serialize()).toEqual([0, 0, 500, 1]);
  128. });
  129. describe('computeSpanCSSMatrixTransform', () => {
  130. it('enforces min scaling', () => {
  131. const manager = new VirtualizedViewManager(
  132. {
  133. list: {width: 0},
  134. span_list: {width: 1},
  135. },
  136. new TraceScheduler(),
  137. new TraceView()
  138. );
  139. manager.view.setTraceSpace([0, 0, 1000, 1]);
  140. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  141. expect(manager.computeSpanCSSMatrixTransform([0, 0.1])).toEqual([
  142. 0.001, 0, 0, 1, 0, 0,
  143. ]);
  144. });
  145. it('computes width scaling correctly', () => {
  146. const manager = new VirtualizedViewManager(
  147. {
  148. list: {width: 0},
  149. span_list: {width: 1},
  150. },
  151. new TraceScheduler(),
  152. new TraceView()
  153. );
  154. manager.view.setTraceSpace([0, 0, 100, 1]);
  155. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  156. expect(manager.computeSpanCSSMatrixTransform([0, 100])).toEqual([1, 0, 0, 1, 0, 0]);
  157. });
  158. it('computes x position correctly', () => {
  159. const manager = new VirtualizedViewManager(
  160. {
  161. list: {width: 0},
  162. span_list: {width: 1},
  163. },
  164. new TraceScheduler(),
  165. new TraceView()
  166. );
  167. manager.view.setTraceSpace([0, 0, 1000, 1]);
  168. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  169. expect(manager.computeSpanCSSMatrixTransform([50, 1000])).toEqual([
  170. 1, 0, 0, 1, 50, 0,
  171. ]);
  172. });
  173. it('computes span x position correctly', () => {
  174. const manager = new VirtualizedViewManager(
  175. {
  176. list: {width: 0},
  177. span_list: {width: 1},
  178. },
  179. new TraceScheduler(),
  180. new TraceView()
  181. );
  182. manager.view.setTraceSpace([0, 0, 1000, 1]);
  183. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  184. expect(manager.computeSpanCSSMatrixTransform([50, 1000])).toEqual([
  185. 1, 0, 0, 1, 50, 0,
  186. ]);
  187. });
  188. describe('when start is not 0', () => {
  189. it('computes width scaling correctly', () => {
  190. const manager = new VirtualizedViewManager(
  191. {
  192. list: {width: 0},
  193. span_list: {width: 1},
  194. },
  195. new TraceScheduler(),
  196. new TraceView()
  197. );
  198. manager.view.setTraceSpace([100, 0, 100, 1]);
  199. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  200. expect(manager.computeSpanCSSMatrixTransform([100, 100])).toEqual([
  201. 1, 0, 0, 1, 0, 0,
  202. ]);
  203. });
  204. it('computes x position correctly when view is offset', () => {
  205. const manager = new VirtualizedViewManager(
  206. {
  207. list: {width: 0},
  208. span_list: {width: 1},
  209. },
  210. new TraceScheduler(),
  211. new TraceView()
  212. );
  213. manager.view.setTraceSpace([100, 0, 100, 1]);
  214. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  215. expect(manager.computeSpanCSSMatrixTransform([100, 100])).toEqual([
  216. 1, 0, 0, 1, 0, 0,
  217. ]);
  218. });
  219. });
  220. });
  221. describe('transformXFromTimestamp', () => {
  222. it('computes x position correctly', () => {
  223. const manager = new VirtualizedViewManager(
  224. {
  225. list: {width: 0},
  226. span_list: {width: 1},
  227. },
  228. new TraceScheduler(),
  229. new TraceView()
  230. );
  231. manager.view.setTraceSpace([0, 0, 1000, 1]);
  232. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  233. expect(manager.transformXFromTimestamp(50)).toEqual(50);
  234. });
  235. it('computes x position correctly when view is offset', () => {
  236. const manager = new VirtualizedViewManager(
  237. {
  238. list: {width: 0},
  239. span_list: {width: 1},
  240. },
  241. new TraceScheduler(),
  242. new TraceView()
  243. );
  244. manager.view.setTraceSpace([50, 0, 1000, 1]);
  245. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  246. manager.view.trace_view.x = 50;
  247. expect(manager.transformXFromTimestamp(-50)).toEqual(-150);
  248. });
  249. it('when view is offset and scaled', () => {
  250. const manager = new VirtualizedViewManager(
  251. {
  252. list: {width: 0},
  253. span_list: {width: 1},
  254. },
  255. new TraceScheduler(),
  256. new TraceView()
  257. );
  258. manager.view.setTraceSpace([100, 0, 1000, 1]);
  259. manager.view.setTracePhysicalSpace([0, 0, 1000, 1], [0, 0, 1000, 1]);
  260. manager.view.setTraceView({width: 500, x: 500});
  261. expect(Math.round(manager.transformXFromTimestamp(100))).toEqual(-500);
  262. });
  263. });
  264. describe('expandToPath', () => {
  265. const organization = OrganizationFixture();
  266. const api = new MockApiClient();
  267. const manager = new VirtualizedViewManager(
  268. {
  269. list: {width: 0.5},
  270. span_list: {width: 0.5},
  271. },
  272. new TraceScheduler(),
  273. new TraceView()
  274. );
  275. it('scrolls to root node', async () => {
  276. const tree = TraceTree.FromTrace(
  277. makeTrace({
  278. transactions: [makeTransaction()],
  279. orphan_errors: [],
  280. }),
  281. null,
  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. null
  304. );
  305. manager.list = makeList();
  306. const result = await TraceTree.ExpandToPath(tree, ['txn-event_id'], () => void 0, {
  307. api: api,
  308. organization,
  309. });
  310. expect(result?.node).toBe(tree.list[2]);
  311. });
  312. it('scrolls to nested transaction', async () => {
  313. const tree = TraceTree.FromTrace(
  314. makeTrace({
  315. transactions: [
  316. makeTransaction({
  317. event_id: 'root',
  318. children: [
  319. makeTransaction({
  320. event_id: 'child',
  321. children: [
  322. makeTransaction({
  323. event_id: 'event_id',
  324. children: [],
  325. }),
  326. ],
  327. }),
  328. ],
  329. }),
  330. ],
  331. }),
  332. null,
  333. null
  334. );
  335. manager.list = makeList();
  336. expect(tree.list[tree.list.length - 1].path).toEqual([
  337. 'txn-event_id',
  338. 'txn-child',
  339. 'txn-root',
  340. ]);
  341. const result = await TraceTree.ExpandToPath(
  342. tree,
  343. ['txn-event_id', 'txn-child', 'txn-root'],
  344. () => void 0,
  345. {
  346. api: api,
  347. organization,
  348. }
  349. );
  350. expect(result?.node).toBe(tree.list[tree.list.length - 1]);
  351. });
  352. it('scrolls to spans of expanded transaction', async () => {
  353. manager.list = makeList();
  354. const tree = TraceTree.FromTrace(
  355. makeTrace({
  356. transactions: [
  357. makeTransaction({
  358. event_id: 'event_id',
  359. project_slug: 'project',
  360. children: [],
  361. }),
  362. ],
  363. }),
  364. null,
  365. null
  366. );
  367. MockApiClient.addMockResponse({
  368. url: EVENT_REQUEST_URL,
  369. method: 'GET',
  370. body: makeEvent(undefined, [makeSpan({span_id: 'span_id'})]),
  371. });
  372. const result = await TraceTree.ExpandToPath(
  373. tree,
  374. ['span-span_id', 'txn-event_id'],
  375. () => void 0,
  376. {
  377. api: api,
  378. organization,
  379. }
  380. );
  381. expect(tree.list[1].zoomedIn).toBe(true);
  382. expect(result?.node).toBe(tree.list[2]);
  383. });
  384. it('scrolls to span -> transaction -> span -> transaction', async () => {
  385. manager.list = makeList();
  386. const tree = TraceTree.FromTrace(
  387. makeTrace({
  388. transactions: [
  389. makeTransaction({
  390. event_id: 'event_id',
  391. project_slug: 'project_slug',
  392. children: [
  393. makeTransaction({
  394. parent_span_id: 'child_span',
  395. event_id: 'child_event_id',
  396. project_slug: 'project_slug',
  397. }),
  398. ],
  399. }),
  400. ],
  401. }),
  402. null,
  403. null
  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. null,
  585. null
  586. );
  587. const result = await TraceTree.ExpandToPath(tree, ['error-ded'], () => void 0, {
  588. api: api,
  589. organization,
  590. });
  591. expect(result?.node).toBe(tree.list[2]);
  592. });
  593. describe('error handling', () => {
  594. it('scrolls to child span of sibling autogrouped node when path is missing autogrouped node', async () => {
  595. manager.list = makeList();
  596. const tree = makeSingleTransactionTree();
  597. MockApiClient.addMockResponse({
  598. url: EVENT_REQUEST_URL,
  599. method: 'GET',
  600. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  601. });
  602. const result = await TraceTree.ExpandToPath(
  603. tree,
  604. ['span-middle_span', 'txn-event_id'],
  605. () => void 0,
  606. {
  607. api: api,
  608. organization,
  609. }
  610. );
  611. expect(result).toBeTruthy();
  612. });
  613. it('scrolls to child span of parent autogrouped node when path is missing autogrouped node', async () => {
  614. manager.list = makeList();
  615. const tree = makeSingleTransactionTree();
  616. MockApiClient.addMockResponse({
  617. url: EVENT_REQUEST_URL,
  618. method: 'GET',
  619. body: makeEvent({}, makeParentAutogroupSpans()),
  620. });
  621. const result = await TraceTree.ExpandToPath(
  622. tree,
  623. ['span-middle_span', 'txn-event_id'],
  624. () => void 0,
  625. {
  626. api: api,
  627. organization,
  628. }
  629. );
  630. expect(result).toBeTruthy();
  631. });
  632. });
  633. });
  634. });