virtualizedViewManager.spec.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  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 transaction', async () => {
  250. const tree = TraceTree.FromTrace(
  251. makeTrace({
  252. transactions: [
  253. makeTransaction(),
  254. makeTransaction({
  255. event_id: 'event_id',
  256. children: [],
  257. }),
  258. ],
  259. })
  260. );
  261. manager.list = makeList();
  262. const result = await manager.scrollToPath(tree, ['txn:event_id'], () => void 0, {
  263. api: api,
  264. organization,
  265. });
  266. expect(result).toBe(tree.list[2]);
  267. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  268. });
  269. it('scrolls to nested transaction', async () => {
  270. const tree = TraceTree.FromTrace(
  271. makeTrace({
  272. transactions: [
  273. makeTransaction({
  274. event_id: 'root',
  275. children: [
  276. makeTransaction({
  277. event_id: 'child',
  278. children: [
  279. makeTransaction({
  280. event_id: 'event_id',
  281. children: [],
  282. }),
  283. ],
  284. }),
  285. ],
  286. }),
  287. ],
  288. })
  289. );
  290. manager.list = makeList();
  291. expect(tree.list[tree.list.length - 1].path).toEqual([
  292. 'txn:event_id',
  293. 'txn:child',
  294. 'txn:root',
  295. ]);
  296. const result = await manager.scrollToPath(
  297. tree,
  298. ['txn:event_id', 'txn:child', 'txn:root'],
  299. () => void 0,
  300. {
  301. api: api,
  302. organization,
  303. }
  304. );
  305. expect(result).toBe(tree.list[tree.list.length - 1]);
  306. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  307. });
  308. it('scrolls to spans of expanded transaction', async () => {
  309. manager.list = makeList();
  310. const tree = TraceTree.FromTrace(
  311. makeTrace({
  312. transactions: [
  313. makeTransaction({
  314. event_id: 'event_id',
  315. project_slug: 'project_slug',
  316. children: [],
  317. }),
  318. ],
  319. })
  320. );
  321. MockApiClient.addMockResponse({
  322. url: '/organizations/org-slug/events/project_slug:event_id/',
  323. method: 'GET',
  324. body: makeEvent(undefined, [makeSpan({span_id: 'span_id'})]),
  325. });
  326. const result = await manager.scrollToPath(
  327. tree,
  328. ['span:span_id', 'txn:event_id'],
  329. () => void 0,
  330. {
  331. api: api,
  332. organization,
  333. }
  334. );
  335. expect(tree.list[1].zoomedIn).toBe(true);
  336. expect(result).toBeTruthy();
  337. expect(result).toBe(tree.list[2]);
  338. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  339. });
  340. it('scrolls to span -> transaction -> span -> transaction', async () => {
  341. manager.list = makeList();
  342. const tree = TraceTree.FromTrace(
  343. makeTrace({
  344. transactions: [
  345. makeTransaction({
  346. event_id: 'event_id',
  347. project_slug: 'project_slug',
  348. children: [
  349. makeTransaction({
  350. parent_span_id: 'child_span',
  351. event_id: 'child_event_id',
  352. project_slug: 'project_slug',
  353. }),
  354. ],
  355. }),
  356. ],
  357. })
  358. );
  359. MockApiClient.addMockResponse({
  360. url: '/organizations/org-slug/events/project_slug:event_id/',
  361. method: 'GET',
  362. body: makeEvent(undefined, [
  363. makeSpan({span_id: 'other_child_span'}),
  364. makeSpan({span_id: 'child_span'}),
  365. ]),
  366. });
  367. MockApiClient.addMockResponse({
  368. url: '/organizations/org-slug/events/project_slug:child_event_id/',
  369. method: 'GET',
  370. body: makeEvent(undefined, [makeSpan({span_id: 'other_child_span'})]),
  371. });
  372. const result = await manager.scrollToPath(
  373. tree,
  374. ['span:other_child_span', 'txn:child_event_id', 'txn:event_id'],
  375. () => void 0,
  376. {
  377. api: api,
  378. organization,
  379. }
  380. );
  381. expect(result).toBeTruthy();
  382. expect(manager.list.scrollToRow).toHaveBeenCalledWith(3);
  383. });
  384. describe('scrolls to directly autogrouped node', () => {
  385. for (const headOrTailId of ['head_span', 'tail_span']) {
  386. it('scrolls to directly autogrouped node head', async () => {
  387. manager.list = makeList();
  388. const tree = makeSingleTransactionTree();
  389. MockApiClient.addMockResponse({
  390. url: '/organizations/org-slug/events/project:event_id/',
  391. method: 'GET',
  392. body: makeEvent({}, makeParentAutogroupSpans()),
  393. });
  394. const result = await manager.scrollToPath(
  395. tree,
  396. [`ag:${headOrTailId}`, 'txn:event_id'],
  397. () => void 0,
  398. {
  399. api: api,
  400. organization,
  401. }
  402. );
  403. expect(result).toBeTruthy();
  404. expect(manager.list.scrollToRow).toHaveBeenCalledWith(2);
  405. });
  406. }
  407. for (const headOrTailId of ['head_span', 'tail_span']) {
  408. it('scrolls to child of autogrouped node head or tail', async () => {
  409. manager.list = makeList();
  410. const tree = makeSingleTransactionTree();
  411. MockApiClient.addMockResponse({
  412. url: '/organizations/org-slug/events/project:event_id/',
  413. method: 'GET',
  414. body: makeEvent({}, makeParentAutogroupSpans()),
  415. });
  416. const result = await manager.scrollToPath(
  417. tree,
  418. ['span:middle_span', `ag:${headOrTailId}`, 'txn:event_id'],
  419. () => void 0,
  420. {
  421. api: api,
  422. organization,
  423. }
  424. );
  425. expect(result).toBeTruthy();
  426. expect(manager.list.scrollToRow).toHaveBeenCalledWith(4);
  427. });
  428. }
  429. });
  430. describe('sibling autogrouping', () => {
  431. it('scrolls to child span of sibling autogrouped node', async () => {
  432. manager.list = makeList();
  433. const tree = makeSingleTransactionTree();
  434. MockApiClient.addMockResponse({
  435. url: '/organizations/org-slug/events/project:event_id/',
  436. method: 'GET',
  437. body: makeEvent({}, makeSiblingAutogroupedSpans()),
  438. });
  439. const result = await manager.scrollToPath(
  440. tree,
  441. ['span:middle_span', `ag:first_span`, 'txn:event_id'],
  442. () => void 0,
  443. {
  444. api: api,
  445. organization,
  446. }
  447. );
  448. expect(result).toBeTruthy();
  449. expect(manager.list.scrollToRow).toHaveBeenCalledWith(4);
  450. });
  451. it.todo('scrolls to orphan transactions');
  452. it.todo('scrolls to orphan transactions child span');
  453. });
  454. });
  455. });