virtualizedViewManager.spec.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  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 {
  6. type VirtualizedList,
  7. VirtualizedViewManager,
  8. } from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager';
  9. import {TraceTree} from '../traceModels/traceTree';
  10. function makeEvent(overrides: Partial<Event> = {}, spans: RawSpanType[] = []): Event {
  11. return {
  12. entries: [{type: EntryType.SPANS, data: spans}],
  13. ...overrides,
  14. } as Event;
  15. }
  16. function makeTrace(
  17. overrides: Partial<TraceSplitResults<TraceTree.Transaction>>
  18. ): TraceSplitResults<TraceTree.Transaction> {
  19. return {
  20. transactions: [],
  21. orphan_errors: [],
  22. ...overrides,
  23. } as TraceSplitResults<TraceTree.Transaction>;
  24. }
  25. function makeTransaction(
  26. overrides: Partial<TraceTree.Transaction> = {}
  27. ): TraceTree.Transaction {
  28. return {
  29. children: [],
  30. start_timestamp: 0,
  31. timestamp: 1,
  32. transaction: 'transaction',
  33. 'transaction.op': '',
  34. 'transaction.status': '',
  35. errors: [],
  36. performance_issues: [],
  37. ...overrides,
  38. } as TraceTree.Transaction;
  39. }
  40. function makeSpan(overrides: Partial<RawSpanType> = {}): RawSpanType {
  41. return {
  42. op: '',
  43. description: '',
  44. span_id: '',
  45. start_timestamp: 0,
  46. timestamp: 10,
  47. ...overrides,
  48. } as RawSpanType;
  49. }
  50. function makeParentAutogroupSpans(): RawSpanType[] {
  51. return [
  52. makeSpan({description: 'span', op: 'db', span_id: 'head_span'}),
  53. makeSpan({
  54. description: 'span',
  55. op: 'db',
  56. span_id: 'middle_span',
  57. parent_span_id: 'head_span',
  58. }),
  59. makeSpan({
  60. description: 'span',
  61. op: 'db',
  62. span_id: 'tail_span',
  63. parent_span_id: 'middle_span',
  64. }),
  65. ];
  66. }
  67. function makeSiblingAutogroupedSpans(): RawSpanType[] {
  68. return [
  69. makeSpan({description: 'span', op: 'db', span_id: 'first_span'}),
  70. makeSpan({description: 'span', op: 'db', span_id: 'middle_span'}),
  71. makeSpan({description: 'span', op: 'db', span_id: 'other_middle_span'}),
  72. makeSpan({description: 'span', op: 'db', span_id: 'another_middle_span'}),
  73. makeSpan({description: 'span', op: 'db', span_id: 'last_span'}),
  74. ];
  75. }
  76. function makeSingleTransactionTree(): TraceTree {
  77. return TraceTree.FromTrace(
  78. makeTrace({
  79. transactions: [
  80. makeTransaction({
  81. transaction: 'transaction',
  82. project_slug: 'project',
  83. event_id: 'event_id',
  84. }),
  85. ],
  86. }),
  87. null
  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. null
  262. );
  263. manager.list = makeList();
  264. const result = await TraceTree.ExpandToPath(tree, tree.list[0].path, () => void 0, {
  265. api: api,
  266. organization,
  267. });
  268. expect(result?.node).toBe(tree.list[0]);
  269. });
  270. it('scrolls to transaction', async () => {
  271. const tree = TraceTree.FromTrace(
  272. makeTrace({
  273. transactions: [
  274. makeTransaction(),
  275. makeTransaction({
  276. event_id: 'event_id',
  277. children: [],
  278. }),
  279. ],
  280. }),
  281. null
  282. );
  283. manager.list = makeList();
  284. const result = await TraceTree.ExpandToPath(tree, ['txn-event_id'], () => void 0, {
  285. api: api,
  286. organization,
  287. });
  288. expect(result?.node).toBe(tree.list[2]);
  289. });
  290. it('scrolls to nested transaction', async () => {
  291. const tree = TraceTree.FromTrace(
  292. makeTrace({
  293. transactions: [
  294. makeTransaction({
  295. event_id: 'root',
  296. children: [
  297. makeTransaction({
  298. event_id: 'child',
  299. children: [
  300. makeTransaction({
  301. event_id: 'event_id',
  302. children: [],
  303. }),
  304. ],
  305. }),
  306. ],
  307. }),
  308. ],
  309. }),
  310. null
  311. );
  312. manager.list = makeList();
  313. expect(tree.list[tree.list.length - 1].path).toEqual([
  314. 'txn-event_id',
  315. 'txn-child',
  316. 'txn-root',
  317. ]);
  318. const result = await TraceTree.ExpandToPath(
  319. tree,
  320. ['txn-event_id', 'txn-child', 'txn-root'],
  321. () => void 0,
  322. {
  323. api: api,
  324. organization,
  325. }
  326. );
  327. expect(result?.node).toBe(tree.list[tree.list.length - 1]);
  328. });
  329. it('scrolls to spans of expanded transaction', async () => {
  330. manager.list = makeList();
  331. const tree = TraceTree.FromTrace(
  332. makeTrace({
  333. transactions: [
  334. makeTransaction({
  335. event_id: 'event_id',
  336. project_slug: 'project',
  337. children: [],
  338. }),
  339. ],
  340. }),
  341. null
  342. );
  343. MockApiClient.addMockResponse({
  344. url: EVENT_REQUEST_URL,
  345. method: 'GET',
  346. body: makeEvent(undefined, [makeSpan({span_id: 'span_id'})]),
  347. });
  348. const result = await TraceTree.ExpandToPath(
  349. tree,
  350. ['span-span_id', 'txn-event_id'],
  351. () => void 0,
  352. {
  353. api: api,
  354. organization,
  355. }
  356. );
  357. expect(tree.list[1].zoomedIn).toBe(true);
  358. expect(result?.node).toBe(tree.list[2]);
  359. });
  360. it('scrolls to empty data node of expanded transaction', async () => {
  361. manager.list = makeList();
  362. const tree = TraceTree.FromTrace(
  363. makeTrace({
  364. transactions: [
  365. makeTransaction({
  366. event_id: 'event_id',
  367. project_slug: 'project',
  368. children: [],
  369. }),
  370. ],
  371. }),
  372. null
  373. );
  374. MockApiClient.addMockResponse({
  375. url: EVENT_REQUEST_URL,
  376. method: 'GET',
  377. body: makeEvent(undefined, []),
  378. });
  379. const result = await TraceTree.ExpandToPath(
  380. tree,
  381. ['empty-node', 'txn-event_id'],
  382. () => void 0,
  383. {
  384. api: api,
  385. organization,
  386. }
  387. );
  388. expect(tree.list[1].zoomedIn).toBe(true);
  389. expect(result?.node).toBe(tree.list[2]);
  390. });
  391. it('scrolls to span -> transaction -> span -> transaction', async () => {
  392. manager.list = makeList();
  393. const tree = TraceTree.FromTrace(
  394. makeTrace({
  395. transactions: [
  396. makeTransaction({
  397. event_id: 'event_id',
  398. project_slug: 'project_slug',
  399. children: [
  400. makeTransaction({
  401. parent_span_id: 'child_span',
  402. event_id: 'child_event_id',
  403. project_slug: 'project_slug',
  404. }),
  405. ],
  406. }),
  407. ],
  408. }),
  409. null
  410. );
  411. MockApiClient.addMockResponse({
  412. url: EVENT_REQUEST_URL,
  413. method: 'GET',
  414. body: makeEvent(undefined, [
  415. makeSpan({span_id: 'other_child_span'}),
  416. makeSpan({span_id: 'child_span'}),
  417. ]),
  418. });
  419. MockApiClient.addMockResponse({
  420. url: '/organizations/org-slug/events/project_slug:child_event_id/?averageColumn=span.self_time&averageColumn=span.duration',
  421. method: 'GET',
  422. body: makeEvent(undefined, [makeSpan({span_id: 'other_child_span'})]),
  423. });
  424. const result = await TraceTree.ExpandToPath(
  425. tree,
  426. ['span-other_child_span', 'txn-child_event_id', 'txn-event_id'],
  427. () => void 0,
  428. {
  429. api: api,
  430. organization,
  431. }
  432. );
  433. expect(result).toBeTruthy();
  434. });
  435. describe('scrolls to directly autogrouped node', () => {
  436. for (const headOrTailId of ['head_span', 'tail_span']) {
  437. it('scrolls to directly autogrouped node head', async () => {
  438. manager.list = makeList();
  439. const tree = makeSingleTransactionTree();
  440. MockApiClient.addMockResponse({
  441. url: EVENT_REQUEST_URL,
  442. method: 'GET',
  443. body: makeEvent({}, makeParentAutogroupSpans()),
  444. });
  445. const result = await TraceTree.ExpandToPath(
  446. tree,
  447. [`ag-${headOrTailId}`, 'txn-event_id'],
  448. () => void 0,
  449. {
  450. api: api,
  451. organization,
  452. }
  453. );
  454. expect(result).toBeTruthy();
  455. });
  456. }
  457. for (const headOrTailId of ['head_span', 'tail_span']) {
  458. it('scrolls to child of autogrouped node head or tail', 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. ['span-middle_span', `ag-${headOrTailId}`, 'txn-event_id'],
  469. () => void 0,
  470. {
  471. api: api,
  472. organization,
  473. }
  474. );
  475. expect(result).toBeTruthy();
  476. });
  477. }
  478. });
  479. describe('sibling autogrouping', () => {
  480. it('scrolls to child span of sibling autogrouped node', async () => {
  481. manager.list = makeList();
  482. const tree = makeSingleTransactionTree();
  483. MockApiClient.addMockResponse({
  484. url: EVENT_REQUEST_URL,
  485. method: 'GET',
  486. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  487. });
  488. const result = await TraceTree.ExpandToPath(
  489. tree,
  490. ['span-middle_span', `ag-first_span`, 'txn-event_id'],
  491. () => void 0,
  492. {
  493. api: api,
  494. organization,
  495. }
  496. );
  497. expect(result).toBeTruthy();
  498. });
  499. });
  500. describe('missing instrumentation', () => {
  501. it('scrolls to missing instrumentation via previous span_id', async () => {
  502. manager.list = makeList();
  503. const tree = makeSingleTransactionTree();
  504. MockApiClient.addMockResponse({
  505. url: EVENT_REQUEST_URL,
  506. method: 'GET',
  507. body: makeEvent({}, [
  508. makeSpan({
  509. description: 'span',
  510. op: 'db',
  511. start_timestamp: 0,
  512. timestamp: 0.5,
  513. span_id: 'first_span',
  514. }),
  515. makeSpan({
  516. description: 'span',
  517. op: 'db',
  518. start_timestamp: 0.7,
  519. timestamp: 1,
  520. span_id: 'middle_span',
  521. }),
  522. ]),
  523. });
  524. const result = await TraceTree.ExpandToPath(
  525. tree,
  526. ['ms-first_span', 'txn-event_id'],
  527. () => void 0,
  528. {
  529. api: api,
  530. organization,
  531. }
  532. );
  533. expect(result).toBeTruthy();
  534. });
  535. it('scrolls to missing instrumentation via next span_id', async () => {
  536. manager.list = makeList();
  537. const tree = makeSingleTransactionTree();
  538. MockApiClient.addMockResponse({
  539. url: EVENT_REQUEST_URL,
  540. method: 'GET',
  541. body: makeEvent({}, [
  542. makeSpan({
  543. description: 'span',
  544. op: 'db',
  545. start_timestamp: 0,
  546. timestamp: 0.5,
  547. span_id: 'first_span',
  548. }),
  549. makeSpan({
  550. description: 'span',
  551. op: 'db',
  552. start_timestamp: 0.7,
  553. timestamp: 1,
  554. span_id: 'second_span',
  555. }),
  556. ]),
  557. });
  558. const result = await TraceTree.ExpandToPath(
  559. tree,
  560. ['ms-second_span', 'txn-event_id'],
  561. () => void 0,
  562. {
  563. api: api,
  564. organization,
  565. }
  566. );
  567. expect(result).toBeTruthy();
  568. });
  569. });
  570. it('scrolls to orphan error', async () => {
  571. manager.list = makeList();
  572. const tree = TraceTree.FromTrace(
  573. makeTrace({
  574. transactions: [makeTransaction()],
  575. orphan_errors: [
  576. {
  577. event_id: 'ded',
  578. project_slug: 'project_slug',
  579. project_id: 1,
  580. issue: 'whoa rusty',
  581. issue_id: 0,
  582. span: '',
  583. level: 'error',
  584. title: 'ded fo good',
  585. message: 'ded fo good',
  586. timestamp: 1,
  587. },
  588. ],
  589. }),
  590. null
  591. );
  592. const result = await TraceTree.ExpandToPath(tree, ['error-ded'], () => void 0, {
  593. api: api,
  594. organization,
  595. });
  596. expect(result?.node).toBe(tree.list[2]);
  597. });
  598. describe('error handling', () => {
  599. it('scrolls to child span of sibling autogrouped node when path is missing autogrouped node', async () => {
  600. manager.list = makeList();
  601. const tree = makeSingleTransactionTree();
  602. MockApiClient.addMockResponse({
  603. url: EVENT_REQUEST_URL,
  604. method: 'GET',
  605. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  606. });
  607. const result = await TraceTree.ExpandToPath(
  608. tree,
  609. ['span-middle_span', 'txn-event_id'],
  610. () => void 0,
  611. {
  612. api: api,
  613. organization,
  614. }
  615. );
  616. expect(result).toBeTruthy();
  617. });
  618. it('scrolls to child span of parent 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({}, makeParentAutogroupSpans()),
  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. });
  638. });
  639. });