virtualizedViewManager.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  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. ...overrides,
  37. } as TraceFullDetailed;
  38. }
  39. function makeSpan(overrides: Partial<RawSpanType> = {}): RawSpanType {
  40. return {
  41. op: '',
  42. description: '',
  43. span_id: '',
  44. start_timestamp: 0,
  45. timestamp: 10,
  46. ...overrides,
  47. } as RawSpanType;
  48. }
  49. function makeParentAutogroupSpans(): RawSpanType[] {
  50. return [
  51. makeSpan({description: 'span', op: 'db', span_id: 'head_span'}),
  52. makeSpan({
  53. description: 'span',
  54. op: 'db',
  55. span_id: 'middle_span',
  56. parent_span_id: 'head_span',
  57. }),
  58. makeSpan({
  59. description: 'span',
  60. op: 'db',
  61. span_id: 'tail_span',
  62. parent_span_id: 'middle_span',
  63. }),
  64. ];
  65. }
  66. function makeSiblingAutogroupedSpans(): RawSpanType[] {
  67. return [
  68. makeSpan({description: 'span', op: 'db', span_id: 'first_span'}),
  69. makeSpan({description: 'span', op: 'db', span_id: 'middle_span'}),
  70. makeSpan({description: 'span', op: 'db', span_id: 'other_middle_span'}),
  71. makeSpan({description: 'span', op: 'db', span_id: 'another_middle_span'}),
  72. makeSpan({description: 'span', op: 'db', span_id: 'last_span'}),
  73. ];
  74. }
  75. function makeSingleTransactionTree(): TraceTree {
  76. return TraceTree.FromTrace(
  77. makeTrace({
  78. transactions: [
  79. makeTransaction({
  80. transaction: 'transaction',
  81. project_slug: 'project',
  82. event_id: 'event_id',
  83. }),
  84. ],
  85. })
  86. );
  87. }
  88. function makeList(): VirtualizedList {
  89. return {
  90. scrollToRow: jest.fn(),
  91. } as unknown as VirtualizedList;
  92. }
  93. describe('VirtualizedViewManger', () => {
  94. it('initializes space', () => {
  95. const manager = new VirtualizedViewManager({
  96. list: {width: 0.5},
  97. span_list: {width: 0.5},
  98. });
  99. manager.initializeTraceSpace([10_000, 0, 1000, 1]);
  100. expect(manager.trace_space.serialize()).toEqual([0, 0, 1000, 1]);
  101. expect(manager.trace_view.serialize()).toEqual([0, 0, 1000, 1]);
  102. });
  103. it('initializes physical space', () => {
  104. const manager = new VirtualizedViewManager({
  105. list: {width: 0.5},
  106. span_list: {width: 0.5},
  107. });
  108. manager.initializePhysicalSpace(1000, 1);
  109. expect(manager.container_physical_space.serialize()).toEqual([0, 0, 1000, 1]);
  110. expect(manager.trace_physical_space.serialize()).toEqual([0, 0, 500, 1]);
  111. });
  112. describe('computeSpanCSSMatrixTransform', () => {
  113. it('enforces min scaling', () => {
  114. const manager = new VirtualizedViewManager({
  115. list: {width: 0},
  116. span_list: {width: 1},
  117. });
  118. manager.initializeTraceSpace([0, 0, 1000, 1]);
  119. manager.initializePhysicalSpace(1000, 1);
  120. expect(manager.computeSpanCSSMatrixTransform([0, 0.1])).toEqual([
  121. 0.001, 0, 0, 1, 0, 0,
  122. ]);
  123. });
  124. it('computes width scaling correctly', () => {
  125. const manager = new VirtualizedViewManager({
  126. list: {width: 0},
  127. span_list: {width: 1},
  128. });
  129. manager.initializeTraceSpace([0, 0, 100, 1]);
  130. manager.initializePhysicalSpace(1000, 1);
  131. expect(manager.computeSpanCSSMatrixTransform([0, 100])).toEqual([1, 0, 0, 1, 0, 0]);
  132. });
  133. it('computes x position correctly', () => {
  134. const manager = new VirtualizedViewManager({
  135. list: {width: 0},
  136. span_list: {width: 1},
  137. });
  138. manager.initializeTraceSpace([0, 0, 1000, 1]);
  139. manager.initializePhysicalSpace(1000, 1);
  140. expect(manager.computeSpanCSSMatrixTransform([50, 1000])).toEqual([
  141. 1, 0, 0, 1, 50, 0,
  142. ]);
  143. });
  144. it('computes span x position correctly', () => {
  145. const manager = new VirtualizedViewManager({
  146. list: {width: 0},
  147. span_list: {width: 1},
  148. });
  149. manager.initializeTraceSpace([0, 0, 1000, 1]);
  150. manager.initializePhysicalSpace(1000, 1);
  151. expect(manager.computeSpanCSSMatrixTransform([50, 1000])).toEqual([
  152. 1, 0, 0, 1, 50, 0,
  153. ]);
  154. });
  155. describe('when start is not 0', () => {
  156. it('computes width scaling correctly', () => {
  157. const manager = new VirtualizedViewManager({
  158. list: {width: 0},
  159. span_list: {width: 1},
  160. });
  161. manager.initializeTraceSpace([100, 0, 100, 1]);
  162. manager.initializePhysicalSpace(1000, 1);
  163. expect(manager.computeSpanCSSMatrixTransform([100, 100])).toEqual([
  164. 1, 0, 0, 1, 0, 0,
  165. ]);
  166. });
  167. it('computes x position correctly when view is offset', () => {
  168. const manager = new VirtualizedViewManager({
  169. list: {width: 0},
  170. span_list: {width: 1},
  171. });
  172. manager.initializeTraceSpace([100, 0, 100, 1]);
  173. manager.initializePhysicalSpace(1000, 1);
  174. expect(manager.computeSpanCSSMatrixTransform([100, 100])).toEqual([
  175. 1, 0, 0, 1, 0, 0,
  176. ]);
  177. });
  178. });
  179. });
  180. describe('computeTransformXFromTimestamp', () => {
  181. it('computes x position correctly', () => {
  182. const manager = new VirtualizedViewManager({
  183. list: {width: 0},
  184. span_list: {width: 1},
  185. });
  186. manager.initializeTraceSpace([0, 0, 1000, 1]);
  187. manager.initializePhysicalSpace(1000, 1);
  188. expect(manager.computeTransformXFromTimestamp(50)).toEqual(50);
  189. });
  190. it('computes x position correctly when view is offset', () => {
  191. const manager = new VirtualizedViewManager({
  192. list: {width: 0},
  193. span_list: {width: 1},
  194. });
  195. manager.initializeTraceSpace([50, 0, 1000, 1]);
  196. manager.initializePhysicalSpace(1000, 1);
  197. manager.trace_view.x = 50;
  198. expect(manager.computeTransformXFromTimestamp(-50)).toEqual(-150);
  199. });
  200. it('when view is offset and scaled', () => {
  201. const manager = new VirtualizedViewManager({
  202. list: {width: 0},
  203. span_list: {width: 1},
  204. });
  205. manager.initializeTraceSpace([50, 0, 100, 1]);
  206. manager.initializePhysicalSpace(1000, 1);
  207. manager.trace_view.width = 50;
  208. manager.trace_view.x = 50;
  209. expect(Math.round(manager.computeTransformXFromTimestamp(75))).toEqual(-250);
  210. });
  211. });
  212. describe('getConfigSpaceCursor', () => {
  213. it('returns the correct x position', () => {
  214. const manager = new VirtualizedViewManager({
  215. list: {width: 0},
  216. span_list: {width: 1},
  217. });
  218. manager.initializeTraceSpace([0, 0, 100, 1]);
  219. manager.initializePhysicalSpace(1000, 1);
  220. expect(manager.getConfigSpaceCursor({x: 500, y: 0})).toEqual([50, 0]);
  221. });
  222. it('returns the correct x position when view scaled', () => {
  223. const manager = new VirtualizedViewManager({
  224. list: {width: 0},
  225. span_list: {width: 1},
  226. });
  227. manager.initializeTraceSpace([0, 0, 100, 1]);
  228. manager.initializePhysicalSpace(1000, 1);
  229. manager.trace_view.x = 50;
  230. manager.trace_view.width = 50;
  231. expect(manager.getConfigSpaceCursor({x: 500, y: 0})).toEqual([75, 0]);
  232. });
  233. it('returns the correct x position when view is offset', () => {
  234. const manager = new VirtualizedViewManager({
  235. list: {width: 0},
  236. span_list: {width: 1},
  237. });
  238. manager.initializeTraceSpace([0, 0, 100, 1]);
  239. manager.initializePhysicalSpace(1000, 1);
  240. manager.trace_view.x = 50;
  241. expect(manager.getConfigSpaceCursor({x: 500, y: 0})).toEqual([100, 0]);
  242. });
  243. });
  244. describe('scrollToPath', () => {
  245. const organization = OrganizationFixture();
  246. const api = new MockApiClient();
  247. const manager = new VirtualizedViewManager({
  248. list: {width: 0.5},
  249. span_list: {width: 0.5},
  250. });
  251. it('scrolls to root node', async () => {
  252. const tree = TraceTree.FromTrace(
  253. makeTrace({
  254. transactions: [makeTransaction()],
  255. orphan_errors: [],
  256. })
  257. );
  258. manager.list = makeList();
  259. const result = await manager.scrollToPath(tree, tree.list[0].path, () => void 0, {
  260. api: api,
  261. organization,
  262. });
  263. expect(result?.node).toBe(tree.list[0]);
  264. expect(manager.list.scrollToRow).toHaveBeenCalledWith(0);
  265. });
  266. it('scrolls to transaction', async () => {
  267. const tree = TraceTree.FromTrace(
  268. makeTrace({
  269. transactions: [
  270. makeTransaction(),
  271. makeTransaction({
  272. event_id: 'event_id',
  273. children: [],
  274. }),
  275. ],
  276. })
  277. );
  278. manager.list = makeList();
  279. const result = await manager.scrollToPath(tree, ['txn:event_id'], () => void 0, {
  280. api: api,
  281. organization,
  282. });
  283. expect(result?.node).toBe(tree.list[2]);
  284. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  285. });
  286. it('scrolls to nested transaction', async () => {
  287. const tree = TraceTree.FromTrace(
  288. makeTrace({
  289. transactions: [
  290. makeTransaction({
  291. event_id: 'root',
  292. children: [
  293. makeTransaction({
  294. event_id: 'child',
  295. children: [
  296. makeTransaction({
  297. event_id: 'event_id',
  298. children: [],
  299. }),
  300. ],
  301. }),
  302. ],
  303. }),
  304. ],
  305. })
  306. );
  307. manager.list = makeList();
  308. expect(tree.list[tree.list.length - 1].path).toEqual([
  309. 'txn:event_id',
  310. 'txn:child',
  311. 'txn:root',
  312. ]);
  313. const result = await manager.scrollToPath(
  314. tree,
  315. ['txn:event_id', 'txn:child', 'txn:root'],
  316. () => void 0,
  317. {
  318. api: api,
  319. organization,
  320. }
  321. );
  322. expect(result?.node).toBe(tree.list[tree.list.length - 1]);
  323. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  324. });
  325. it('scrolls to spans of expanded transaction', async () => {
  326. manager.list = makeList();
  327. const tree = TraceTree.FromTrace(
  328. makeTrace({
  329. transactions: [
  330. makeTransaction({
  331. event_id: 'event_id',
  332. project_slug: 'project_slug',
  333. children: [],
  334. }),
  335. ],
  336. })
  337. );
  338. MockApiClient.addMockResponse({
  339. url: '/organizations/org-slug/events/project_slug:event_id/',
  340. method: 'GET',
  341. body: makeEvent(undefined, [makeSpan({span_id: 'span_id'})]),
  342. });
  343. const result = await manager.scrollToPath(
  344. tree,
  345. ['span:span_id', 'txn:event_id'],
  346. () => void 0,
  347. {
  348. api: api,
  349. organization,
  350. }
  351. );
  352. expect(tree.list[1].zoomedIn).toBe(true);
  353. expect(result?.node).toBe(tree.list[2]);
  354. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  355. });
  356. it('scrolls to span -> transaction -> span -> transaction', async () => {
  357. manager.list = makeList();
  358. const tree = TraceTree.FromTrace(
  359. makeTrace({
  360. transactions: [
  361. makeTransaction({
  362. event_id: 'event_id',
  363. project_slug: 'project_slug',
  364. children: [
  365. makeTransaction({
  366. parent_span_id: 'child_span',
  367. event_id: 'child_event_id',
  368. project_slug: 'project_slug',
  369. }),
  370. ],
  371. }),
  372. ],
  373. })
  374. );
  375. MockApiClient.addMockResponse({
  376. url: '/organizations/org-slug/events/project_slug:event_id/',
  377. method: 'GET',
  378. body: makeEvent(undefined, [
  379. makeSpan({span_id: 'other_child_span'}),
  380. makeSpan({span_id: 'child_span'}),
  381. ]),
  382. });
  383. MockApiClient.addMockResponse({
  384. url: '/organizations/org-slug/events/project_slug:child_event_id/',
  385. method: 'GET',
  386. body: makeEvent(undefined, [makeSpan({span_id: 'other_child_span'})]),
  387. });
  388. const result = await manager.scrollToPath(
  389. tree,
  390. ['span:other_child_span', 'txn:child_event_id', 'txn:event_id'],
  391. () => void 0,
  392. {
  393. api: api,
  394. organization,
  395. }
  396. );
  397. expect(result).toBeTruthy();
  398. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  399. });
  400. describe('scrolls to directly autogrouped node', () => {
  401. for (const headOrTailId of ['head_span', 'tail_span']) {
  402. it('scrolls to directly autogrouped node head', async () => {
  403. manager.list = makeList();
  404. const tree = makeSingleTransactionTree();
  405. MockApiClient.addMockResponse({
  406. url: '/organizations/org-slug/events/project:event_id/',
  407. method: 'GET',
  408. body: makeEvent({}, makeParentAutogroupSpans()),
  409. });
  410. const result = await manager.scrollToPath(
  411. tree,
  412. [`ag:${headOrTailId}`, 'txn:event_id'],
  413. () => void 0,
  414. {
  415. api: api,
  416. organization,
  417. }
  418. );
  419. expect(result).toBeTruthy();
  420. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  421. });
  422. }
  423. for (const headOrTailId of ['head_span', 'tail_span']) {
  424. it('scrolls to child of autogrouped node head or tail', async () => {
  425. manager.list = makeList();
  426. const tree = makeSingleTransactionTree();
  427. MockApiClient.addMockResponse({
  428. url: '/organizations/org-slug/events/project:event_id/',
  429. method: 'GET',
  430. body: makeEvent({}, makeParentAutogroupSpans()),
  431. });
  432. const result = await manager.scrollToPath(
  433. tree,
  434. ['span:middle_span', `ag:${headOrTailId}`, 'txn:event_id'],
  435. () => void 0,
  436. {
  437. api: api,
  438. organization,
  439. }
  440. );
  441. expect(result).toBeTruthy();
  442. expect(manager.list.scrollToRow).toHaveBeenCalledWith(4);
  443. });
  444. }
  445. });
  446. describe('sibling autogrouping', () => {
  447. it('scrolls to child span of sibling autogrouped node', async () => {
  448. manager.list = makeList();
  449. const tree = makeSingleTransactionTree();
  450. MockApiClient.addMockResponse({
  451. url: '/organizations/org-slug/events/project:event_id/',
  452. method: 'GET',
  453. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  454. });
  455. const result = await manager.scrollToPath(
  456. tree,
  457. ['span:middle_span', `ag:first_span`, 'txn:event_id'],
  458. () => void 0,
  459. {
  460. api: api,
  461. organization,
  462. }
  463. );
  464. expect(result).toBeTruthy();
  465. expect(manager.list.scrollToRow).toHaveBeenCalledWith(4);
  466. });
  467. });
  468. describe('missing instrumentation', () => {
  469. it('scrolls to missing instrumentation via previous span_id', async () => {
  470. manager.list = makeList();
  471. const tree = makeSingleTransactionTree();
  472. MockApiClient.addMockResponse({
  473. url: '/organizations/org-slug/events/project:event_id/',
  474. method: 'GET',
  475. body: makeEvent({}, [
  476. makeSpan({
  477. description: 'span',
  478. op: 'db',
  479. start_timestamp: 0,
  480. timestamp: 0.5,
  481. span_id: 'first_span',
  482. }),
  483. makeSpan({
  484. description: 'span',
  485. op: 'db',
  486. start_timestamp: 0.7,
  487. timestamp: 1,
  488. span_id: 'middle_span',
  489. }),
  490. ]),
  491. });
  492. const result = await manager.scrollToPath(
  493. tree,
  494. ['ms:first_span', 'txn:event_id'],
  495. () => void 0,
  496. {
  497. api: api,
  498. organization,
  499. }
  500. );
  501. expect(result).toBeTruthy();
  502. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  503. });
  504. it('scrolls to missing instrumentation via next span_id', async () => {
  505. manager.list = makeList();
  506. const tree = makeSingleTransactionTree();
  507. MockApiClient.addMockResponse({
  508. url: '/organizations/org-slug/events/project:event_id/',
  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: 'second_span',
  524. }),
  525. ]),
  526. });
  527. const result = await manager.scrollToPath(
  528. tree,
  529. ['ms:second_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. });
  540. describe('scrolls to orphan error', () => {
  541. it('scrolls to orphan error', async () => {
  542. manager.list = makeList();
  543. const tree = TraceTree.FromTrace(
  544. makeTrace({
  545. transactions: [makeTransaction()],
  546. orphan_errors: [
  547. {
  548. event_id: 'ded',
  549. project_slug: 'project_slug',
  550. project_id: 1,
  551. issue: 'whoa rusty',
  552. issue_id: 0,
  553. span: '',
  554. level: 'error',
  555. title: 'ded fo good',
  556. },
  557. ],
  558. })
  559. );
  560. const result = await manager.scrollToPath(tree, ['error:ded'], () => void 0, {
  561. api: api,
  562. organization,
  563. });
  564. expect(result?.node).toBe(tree.list[2]);
  565. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  566. });
  567. });
  568. });
  569. });