virtualizedViewManager.spec.tsx 18 KB

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