traceTree.spec.tsx 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509
  1. import {OrganizationFixture} from 'sentry-fixture/organization';
  2. import {waitFor} from 'sentry-test/reactTestingLibrary';
  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 {TraceType} from '../traceDetails/newTraceDetailsContent';
  10. import {
  11. isAutogroupedNode,
  12. isMissingInstrumentationNode,
  13. isSpanNode,
  14. isTransactionNode,
  15. } from './guards';
  16. import {
  17. ParentAutogroupNode,
  18. type SiblingAutogroupNode,
  19. TraceTree,
  20. TraceTreeNode,
  21. } from './traceTree';
  22. function makeTrace(
  23. overrides: Partial<TraceSplitResults<TraceFullDetailed>>
  24. ): TraceSplitResults<TraceFullDetailed> {
  25. return {
  26. transactions: [],
  27. orphan_errors: [],
  28. ...overrides,
  29. } as TraceSplitResults<TraceFullDetailed>;
  30. }
  31. function makeTransaction(overrides: Partial<TraceFullDetailed> = {}): TraceFullDetailed {
  32. return {
  33. children: [],
  34. start_timestamp: 0,
  35. timestamp: 1,
  36. transaction: 'transaction',
  37. 'transaction.op': '',
  38. 'transaction.status': '',
  39. ...overrides,
  40. } as TraceFullDetailed;
  41. }
  42. function makeSpan(overrides: Partial<RawSpanType> = {}): RawSpanType {
  43. return {
  44. op: '',
  45. description: '',
  46. span_id: '',
  47. start_timestamp: 0,
  48. timestamp: 10,
  49. ...overrides,
  50. } as RawSpanType;
  51. }
  52. function makeTraceError(
  53. overrides: Partial<TraceTree.TraceError> = {}
  54. ): TraceTree.TraceError {
  55. return {
  56. title: 'MaybeEncodingError: Error sending result',
  57. level: 'error',
  58. data: {},
  59. ...overrides,
  60. } as TraceTree.TraceError;
  61. }
  62. function makeEvent(overrides: Partial<Event> = {}, spans: RawSpanType[] = []): Event {
  63. return {
  64. entries: [{type: EntryType.SPANS, data: spans}],
  65. ...overrides,
  66. } as Event;
  67. }
  68. function assertSpanNode(
  69. node: TraceTreeNode<TraceTree.NodeValue>
  70. ): asserts node is TraceTreeNode<TraceTree.Span> {
  71. if (!isSpanNode(node)) {
  72. throw new Error('node is not a span');
  73. }
  74. }
  75. // function assertTraceNode(
  76. // node: TraceTreeNode<TraceTree.NodeValue>
  77. // ): asserts node is TraceTreeNode<TraceTree.Trace> {
  78. // if (!isTraceNode(node)) {
  79. // throw new Error('node is not a trace');
  80. // }
  81. // }
  82. function assertTransactionNode(
  83. node: TraceTreeNode<TraceTree.NodeValue> | null
  84. ): asserts node is TraceTreeNode<TraceTree.Transaction> {
  85. if (!node || !isTransactionNode(node)) {
  86. throw new Error('node is not a transaction');
  87. }
  88. }
  89. function assertMissingInstrumentationNode(
  90. node: TraceTreeNode<TraceTree.NodeValue>
  91. ): asserts node is TraceTreeNode<TraceTree.MissingInstrumentationSpan> {
  92. if (!isMissingInstrumentationNode(node)) {
  93. throw new Error('node is not a missing instrumentation node');
  94. }
  95. }
  96. function assertAutogroupedNode(
  97. node: TraceTreeNode<TraceTree.NodeValue>
  98. ): asserts node is ParentAutogroupNode | SiblingAutogroupNode {
  99. if (!isAutogroupedNode(node)) {
  100. throw new Error('node is not a autogrouped node');
  101. }
  102. }
  103. function assertParentAutogroupedNode(
  104. node: TraceTreeNode<TraceTree.NodeValue>
  105. ): asserts node is ParentAutogroupNode {
  106. if (!(node instanceof ParentAutogroupNode)) {
  107. throw new Error('node is not a parent autogrouped node');
  108. }
  109. }
  110. // function _assertSiblingAutogroupedNode(
  111. // node: TraceTreeNode<TraceTree.NodeValue>
  112. // ): asserts node is ParentAutogroupNode {
  113. // if (!(node instanceof SiblingAutogroupNode)) {
  114. // throw new Error('node is not a parent node');
  115. // }
  116. // }
  117. describe('TreeNode', () => {
  118. it('expands transaction nodes by default', () => {
  119. const node = new TraceTreeNode(null, makeTransaction(), {
  120. project_slug: '',
  121. event_id: '',
  122. });
  123. expect(node.expanded).toBe(true);
  124. });
  125. it('points parent to node', () => {
  126. const root = new TraceTreeNode(null, makeTransaction(), {
  127. project_slug: '',
  128. event_id: '',
  129. });
  130. const child = new TraceTreeNode(root, makeTransaction(), {
  131. project_slug: '',
  132. event_id: '',
  133. });
  134. expect(child.parent).toBe(root);
  135. });
  136. it('depth', () => {
  137. const root = new TraceTreeNode(null, makeTransaction(), {
  138. project_slug: '',
  139. event_id: '',
  140. });
  141. const child = new TraceTreeNode(root, makeTransaction(), {
  142. project_slug: '',
  143. event_id: '',
  144. });
  145. const grandChild = new TraceTreeNode(child, makeTransaction(), {
  146. project_slug: '',
  147. event_id: '',
  148. });
  149. expect(grandChild.depth).toBe(1);
  150. });
  151. it('getVisibleChildren', () => {
  152. const root = new TraceTreeNode(null, makeTransaction(), {
  153. project_slug: '',
  154. event_id: '',
  155. });
  156. const child = new TraceTreeNode(root, makeTransaction(), {
  157. project_slug: '',
  158. event_id: '',
  159. });
  160. root.children.push(child);
  161. expect(root.getVisibleChildren()).toHaveLength(1);
  162. expect(root.getVisibleChildren()[0]).toBe(child);
  163. root.expanded = false;
  164. expect(root.getVisibleChildren()).toHaveLength(0);
  165. });
  166. it('getVisibleChildrenCount', () => {
  167. const root = new TraceTreeNode(null, makeTransaction(), {
  168. project_slug: '',
  169. event_id: '',
  170. });
  171. const child = new TraceTreeNode(root, makeTransaction(), {
  172. project_slug: '',
  173. event_id: '',
  174. });
  175. root.children.push(child);
  176. expect(root.getVisibleChildrenCount()).toBe(1);
  177. root.expanded = false;
  178. expect(root.getVisibleChildrenCount()).toBe(0);
  179. });
  180. });
  181. describe('TraceTree', () => {
  182. beforeEach(() => {
  183. MockApiClient.clearMockResponses();
  184. });
  185. it('builds from transactions', () => {
  186. const tree = TraceTree.FromTrace(
  187. makeTrace({
  188. transactions: [
  189. makeTransaction({
  190. children: [],
  191. }),
  192. makeTransaction({
  193. children: [],
  194. }),
  195. ],
  196. })
  197. );
  198. expect(tree.list).toHaveLength(3);
  199. });
  200. it('builds orphan errors as well', () => {
  201. const tree = TraceTree.FromTrace(
  202. makeTrace({
  203. transactions: [
  204. makeTransaction({
  205. children: [],
  206. }),
  207. makeTransaction({
  208. children: [],
  209. }),
  210. ],
  211. orphan_errors: [makeTraceError()],
  212. })
  213. );
  214. expect(tree.list).toHaveLength(4);
  215. });
  216. it('calculates correct trace type', () => {
  217. let tree = TraceTree.FromTrace(
  218. makeTrace({
  219. transactions: [],
  220. orphan_errors: [],
  221. })
  222. );
  223. expect(TraceTree.GetTraceType(tree.root)).toBe(TraceType.EMPTY_TRACE);
  224. tree = TraceTree.FromTrace(
  225. makeTrace({
  226. transactions: [
  227. makeTransaction({
  228. children: [],
  229. }),
  230. makeTransaction({
  231. children: [],
  232. }),
  233. ],
  234. orphan_errors: [],
  235. })
  236. );
  237. expect(TraceTree.GetTraceType(tree.root)).toBe(TraceType.NO_ROOT);
  238. tree = TraceTree.FromTrace(
  239. makeTrace({
  240. transactions: [
  241. makeTransaction({
  242. parent_span_id: null,
  243. children: [],
  244. }),
  245. ],
  246. orphan_errors: [],
  247. })
  248. );
  249. expect(TraceTree.GetTraceType(tree.root)).toBe(TraceType.ONE_ROOT);
  250. tree = TraceTree.FromTrace(
  251. makeTrace({
  252. transactions: [
  253. makeTransaction({
  254. parent_span_id: null,
  255. children: [],
  256. }),
  257. makeTransaction({
  258. children: [],
  259. }),
  260. ],
  261. orphan_errors: [],
  262. })
  263. );
  264. expect(TraceTree.GetTraceType(tree.root)).toBe(TraceType.BROKEN_SUBTRACES);
  265. tree = TraceTree.FromTrace(
  266. makeTrace({
  267. transactions: [
  268. makeTransaction({
  269. parent_span_id: null,
  270. children: [],
  271. }),
  272. makeTransaction({
  273. parent_span_id: null,
  274. children: [],
  275. }),
  276. ],
  277. orphan_errors: [],
  278. })
  279. );
  280. expect(TraceTree.GetTraceType(tree.root)).toBe(TraceType.MULTIPLE_ROOTS);
  281. tree = TraceTree.FromTrace(
  282. makeTrace({
  283. transactions: [],
  284. orphan_errors: [makeTraceError()],
  285. })
  286. );
  287. expect(TraceTree.GetTraceType(tree.root)).toBe(TraceType.ONLY_ERRORS);
  288. });
  289. it('builds from spans when root is a transaction node', () => {
  290. const root = new TraceTreeNode(
  291. null,
  292. makeTransaction({
  293. children: [],
  294. }),
  295. {project_slug: '', event_id: ''}
  296. );
  297. const node = TraceTree.FromSpans(
  298. root,
  299. [
  300. makeSpan({start_timestamp: 0, op: '1', span_id: '1'}),
  301. makeSpan({start_timestamp: 1, op: '2', span_id: '2', parent_span_id: '1'}),
  302. makeSpan({start_timestamp: 2, op: '3', span_id: '3', parent_span_id: '2'}),
  303. makeSpan({start_timestamp: 3, op: '4', span_id: '4', parent_span_id: '1'}),
  304. ],
  305. {sdk: undefined}
  306. );
  307. if (!isSpanNode(node.children[0])) {
  308. throw new Error('Child needs to be a span');
  309. }
  310. expect(node.children[0].value.span_id).toBe('1');
  311. expect(node.children[0].value.start_timestamp).toBe(0);
  312. expect(node.children.length).toBe(1);
  313. assertSpanNode(node.children[0].children[0]);
  314. assertSpanNode(node.children[0].children[0].children[0]);
  315. assertSpanNode(node.children[0].children[1]);
  316. expect(node.children[0].children[0].value.start_timestamp).toBe(1);
  317. expect(node.children[0].children[0].children[0].value.start_timestamp).toBe(2);
  318. expect(node.children[0].children[1].value.start_timestamp).toBe(3);
  319. });
  320. it('builds from spans and copies txn nodes', () => {
  321. // transaction transaction
  322. // - child transaction -> - span
  323. // - child-transaction
  324. // - span
  325. const root = new TraceTreeNode(
  326. null,
  327. makeTransaction({
  328. children: [],
  329. }),
  330. {project_slug: '', event_id: ''}
  331. );
  332. root.children.push(
  333. new TraceTreeNode(
  334. root,
  335. makeTransaction({
  336. parent_span_id: 'child-transaction',
  337. }),
  338. {project_slug: '', event_id: ''}
  339. )
  340. );
  341. const node = TraceTree.FromSpans(
  342. root,
  343. [
  344. makeSpan({start_timestamp: 0, timestamp: 0.1, op: 'span', span_id: 'none'}),
  345. makeSpan({
  346. start_timestamp: 0.1,
  347. timestamp: 0.2,
  348. op: 'child-transaction',
  349. span_id: 'child-transaction',
  350. }),
  351. makeSpan({start_timestamp: 0.2, timestamp: 0.25, op: 'span', span_id: 'none'}),
  352. ],
  353. {sdk: undefined}
  354. );
  355. assertTransactionNode(node.children[1]);
  356. });
  357. it('builds from spans and copies txn nodes to nested children', () => {
  358. // parent transaction parent transaction
  359. // - child transaction -> - span
  360. // - grandchild transaction -> - child-transaction
  361. // - grandchild-transaction
  362. //
  363. const root = new TraceTreeNode(
  364. null,
  365. makeTransaction({
  366. span_id: 'parent-transaction',
  367. children: [],
  368. }),
  369. {project_slug: '', event_id: ''}
  370. );
  371. let start: TraceTreeNode<TraceTree.NodeValue> = root;
  372. for (let i = 0; i < 2; i++) {
  373. const node = new TraceTreeNode(
  374. start,
  375. makeTransaction({
  376. transaction: `${i === 0 ? 'child' : 'grandchild'}-transaction`,
  377. parent_span_id: `${i === 0 ? 'child' : 'grandchild'}-transaction`,
  378. }),
  379. {project_slug: '', event_id: ''}
  380. );
  381. start.children.push(node);
  382. start = node;
  383. }
  384. const node = TraceTree.FromSpans(
  385. root,
  386. [
  387. makeSpan({start_timestamp: 0, timestamp: 0.1, op: 'span', span_id: 'none'}),
  388. makeSpan({
  389. start_timestamp: 0.1,
  390. timestamp: 0.2,
  391. op: 'child-transaction',
  392. span_id: 'child-transaction',
  393. }),
  394. ],
  395. {sdk: undefined}
  396. );
  397. assertTransactionNode(node.children[1]);
  398. assertTransactionNode(node.children[1].children[0]);
  399. });
  400. it('injects missing spans', () => {
  401. const root = new TraceTreeNode(
  402. null,
  403. makeTransaction({
  404. children: [],
  405. }),
  406. {project_slug: '', event_id: ''}
  407. );
  408. const date = new Date().getTime();
  409. const node = TraceTree.FromSpans(
  410. root,
  411. [
  412. makeSpan({
  413. start_timestamp: date,
  414. timestamp: date + 1,
  415. span_id: '1',
  416. op: 'span 1',
  417. }),
  418. makeSpan({
  419. start_timestamp: date + 2,
  420. timestamp: date + 4,
  421. op: 'span 2',
  422. span_id: '2',
  423. }),
  424. ],
  425. {sdk: undefined}
  426. );
  427. assertSpanNode(node.children[0]);
  428. assertMissingInstrumentationNode(node.children[1]);
  429. assertSpanNode(node.children[2]);
  430. expect(node.children.length).toBe(3);
  431. expect(node.children[0].value.op).toBe('span 1');
  432. expect(node.children[1].value.type).toBe('missing_instrumentation');
  433. expect(node.children[2].value.op).toBe('span 2');
  434. });
  435. it('does not inject missing spans for javascript platform', () => {
  436. const root = new TraceTreeNode(
  437. null,
  438. makeTransaction({
  439. children: [],
  440. }),
  441. {project_slug: '', event_id: ''}
  442. );
  443. const date = new Date().getTime();
  444. const node = TraceTree.FromSpans(
  445. root,
  446. [
  447. makeSpan({
  448. start_timestamp: date,
  449. timestamp: date + 1,
  450. span_id: '1',
  451. op: 'span 1',
  452. }),
  453. makeSpan({
  454. start_timestamp: date + 2,
  455. timestamp: date + 4,
  456. op: 'span 2',
  457. span_id: '2',
  458. }),
  459. ],
  460. {sdk: 'sentry.javascript.browser'}
  461. );
  462. assertSpanNode(node.children[0]);
  463. assertSpanNode(node.children[1]);
  464. expect(node.children.length).toBe(2);
  465. expect(node.children[0].value.op).toBe('span 1');
  466. expect(node.children[1].value.op).toBe('span 2');
  467. });
  468. it('builds and preserves list order', async () => {
  469. const organization = OrganizationFixture();
  470. const api = new MockApiClient();
  471. const tree = TraceTree.FromTrace(
  472. makeTrace({
  473. transactions: [
  474. makeTransaction({
  475. transaction: 'txn 1',
  476. start_timestamp: 0,
  477. children: [makeTransaction({start_timestamp: 1, transaction: 'txn 2'})],
  478. }),
  479. ],
  480. })
  481. );
  482. tree.expand(tree.list[0], true);
  483. const node = tree.list[1];
  484. const request = MockApiClient.addMockResponse({
  485. url: '/organizations/org-slug/events/undefined:undefined/',
  486. method: 'GET',
  487. body: makeEvent({startTimestamp: 0}, [
  488. makeSpan({start_timestamp: 1, op: 'span 1', span_id: '1'}),
  489. makeSpan({
  490. start_timestamp: 2,
  491. op: 'span 2',
  492. span_id: '2',
  493. parent_span_id: '1',
  494. }),
  495. makeSpan({start_timestamp: 3, op: 'span 3', parent_span_id: '2'}),
  496. makeSpan({start_timestamp: 4, op: 'span 4', parent_span_id: '1'}),
  497. ]),
  498. });
  499. // 0
  500. // 1
  501. // 2
  502. // 3
  503. // 4
  504. tree.zoomIn(node, true, {api, organization});
  505. await waitFor(() => {
  506. expect(node.zoomedIn).toBe(true);
  507. });
  508. expect(request).toHaveBeenCalled();
  509. expect(tree.list.length).toBe(6);
  510. assertTransactionNode(tree.list[1]);
  511. assertSpanNode(tree.list[2]);
  512. assertSpanNode(tree.list[3]);
  513. expect(tree.list[1].value.start_timestamp).toBe(0);
  514. expect(tree.list[2].value.start_timestamp).toBe(1);
  515. expect(tree.list[3].value.start_timestamp).toBe(2);
  516. });
  517. it('preserves input order', () => {
  518. const firstChild = makeTransaction({
  519. start_timestamp: 0,
  520. timestamp: 1,
  521. children: [],
  522. });
  523. const secondChild = makeTransaction({
  524. start_timestamp: 1,
  525. timestamp: 2,
  526. children: [],
  527. });
  528. const tree = TraceTree.FromTrace(
  529. makeTrace({
  530. transactions: [
  531. makeTransaction({
  532. start_timestamp: 0,
  533. timestamp: 2,
  534. children: [firstChild, secondChild],
  535. }),
  536. makeTransaction({
  537. start_timestamp: 2,
  538. timestamp: 4,
  539. }),
  540. ],
  541. })
  542. );
  543. expect(tree.list).toHaveLength(5);
  544. expect(tree.expand(tree.list[1], false)).toBe(true);
  545. expect(tree.list).toHaveLength(3);
  546. expect(tree.expand(tree.list[1], true)).toBe(true);
  547. expect(tree.list).toHaveLength(5);
  548. expect(tree.list[2].value).toBe(firstChild);
  549. expect(tree.list[3].value).toBe(secondChild);
  550. });
  551. it('creates children -> parent references', () => {
  552. const tree = TraceTree.FromTrace(
  553. makeTrace({
  554. transactions: [
  555. makeTransaction({
  556. start_timestamp: 0,
  557. timestamp: 2,
  558. children: [makeTransaction({start_timestamp: 1, timestamp: 2})],
  559. }),
  560. makeTransaction({
  561. start_timestamp: 2,
  562. timestamp: 4,
  563. }),
  564. ],
  565. })
  566. );
  567. expect(tree.list).toHaveLength(4);
  568. expect(tree.list[2].parent?.value).toBe(tree.list[1].value);
  569. });
  570. it('establishes parent-child relationships', () => {
  571. const tree = TraceTree.FromTrace(
  572. makeTrace({
  573. transactions: [
  574. makeTransaction({
  575. children: [makeTransaction()],
  576. }),
  577. ],
  578. })
  579. );
  580. expect(tree.root.children).toHaveLength(1);
  581. expect(tree.root.children[0].children).toHaveLength(1);
  582. });
  583. it('isLastChild', () => {
  584. const tree = TraceTree.FromTrace(
  585. makeTrace({
  586. transactions: [
  587. makeTransaction({
  588. children: [makeTransaction(), makeTransaction()],
  589. }),
  590. makeTransaction(),
  591. ],
  592. orphan_errors: [],
  593. })
  594. );
  595. tree.expand(tree.list[1], true);
  596. expect(tree.list[0].isLastChild).toBe(true);
  597. expect(tree.list[1].isLastChild).toBe(false);
  598. expect(tree.list[2].isLastChild).toBe(false);
  599. expect(tree.list[3].isLastChild).toBe(true);
  600. expect(tree.list[4].isLastChild).toBe(true);
  601. });
  602. describe('connectors', () => {
  603. it('computes transaction connectors', () => {
  604. const tree = TraceTree.FromTrace(
  605. makeTrace({
  606. transactions: [
  607. makeTransaction({
  608. transaction: 'sibling',
  609. children: [
  610. makeTransaction({transaction: 'child'}),
  611. makeTransaction({transaction: 'child'}),
  612. ],
  613. }),
  614. makeTransaction({transaction: 'sibling'}),
  615. ],
  616. })
  617. );
  618. // -1 root
  619. // ------ list begins here
  620. // 0 transaction
  621. // 0 |- sibling
  622. // -1, 2| | - child
  623. // -1| | - child
  624. // 0 |- sibling
  625. tree.expand(tree.list[1], true);
  626. expect(tree.list.length).toBe(5);
  627. expect(tree.list[0].connectors.length).toBe(0);
  628. expect(tree.list[1].connectors.length).toBe(1);
  629. expect(tree.list[1].connectors[0]).toBe(-1);
  630. expect(tree.list[2].connectors[0]).toBe(-1);
  631. expect(tree.list[2].connectors[1]).toBe(2);
  632. expect(tree.list[2].connectors.length).toBe(2);
  633. expect(tree.list[3].connectors[0]).toBe(-1);
  634. expect(tree.list[3].connectors.length).toBe(1);
  635. expect(tree.list[4].connectors.length).toBe(0);
  636. });
  637. it('computes span connectors', async () => {
  638. const tree = TraceTree.FromTrace(
  639. makeTrace({
  640. transactions: [
  641. makeTransaction({
  642. project_slug: 'project',
  643. event_id: 'event_id',
  644. transaction: 'transaction',
  645. children: [],
  646. }),
  647. ],
  648. })
  649. );
  650. // root
  651. // |- node1 []
  652. // |- node2 []
  653. MockApiClient.addMockResponse({
  654. url: '/organizations/org-slug/events/project:event_id/',
  655. method: 'GET',
  656. body: makeEvent({}, [makeSpan({start_timestamp: 0, op: 'span', span_id: '1'})]),
  657. });
  658. expect(tree.list.length).toBe(2);
  659. tree.zoomIn(tree.list[1], true, {
  660. api: new MockApiClient(),
  661. organization: OrganizationFixture(),
  662. });
  663. await waitFor(() => {
  664. expect(tree.list.length).toBe(3);
  665. });
  666. // root
  667. // |- node1 []
  668. // |- node2 []
  669. // |- span1 []
  670. const span = tree.list[tree.list.length - 1];
  671. expect(span.connectors.length).toBe(0);
  672. });
  673. });
  674. describe('expanding', () => {
  675. it('expands a node and updates the list', () => {
  676. const tree = TraceTree.FromTrace(
  677. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  678. );
  679. const node = tree.list[1];
  680. expect(tree.expand(node, false)).toBe(true);
  681. expect(tree.list.length).toBe(2);
  682. expect(node.expanded).toBe(false);
  683. expect(tree.expand(node, true)).toBe(true);
  684. expect(node.expanded).toBe(true);
  685. // Assert that the list has been updated
  686. expect(tree.list).toHaveLength(3);
  687. expect(tree.list[2]).toBe(node.children[0]);
  688. });
  689. it('collapses a node and updates the list', () => {
  690. const tree = TraceTree.FromTrace(
  691. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  692. );
  693. const node = tree.list[1];
  694. tree.expand(node, true);
  695. expect(tree.list.length).toBe(3);
  696. expect(tree.expand(node, false)).toBe(true);
  697. expect(node.expanded).toBe(false);
  698. // Assert that the list has been updated
  699. expect(tree.list).toHaveLength(2);
  700. expect(tree.list[1]).toBe(node);
  701. });
  702. it('preserves children expanded state', () => {
  703. const tree = TraceTree.FromTrace(
  704. makeTrace({
  705. transactions: [
  706. makeTransaction({
  707. children: [
  708. makeTransaction({children: [makeTransaction({start_timestamp: 1000})]}),
  709. makeTransaction({start_timestamp: 5}),
  710. ],
  711. }),
  712. ],
  713. })
  714. );
  715. expect(tree.expand(tree.list[2], false)).toBe(true);
  716. // Assert that the list has been updated
  717. expect(tree.list).toHaveLength(4);
  718. expect(tree.expand(tree.list[2], true)).toBe(true);
  719. expect(tree.list.length).toBe(5);
  720. expect(tree.list[tree.list.length - 1].value).toEqual(
  721. makeTransaction({start_timestamp: 5})
  722. );
  723. });
  724. it('expanding or collapsing a zoomed in node doesnt do anything', async () => {
  725. const organization = OrganizationFixture();
  726. const api = new MockApiClient();
  727. const tree = TraceTree.FromTrace(
  728. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  729. );
  730. const node = tree.list[0];
  731. const request = MockApiClient.addMockResponse({
  732. url: '/organizations/org-slug/events/undefined:undefined/',
  733. method: 'GET',
  734. body: makeEvent(),
  735. });
  736. tree.zoomIn(node, true, {api, organization});
  737. await waitFor(() => {
  738. expect(node.zoomedIn).toBe(true);
  739. });
  740. expect(request).toHaveBeenCalled();
  741. expect(tree.expand(node, true)).toBe(false);
  742. });
  743. });
  744. describe('zooming', () => {
  745. it('marks node as zoomed in', async () => {
  746. const organization = OrganizationFixture();
  747. const api = new MockApiClient();
  748. const tree = TraceTree.FromTrace(
  749. makeTrace({
  750. transactions: [
  751. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  752. ],
  753. })
  754. );
  755. const request = MockApiClient.addMockResponse({
  756. url: '/organizations/org-slug/events/project:event_id/',
  757. method: 'GET',
  758. body: makeEvent(),
  759. });
  760. const node = tree.list[1];
  761. expect(node.zoomedIn).toBe(false);
  762. tree.zoomIn(node, true, {api, organization});
  763. await waitFor(() => {
  764. expect(node.zoomedIn).toBe(true);
  765. });
  766. expect(request).toHaveBeenCalled();
  767. });
  768. it('fetches spans for node when zooming in', async () => {
  769. const tree = TraceTree.FromTrace(
  770. makeTrace({
  771. transactions: [
  772. makeTransaction({
  773. transaction: 'txn',
  774. project_slug: 'project',
  775. event_id: 'event_id',
  776. }),
  777. ],
  778. })
  779. );
  780. const request = MockApiClient.addMockResponse({
  781. url: '/organizations/org-slug/events/project:event_id/',
  782. method: 'GET',
  783. body: makeEvent({}, [makeSpan()]),
  784. });
  785. const node = tree.list[1];
  786. expect(node.children).toHaveLength(0);
  787. tree.zoomIn(node, true, {
  788. api: new MockApiClient(),
  789. organization: OrganizationFixture(),
  790. });
  791. expect(request).toHaveBeenCalled();
  792. await waitFor(() => {
  793. expect(node.children).toHaveLength(1);
  794. });
  795. // Assert that the children have been updated
  796. assertTransactionNode(node.children[0].parent);
  797. expect(node.children[0].parent.value.transaction).toBe('txn');
  798. expect(node.children[0].depth).toBe(node.depth + 1);
  799. });
  800. it('zooms out', async () => {
  801. const tree = TraceTree.FromTrace(
  802. makeTrace({
  803. transactions: [
  804. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  805. ],
  806. })
  807. );
  808. MockApiClient.addMockResponse({
  809. url: '/organizations/org-slug/events/project:event_id/',
  810. method: 'GET',
  811. body: makeEvent({}, [makeSpan({span_id: 'span1', description: 'span1'})]),
  812. });
  813. tree.zoomIn(tree.list[1], true, {
  814. api: new MockApiClient(),
  815. organization: OrganizationFixture(),
  816. });
  817. await waitFor(() => {
  818. assertSpanNode(tree.list[1].children[0]);
  819. expect(tree.list[1].children[0].value.description).toBe('span1');
  820. });
  821. tree.zoomIn(tree.list[1], false, {
  822. api: new MockApiClient(),
  823. organization: OrganizationFixture(),
  824. });
  825. await waitFor(() => {
  826. // Assert child no longer points to children
  827. expect(tree.list[1].zoomedIn).toBe(false);
  828. expect(tree.list[1].children[0]).toBe(undefined);
  829. expect(tree.list[2]).toBe(undefined);
  830. });
  831. });
  832. it('zooms in and out', async () => {
  833. const tree = TraceTree.FromTrace(
  834. makeTrace({
  835. transactions: [
  836. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  837. ],
  838. })
  839. );
  840. MockApiClient.addMockResponse({
  841. url: '/organizations/org-slug/events/project:event_id/',
  842. method: 'GET',
  843. body: makeEvent({}, [makeSpan({span_id: 'span 1', description: 'span1'})]),
  844. });
  845. // Zoom in
  846. tree.zoomIn(tree.list[1], true, {
  847. api: new MockApiClient(),
  848. organization: OrganizationFixture(),
  849. });
  850. await waitFor(() => {
  851. assertSpanNode(tree.list[1].children[0]);
  852. expect(tree.list[1].children[0].value.description).toBe('span1');
  853. });
  854. // Zoom out
  855. tree.zoomIn(tree.list[1], false, {
  856. api: new MockApiClient(),
  857. organization: OrganizationFixture(),
  858. });
  859. await waitFor(() => {
  860. expect(tree.list[2]).toBe(undefined);
  861. });
  862. // Zoom in
  863. tree.zoomIn(tree.list[1], true, {
  864. api: new MockApiClient(),
  865. organization: OrganizationFixture(),
  866. });
  867. await waitFor(() => {
  868. assertSpanNode(tree.list[1].children[0]);
  869. expect(tree.list[1].children[0].value?.description).toBe('span1');
  870. });
  871. });
  872. it('zooms in and out preserving siblings', async () => {
  873. const tree = TraceTree.FromTrace(
  874. makeTrace({
  875. transactions: [
  876. makeTransaction({
  877. project_slug: 'project',
  878. event_id: 'event_id',
  879. start_timestamp: 0,
  880. children: [
  881. makeTransaction({
  882. start_timestamp: 1,
  883. timestamp: 2,
  884. project_slug: 'other_project',
  885. event_id: 'event_id',
  886. }),
  887. makeTransaction({start_timestamp: 2, timestamp: 3}),
  888. ],
  889. }),
  890. ],
  891. })
  892. );
  893. const request = MockApiClient.addMockResponse({
  894. url: '/organizations/org-slug/events/other_project:event_id/',
  895. method: 'GET',
  896. body: makeEvent({}, [makeSpan({description: 'span1'})]),
  897. });
  898. tree.expand(tree.list[1], true);
  899. tree.zoomIn(tree.list[2], true, {
  900. api: new MockApiClient(),
  901. organization: OrganizationFixture(),
  902. });
  903. expect(request).toHaveBeenCalled();
  904. // Zoom in
  905. await waitFor(() => {
  906. expect(tree.list.length).toBe(5);
  907. });
  908. // Zoom out
  909. tree.zoomIn(tree.list[2], false, {
  910. api: new MockApiClient(),
  911. organization: OrganizationFixture(),
  912. });
  913. await waitFor(() => {
  914. expect(tree.list.length).toBe(4);
  915. });
  916. });
  917. it('preserves expanded state when zooming in and out', async () => {
  918. const tree = TraceTree.FromTrace(
  919. makeTrace({
  920. transactions: [
  921. makeTransaction({
  922. project_slug: 'project',
  923. event_id: 'event_id',
  924. children: [
  925. makeTransaction({project_slug: 'other_project', event_id: 'event_id'}),
  926. ],
  927. }),
  928. ],
  929. })
  930. );
  931. MockApiClient.addMockResponse({
  932. url: '/organizations/org-slug/events/project:event_id/',
  933. method: 'GET',
  934. body: makeEvent({}, [
  935. makeSpan({description: 'span1'}),
  936. makeSpan({description: 'span2'}),
  937. ]),
  938. });
  939. tree.expand(tree.list[1], true);
  940. expect(tree.list.length).toBe(3);
  941. tree.zoomIn(tree.list[1], true, {
  942. api: new MockApiClient(),
  943. organization: OrganizationFixture(),
  944. });
  945. await waitFor(() => {
  946. expect(tree.list.length).toBe(4);
  947. });
  948. tree.zoomIn(tree.list[1], false, {
  949. api: new MockApiClient(),
  950. organization: OrganizationFixture(),
  951. });
  952. await waitFor(() => {
  953. expect(tree.list.length).toBe(3);
  954. });
  955. expect(tree.list[1].expanded).toBe(true);
  956. });
  957. });
  958. describe('autogrouping', () => {
  959. it('auto groups sibling spans and preserves tail spans', () => {
  960. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  961. project_slug: '',
  962. event_id: '',
  963. });
  964. for (let i = 0; i < 5; i++) {
  965. root.children.push(
  966. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  967. project_slug: '',
  968. event_id: '',
  969. })
  970. );
  971. }
  972. root.children.push(
  973. new TraceTreeNode(root, makeSpan({description: 'span', op: 'http'}), {
  974. project_slug: '',
  975. event_id: '',
  976. })
  977. );
  978. expect(root.children.length).toBe(6);
  979. TraceTree.AutogroupSiblingSpanNodes(root);
  980. expect(root.children.length).toBe(2);
  981. });
  982. it('autogroups when number of children is exactly 5', () => {
  983. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  984. project_slug: '',
  985. event_id: '',
  986. });
  987. for (let i = 0; i < 5; i++) {
  988. root.children.push(
  989. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  990. project_slug: '',
  991. event_id: '',
  992. })
  993. );
  994. }
  995. expect(root.children.length).toBe(5);
  996. TraceTree.AutogroupSiblingSpanNodes(root);
  997. expect(root.children.length).toBe(1);
  998. });
  999. it('correctly adds autogrouped siblings as children under autogrouped node', () => {
  1000. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  1001. project_slug: '',
  1002. event_id: '',
  1003. });
  1004. for (let i = 0; i < 5; i++) {
  1005. root.children.push(
  1006. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  1007. project_slug: '',
  1008. event_id: '',
  1009. })
  1010. );
  1011. }
  1012. expect(root.children.length).toBe(5);
  1013. TraceTree.AutogroupSiblingSpanNodes(root);
  1014. expect(root.children.length).toBe(1);
  1015. const autoGroupedNode = root.children[0];
  1016. assertAutogroupedNode(autoGroupedNode);
  1017. expect(autoGroupedNode.groupCount).toBe(5);
  1018. expect(autoGroupedNode.children.length).toBe(5);
  1019. });
  1020. it('autogroups when number of children is > 5', () => {
  1021. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  1022. project_slug: '',
  1023. event_id: '',
  1024. });
  1025. for (let i = 0; i < 7; i++) {
  1026. root.children.push(
  1027. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  1028. project_slug: '',
  1029. event_id: '',
  1030. })
  1031. );
  1032. }
  1033. expect(root.children.length).toBe(7);
  1034. TraceTree.AutogroupSiblingSpanNodes(root);
  1035. expect(root.children.length).toBe(1);
  1036. });
  1037. it('autogroups direct children case', () => {
  1038. // db db db
  1039. // http -> parent autogroup (3) -> parent autogroup (3)
  1040. // http http
  1041. // http http
  1042. // http
  1043. const root: TraceTreeNode<TraceTree.Span> = new TraceTreeNode(
  1044. null,
  1045. makeSpan({
  1046. description: `span1`,
  1047. span_id: `1`,
  1048. op: 'db',
  1049. }),
  1050. {project_slug: '', event_id: ''}
  1051. );
  1052. let last: TraceTreeNode<any> = root;
  1053. for (let i = 0; i < 3; i++) {
  1054. const node = new TraceTreeNode(
  1055. last,
  1056. makeSpan({
  1057. description: `span${i}`,
  1058. span_id: `${i}`,
  1059. op: 'http',
  1060. }),
  1061. {
  1062. project_slug: '',
  1063. event_id: '',
  1064. }
  1065. );
  1066. last.children.push(node);
  1067. last = node;
  1068. }
  1069. if (!root) {
  1070. throw new Error('root is null');
  1071. }
  1072. expect(root.children.length).toBe(1);
  1073. expect(root.children[0].children.length).toBe(1);
  1074. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1075. expect(root.children.length).toBe(1);
  1076. assertAutogroupedNode(root.children[0]);
  1077. expect(root.children[0].children.length).toBe(0);
  1078. root.children[0].expanded = true;
  1079. expect((root.children[0].children[0].value as RawSpanType).description).toBe(
  1080. 'span0'
  1081. );
  1082. });
  1083. it('autogrouping direct children skips rendering intermediary nodes', () => {
  1084. // db db db
  1085. // http autogrouped (3) autogrouped (3)
  1086. // http -> db -> http
  1087. // http http
  1088. // db http
  1089. // db
  1090. const root = new TraceTreeNode(
  1091. null,
  1092. makeSpan({span_id: 'span1', description: 'span1', op: 'db'}),
  1093. {
  1094. project_slug: '',
  1095. event_id: '',
  1096. }
  1097. );
  1098. let last = root;
  1099. for (let i = 0; i < 4; i++) {
  1100. const node = new TraceTreeNode(
  1101. last,
  1102. makeSpan({
  1103. span_id: `span`,
  1104. description: `span`,
  1105. op: i === 3 ? 'db' : 'http',
  1106. }),
  1107. {
  1108. project_slug: '',
  1109. event_id: '',
  1110. }
  1111. );
  1112. last.children.push(node);
  1113. last = node;
  1114. }
  1115. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1116. const autoGroupedNode = root.children[0];
  1117. assertAutogroupedNode(autoGroupedNode);
  1118. expect(autoGroupedNode.children.length).toBe(1);
  1119. expect((autoGroupedNode.children[0].value as RawSpanType).op).toBe('db');
  1120. autoGroupedNode.expanded = true;
  1121. expect(autoGroupedNode.children.length).toBe(1);
  1122. expect((autoGroupedNode.children[0].value as RawSpanType).op).toBe('http');
  1123. });
  1124. it('nested direct autogrouping', () => {
  1125. // db db db
  1126. // http -> parent autogroup (3) -> parent autogroup (3)
  1127. // http db http
  1128. // http parent autogroup (3) http
  1129. // db http
  1130. // http db
  1131. // http parent autogrouped (3)
  1132. // http http
  1133. // http
  1134. // http
  1135. const root = new TraceTreeNode(
  1136. null,
  1137. makeSpan({span_id: 'span', description: 'span', op: 'db'}),
  1138. {
  1139. project_slug: '',
  1140. event_id: '',
  1141. }
  1142. );
  1143. let last = root;
  1144. for (let i = 0; i < 3; i++) {
  1145. if (i === 1) {
  1146. const autogroupBreakingSpan = new TraceTreeNode(
  1147. last,
  1148. makeSpan({span_id: 'span', description: 'span', op: 'db'}),
  1149. {
  1150. project_slug: '',
  1151. event_id: '',
  1152. }
  1153. );
  1154. last.children.push(autogroupBreakingSpan);
  1155. last = autogroupBreakingSpan;
  1156. } else {
  1157. for (let j = 0; j < 3; j++) {
  1158. const node = new TraceTreeNode(
  1159. last,
  1160. makeSpan({span_id: `span${j}`, description: `span${j}`, op: 'http'}),
  1161. {
  1162. project_slug: '',
  1163. event_id: '',
  1164. }
  1165. );
  1166. last.children.push(node);
  1167. last = node;
  1168. }
  1169. }
  1170. }
  1171. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1172. assertAutogroupedNode(root.children[0]);
  1173. assertAutogroupedNode(root.children[0].children[0].children[0]);
  1174. });
  1175. it('sibling autogrouping', () => {
  1176. // db db
  1177. // http sibling autogrouped (5)
  1178. // http
  1179. // http ->
  1180. // http
  1181. // http
  1182. const root = new TraceTreeNode(
  1183. null,
  1184. makeTransaction({start_timestamp: 0, timestamp: 10}),
  1185. {
  1186. project_slug: '',
  1187. event_id: '',
  1188. }
  1189. );
  1190. for (let i = 0; i < 5; i++) {
  1191. root.children.push(
  1192. new TraceTreeNode(root, makeSpan({start_timestamp: i, timestamp: i + 1}), {
  1193. project_slug: '',
  1194. event_id: '',
  1195. })
  1196. );
  1197. }
  1198. TraceTree.AutogroupSiblingSpanNodes(root);
  1199. expect(root.children.length).toBe(1);
  1200. assertAutogroupedNode(root.children[0]);
  1201. });
  1202. it('multiple sibling autogrouping', () => {
  1203. // db db
  1204. // http sibling autogrouped (5)
  1205. // http db
  1206. // http -> sibling autogrouped (5)
  1207. // http
  1208. // http
  1209. // db
  1210. // http
  1211. // http
  1212. // http
  1213. // http
  1214. // http
  1215. const root = new TraceTreeNode(
  1216. null,
  1217. makeTransaction({start_timestamp: 0, timestamp: 10}),
  1218. {
  1219. project_slug: '',
  1220. event_id: '',
  1221. }
  1222. );
  1223. for (let i = 0; i < 10; i++) {
  1224. if (i === 5) {
  1225. root.children.push(
  1226. new TraceTreeNode(
  1227. root,
  1228. makeSpan({start_timestamp: i, timestamp: i + 1, op: 'db'}),
  1229. {
  1230. project_slug: '',
  1231. event_id: '',
  1232. }
  1233. )
  1234. );
  1235. }
  1236. root.children.push(
  1237. new TraceTreeNode(
  1238. root,
  1239. makeSpan({start_timestamp: i, timestamp: i + 1, op: 'http'}),
  1240. {
  1241. project_slug: '',
  1242. event_id: '',
  1243. }
  1244. )
  1245. );
  1246. }
  1247. TraceTree.AutogroupSiblingSpanNodes(root);
  1248. assertAutogroupedNode(root.children[0]);
  1249. expect(root.children).toHaveLength(3);
  1250. assertAutogroupedNode(root.children[2]);
  1251. });
  1252. it('renders children of autogrouped direct children nodes', async () => {
  1253. const tree = TraceTree.FromTrace(
  1254. makeTrace({
  1255. transactions: [
  1256. makeTransaction({
  1257. transaction: '/',
  1258. project_slug: 'project',
  1259. event_id: 'event_id',
  1260. }),
  1261. ],
  1262. })
  1263. );
  1264. MockApiClient.addMockResponse({
  1265. url: '/organizations/org-slug/events/project:event_id/',
  1266. method: 'GET',
  1267. body: makeEvent({}, [
  1268. makeSpan({description: 'parent span', op: 'http', span_id: '1'}),
  1269. makeSpan({description: 'span', op: 'db', span_id: '2', parent_span_id: '1'}),
  1270. makeSpan({description: 'span', op: 'db', span_id: '3', parent_span_id: '2'}),
  1271. makeSpan({description: 'span', op: 'db', span_id: '4', parent_span_id: '3'}),
  1272. makeSpan({description: 'span', op: 'db', span_id: '5', parent_span_id: '4'}),
  1273. makeSpan({
  1274. description: 'span',
  1275. op: 'redis',
  1276. span_id: '6',
  1277. parent_span_id: '5',
  1278. }),
  1279. makeSpan({description: 'span', op: 'https', parent_span_id: '1'}),
  1280. ]),
  1281. });
  1282. expect(tree.list.length).toBe(2);
  1283. tree.zoomIn(tree.list[1], true, {
  1284. api: new MockApiClient(),
  1285. organization: OrganizationFixture(),
  1286. });
  1287. await waitFor(() => {
  1288. expect(tree.list.length).toBe(6);
  1289. });
  1290. const autogroupedNode = tree.list[tree.list.length - 3];
  1291. assertParentAutogroupedNode(autogroupedNode);
  1292. expect('autogrouped_by' in autogroupedNode?.value).toBeTruthy();
  1293. expect(autogroupedNode.groupCount).toBe(4);
  1294. expect(autogroupedNode.head.value.span_id).toBe('2');
  1295. expect(autogroupedNode.tail.value.span_id).toBe('5');
  1296. // Expand autogrouped node
  1297. expect(tree.expand(autogroupedNode, true)).toBe(true);
  1298. expect(tree.list.length).toBe(10);
  1299. // Collapse autogrouped node
  1300. expect(tree.expand(autogroupedNode, false)).toBe(true);
  1301. expect(tree.list.length).toBe(6);
  1302. expect(autogroupedNode.children[0].depth).toBe(4);
  1303. });
  1304. });
  1305. });