virtualizedViewManager.spec.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  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/virtualizedViewManager';
  12. import {TraceTree} from './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';
  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('scrollToPath', () => {
  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 manager.scrollToPath(tree, tree.list[0].path, () => void 0, {
  264. api: api,
  265. organization,
  266. });
  267. expect(result?.node).toBe(tree.list[0]);
  268. expect(manager.list.scrollToRow).toHaveBeenCalledWith(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. );
  282. manager.list = makeList();
  283. const result = await manager.scrollToPath(tree, ['txn:event_id'], () => void 0, {
  284. api: api,
  285. organization,
  286. });
  287. expect(result?.node).toBe(tree.list[2]);
  288. expect(manager.list.scrollToRow).toHaveBeenCalledWith(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. );
  311. manager.list = makeList();
  312. expect(tree.list[tree.list.length - 1].path).toEqual([
  313. 'txn:event_id',
  314. 'txn:child',
  315. 'txn:root',
  316. ]);
  317. const result = await manager.scrollToPath(
  318. tree,
  319. ['txn:event_id', 'txn:child', 'txn:root'],
  320. () => void 0,
  321. {
  322. api: api,
  323. organization,
  324. }
  325. );
  326. expect(result?.node).toBe(tree.list[tree.list.length - 1]);
  327. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  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. );
  342. MockApiClient.addMockResponse({
  343. url: EVENT_REQUEST_URL,
  344. method: 'GET',
  345. body: makeEvent(undefined, [makeSpan({span_id: 'span_id'})]),
  346. });
  347. const result = await manager.scrollToPath(
  348. tree,
  349. ['span:span_id', 'txn:event_id'],
  350. () => void 0,
  351. {
  352. api: api,
  353. organization,
  354. }
  355. );
  356. expect(tree.list[1].zoomedIn).toBe(true);
  357. expect(result?.node).toBe(tree.list[2]);
  358. expect(manager.list.scrollToRow).toHaveBeenCalledWith(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. );
  373. MockApiClient.addMockResponse({
  374. url: EVENT_REQUEST_URL,
  375. method: 'GET',
  376. body: makeEvent(undefined, []),
  377. });
  378. const result = await manager.scrollToPath(
  379. tree,
  380. ['empty:node', 'txn:event_id'],
  381. () => void 0,
  382. {
  383. api: api,
  384. organization,
  385. }
  386. );
  387. expect(tree.list[1].zoomedIn).toBe(true);
  388. expect(result?.node).toBe(tree.list[2]);
  389. expect(manager.list.scrollToRow).toHaveBeenCalledWith(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. );
  410. MockApiClient.addMockResponse({
  411. url: EVENT_REQUEST_URL,
  412. method: 'GET',
  413. body: makeEvent(undefined, [
  414. makeSpan({span_id: 'other_child_span'}),
  415. makeSpan({span_id: 'child_span'}),
  416. ]),
  417. });
  418. MockApiClient.addMockResponse({
  419. url: '/organizations/org-slug/events/project_slug:child_event_id/?averageColumn=span.self_time',
  420. method: 'GET',
  421. body: makeEvent(undefined, [makeSpan({span_id: 'other_child_span'})]),
  422. });
  423. const result = await manager.scrollToPath(
  424. tree,
  425. ['span:other_child_span', 'txn:child_event_id', 'txn:event_id'],
  426. () => void 0,
  427. {
  428. api: api,
  429. organization,
  430. }
  431. );
  432. expect(result).toBeTruthy();
  433. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  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 manager.scrollToPath(
  446. tree,
  447. [`ag:${headOrTailId}`, 'txn:event_id'],
  448. () => void 0,
  449. {
  450. api: api,
  451. organization,
  452. }
  453. );
  454. expect(result).toBeTruthy();
  455. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  456. });
  457. }
  458. for (const headOrTailId of ['head_span', 'tail_span']) {
  459. it('scrolls to child of autogrouped node head or tail', async () => {
  460. manager.list = makeList();
  461. const tree = makeSingleTransactionTree();
  462. MockApiClient.addMockResponse({
  463. url: EVENT_REQUEST_URL,
  464. method: 'GET',
  465. body: makeEvent({}, makeParentAutogroupSpans()),
  466. });
  467. const result = await manager.scrollToPath(
  468. tree,
  469. ['span:middle_span', `ag:${headOrTailId}`, 'txn:event_id'],
  470. () => void 0,
  471. {
  472. api: api,
  473. organization,
  474. }
  475. );
  476. expect(result).toBeTruthy();
  477. expect(manager.list.scrollToRow).toHaveBeenCalledWith(4);
  478. });
  479. }
  480. });
  481. describe('sibling autogrouping', () => {
  482. it('scrolls to child span of sibling autogrouped node', async () => {
  483. manager.list = makeList();
  484. const tree = makeSingleTransactionTree();
  485. MockApiClient.addMockResponse({
  486. url: EVENT_REQUEST_URL,
  487. method: 'GET',
  488. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  489. });
  490. const result = await manager.scrollToPath(
  491. tree,
  492. ['span:middle_span', `ag:first_span`, 'txn:event_id'],
  493. () => void 0,
  494. {
  495. api: api,
  496. organization,
  497. }
  498. );
  499. expect(result).toBeTruthy();
  500. expect(manager.list.scrollToRow).toHaveBeenCalledWith(4);
  501. });
  502. });
  503. describe('missing instrumentation', () => {
  504. it('scrolls to missing instrumentation via previous span_id', async () => {
  505. manager.list = makeList();
  506. const tree = makeSingleTransactionTree();
  507. MockApiClient.addMockResponse({
  508. url: EVENT_REQUEST_URL,
  509. method: 'GET',
  510. body: makeEvent({}, [
  511. makeSpan({
  512. description: 'span',
  513. op: 'db',
  514. start_timestamp: 0,
  515. timestamp: 0.5,
  516. span_id: 'first_span',
  517. }),
  518. makeSpan({
  519. description: 'span',
  520. op: 'db',
  521. start_timestamp: 0.7,
  522. timestamp: 1,
  523. span_id: 'middle_span',
  524. }),
  525. ]),
  526. });
  527. const result = await manager.scrollToPath(
  528. tree,
  529. ['ms:first_span', 'txn:event_id'],
  530. () => void 0,
  531. {
  532. api: api,
  533. organization,
  534. }
  535. );
  536. expect(result).toBeTruthy();
  537. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  538. });
  539. it('scrolls to missing instrumentation via next span_id', async () => {
  540. manager.list = makeList();
  541. const tree = makeSingleTransactionTree();
  542. MockApiClient.addMockResponse({
  543. url: EVENT_REQUEST_URL,
  544. method: 'GET',
  545. body: makeEvent({}, [
  546. makeSpan({
  547. description: 'span',
  548. op: 'db',
  549. start_timestamp: 0,
  550. timestamp: 0.5,
  551. span_id: 'first_span',
  552. }),
  553. makeSpan({
  554. description: 'span',
  555. op: 'db',
  556. start_timestamp: 0.7,
  557. timestamp: 1,
  558. span_id: 'second_span',
  559. }),
  560. ]),
  561. });
  562. const result = await manager.scrollToPath(
  563. tree,
  564. ['ms:second_span', 'txn:event_id'],
  565. () => void 0,
  566. {
  567. api: api,
  568. organization,
  569. }
  570. );
  571. expect(result).toBeTruthy();
  572. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  573. });
  574. });
  575. it('scrolls to orphan error', async () => {
  576. manager.list = makeList();
  577. const tree = TraceTree.FromTrace(
  578. makeTrace({
  579. transactions: [makeTransaction()],
  580. orphan_errors: [
  581. {
  582. event_id: 'ded',
  583. project_slug: 'project_slug',
  584. project_id: 1,
  585. issue: 'whoa rusty',
  586. issue_id: 0,
  587. span: '',
  588. level: 'error',
  589. title: 'ded fo good',
  590. timestamp: 1,
  591. },
  592. ],
  593. })
  594. );
  595. const result = await manager.scrollToPath(tree, ['error:ded'], () => void 0, {
  596. api: api,
  597. organization,
  598. });
  599. expect(result?.node).toBe(tree.list[2]);
  600. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  601. });
  602. });
  603. });