virtualizedViewManager.spec.tsx 18 KB

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