traceTree.spec.tsx 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427
  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(root, [
  298. makeSpan({start_timestamp: 0, op: '1', span_id: '1'}),
  299. makeSpan({start_timestamp: 1, op: '2', span_id: '2', parent_span_id: '1'}),
  300. makeSpan({start_timestamp: 2, op: '3', span_id: '3', parent_span_id: '2'}),
  301. makeSpan({start_timestamp: 3, op: '4', span_id: '4', parent_span_id: '1'}),
  302. ]);
  303. if (!isSpanNode(node.children[0])) {
  304. throw new Error('Child needs to be a span');
  305. }
  306. expect(node.children[0].value.span_id).toBe('1');
  307. expect(node.children[0].value.start_timestamp).toBe(0);
  308. expect(node.children.length).toBe(1);
  309. assertSpanNode(node.children[0].children[0]);
  310. assertSpanNode(node.children[0].children[0].children[0]);
  311. assertSpanNode(node.children[0].children[1]);
  312. expect(node.children[0].children[0].value.start_timestamp).toBe(1);
  313. expect(node.children[0].children[0].children[0].value.start_timestamp).toBe(2);
  314. expect(node.children[0].children[1].value.start_timestamp).toBe(3);
  315. });
  316. it('builds from spans and copies txn nodes', () => {
  317. // transaction transaction
  318. // - child transaction -> - span
  319. // - child-transaction
  320. // - span
  321. const root = new TraceTreeNode(
  322. null,
  323. makeTransaction({
  324. children: [],
  325. }),
  326. {project_slug: '', event_id: ''}
  327. );
  328. root.children.push(
  329. new TraceTreeNode(
  330. root,
  331. makeTransaction({
  332. parent_span_id: 'child-transaction',
  333. }),
  334. {project_slug: '', event_id: ''}
  335. )
  336. );
  337. const node = TraceTree.FromSpans(root, [
  338. makeSpan({start_timestamp: 0, timestamp: 0.1, op: 'span', span_id: 'none'}),
  339. makeSpan({
  340. start_timestamp: 0.1,
  341. timestamp: 0.2,
  342. op: 'child-transaction',
  343. span_id: 'child-transaction',
  344. }),
  345. makeSpan({start_timestamp: 0.2, timestamp: 0.25, op: 'span', span_id: 'none'}),
  346. ]);
  347. assertTransactionNode(node.children[1]);
  348. });
  349. it('builds from spans and copies txn nodes to nested children', () => {
  350. // parent transaction parent transaction
  351. // - child transaction -> - span
  352. // - grandchild transaction -> - child-transaction
  353. // - grandchild-transaction
  354. //
  355. const root = new TraceTreeNode(
  356. null,
  357. makeTransaction({
  358. span_id: 'parent-transaction',
  359. children: [],
  360. }),
  361. {project_slug: '', event_id: ''}
  362. );
  363. let start: TraceTreeNode<TraceTree.NodeValue> = root;
  364. for (let i = 0; i < 2; i++) {
  365. const node = new TraceTreeNode(
  366. start,
  367. makeTransaction({
  368. transaction: `${i === 0 ? 'child' : 'grandchild'}-transaction`,
  369. parent_span_id: `${i === 0 ? 'child' : 'grandchild'}-transaction`,
  370. }),
  371. {project_slug: '', event_id: ''}
  372. );
  373. start.children.push(node);
  374. start = node;
  375. }
  376. const node = TraceTree.FromSpans(root, [
  377. makeSpan({start_timestamp: 0, timestamp: 0.1, op: 'span', span_id: 'none'}),
  378. makeSpan({
  379. start_timestamp: 0.1,
  380. timestamp: 0.2,
  381. op: 'child-transaction',
  382. span_id: 'child-transaction',
  383. }),
  384. ]);
  385. assertTransactionNode(node.children[1]);
  386. assertTransactionNode(node.children[1].children[0]);
  387. });
  388. it('injects missing spans', () => {
  389. const root = new TraceTreeNode(
  390. null,
  391. makeTransaction({
  392. children: [],
  393. }),
  394. {project_slug: '', event_id: ''}
  395. );
  396. const date = new Date().getTime();
  397. const node = TraceTree.FromSpans(root, [
  398. makeSpan({
  399. start_timestamp: date,
  400. timestamp: date + 1,
  401. span_id: '1',
  402. op: 'span 1',
  403. }),
  404. makeSpan({
  405. start_timestamp: date + 2,
  406. timestamp: date + 4,
  407. op: 'span 2',
  408. span_id: '2',
  409. }),
  410. ]);
  411. assertSpanNode(node.children[0]);
  412. assertMissingInstrumentationNode(node.children[1]);
  413. assertSpanNode(node.children[2]);
  414. expect(node.children.length).toBe(3);
  415. expect(node.children[0].value.op).toBe('span 1');
  416. expect(node.children[1].value.type).toBe('missing_instrumentation');
  417. expect(node.children[2].value.op).toBe('span 2');
  418. });
  419. it('builds and preserves list order', async () => {
  420. const organization = OrganizationFixture();
  421. const api = new MockApiClient();
  422. const tree = TraceTree.FromTrace(
  423. makeTrace({
  424. transactions: [
  425. makeTransaction({
  426. transaction: 'txn 1',
  427. start_timestamp: 0,
  428. children: [makeTransaction({start_timestamp: 1, transaction: 'txn 2'})],
  429. }),
  430. ],
  431. })
  432. );
  433. tree.expand(tree.list[0], true);
  434. const node = tree.list[1];
  435. const request = MockApiClient.addMockResponse({
  436. url: '/organizations/org-slug/events/undefined:undefined/',
  437. method: 'GET',
  438. body: makeEvent({startTimestamp: 0}, [
  439. makeSpan({start_timestamp: 1, op: 'span 1', span_id: '1'}),
  440. makeSpan({
  441. start_timestamp: 2,
  442. op: 'span 2',
  443. span_id: '2',
  444. parent_span_id: '1',
  445. }),
  446. makeSpan({start_timestamp: 3, op: 'span 3', parent_span_id: '2'}),
  447. makeSpan({start_timestamp: 4, op: 'span 4', parent_span_id: '1'}),
  448. ]),
  449. });
  450. // 0
  451. // 1
  452. // 2
  453. // 3
  454. // 4
  455. tree.zoomIn(node, true, {api, organization});
  456. await waitFor(() => {
  457. expect(node.zoomedIn).toBe(true);
  458. });
  459. expect(request).toHaveBeenCalled();
  460. expect(tree.list.length).toBe(6);
  461. assertTransactionNode(tree.list[1]);
  462. assertSpanNode(tree.list[2]);
  463. assertSpanNode(tree.list[3]);
  464. expect(tree.list[1].value.start_timestamp).toBe(0);
  465. expect(tree.list[2].value.start_timestamp).toBe(1);
  466. expect(tree.list[3].value.start_timestamp).toBe(2);
  467. });
  468. it('preserves input order', () => {
  469. const firstChild = makeTransaction({
  470. start_timestamp: 0,
  471. timestamp: 1,
  472. children: [],
  473. });
  474. const secondChild = makeTransaction({
  475. start_timestamp: 1,
  476. timestamp: 2,
  477. children: [],
  478. });
  479. const tree = TraceTree.FromTrace(
  480. makeTrace({
  481. transactions: [
  482. makeTransaction({
  483. start_timestamp: 0,
  484. timestamp: 2,
  485. children: [firstChild, secondChild],
  486. }),
  487. makeTransaction({
  488. start_timestamp: 2,
  489. timestamp: 4,
  490. }),
  491. ],
  492. })
  493. );
  494. expect(tree.list).toHaveLength(5);
  495. expect(tree.expand(tree.list[1], false)).toBe(true);
  496. expect(tree.list).toHaveLength(3);
  497. expect(tree.expand(tree.list[1], true)).toBe(true);
  498. expect(tree.list).toHaveLength(5);
  499. expect(tree.list[2].value).toBe(firstChild);
  500. expect(tree.list[3].value).toBe(secondChild);
  501. });
  502. it('creates children -> parent references', () => {
  503. const tree = TraceTree.FromTrace(
  504. makeTrace({
  505. transactions: [
  506. makeTransaction({
  507. start_timestamp: 0,
  508. timestamp: 2,
  509. children: [makeTransaction({start_timestamp: 1, timestamp: 2})],
  510. }),
  511. makeTransaction({
  512. start_timestamp: 2,
  513. timestamp: 4,
  514. }),
  515. ],
  516. })
  517. );
  518. expect(tree.list).toHaveLength(4);
  519. expect(tree.list[2].parent?.value).toBe(tree.list[1].value);
  520. });
  521. it('establishes parent-child relationships', () => {
  522. const tree = TraceTree.FromTrace(
  523. makeTrace({
  524. transactions: [
  525. makeTransaction({
  526. children: [makeTransaction()],
  527. }),
  528. ],
  529. })
  530. );
  531. expect(tree.root.children).toHaveLength(1);
  532. expect(tree.root.children[0].children).toHaveLength(1);
  533. });
  534. it('isLastChild', () => {
  535. const tree = TraceTree.FromTrace(
  536. makeTrace({
  537. transactions: [
  538. makeTransaction({
  539. children: [makeTransaction(), makeTransaction()],
  540. }),
  541. makeTransaction(),
  542. ],
  543. orphan_errors: [],
  544. })
  545. );
  546. tree.expand(tree.list[1], true);
  547. expect(tree.list[0].isLastChild).toBe(true);
  548. expect(tree.list[1].isLastChild).toBe(false);
  549. expect(tree.list[2].isLastChild).toBe(false);
  550. expect(tree.list[3].isLastChild).toBe(true);
  551. expect(tree.list[4].isLastChild).toBe(true);
  552. });
  553. describe('connectors', () => {
  554. it('computes transaction connectors', () => {
  555. const tree = TraceTree.FromTrace(
  556. makeTrace({
  557. transactions: [
  558. makeTransaction({
  559. transaction: 'sibling',
  560. children: [
  561. makeTransaction({transaction: 'child'}),
  562. makeTransaction({transaction: 'child'}),
  563. ],
  564. }),
  565. makeTransaction({transaction: 'sibling'}),
  566. ],
  567. })
  568. );
  569. // -1 root
  570. // ------ list begins here
  571. // 0 transaction
  572. // 0 |- sibling
  573. // -1, 2| | - child
  574. // -1| | - child
  575. // 0 |- sibling
  576. tree.expand(tree.list[1], true);
  577. expect(tree.list.length).toBe(5);
  578. expect(tree.list[0].connectors.length).toBe(0);
  579. expect(tree.list[1].connectors.length).toBe(1);
  580. expect(tree.list[1].connectors[0]).toBe(-1);
  581. expect(tree.list[2].connectors[0]).toBe(-1);
  582. expect(tree.list[2].connectors[1]).toBe(2);
  583. expect(tree.list[2].connectors.length).toBe(2);
  584. expect(tree.list[3].connectors[0]).toBe(-1);
  585. expect(tree.list[3].connectors.length).toBe(1);
  586. expect(tree.list[4].connectors.length).toBe(0);
  587. });
  588. it('computes span connectors', async () => {
  589. const tree = TraceTree.FromTrace(
  590. makeTrace({
  591. transactions: [
  592. makeTransaction({
  593. project_slug: 'project',
  594. event_id: 'event_id',
  595. transaction: 'transaction',
  596. children: [],
  597. }),
  598. ],
  599. })
  600. );
  601. // root
  602. // |- node1 []
  603. // |- node2 []
  604. MockApiClient.addMockResponse({
  605. url: '/organizations/org-slug/events/project:event_id/',
  606. method: 'GET',
  607. body: makeEvent({}, [makeSpan({start_timestamp: 0, op: 'span', span_id: '1'})]),
  608. });
  609. expect(tree.list.length).toBe(2);
  610. tree.zoomIn(tree.list[1], true, {
  611. api: new MockApiClient(),
  612. organization: OrganizationFixture(),
  613. });
  614. await waitFor(() => {
  615. expect(tree.list.length).toBe(3);
  616. });
  617. // root
  618. // |- node1 []
  619. // |- node2 []
  620. // |- span1 []
  621. const span = tree.list[tree.list.length - 1];
  622. expect(span.connectors.length).toBe(0);
  623. });
  624. });
  625. describe('expanding', () => {
  626. it('expands a node and updates the list', () => {
  627. const tree = TraceTree.FromTrace(
  628. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  629. );
  630. const node = tree.list[1];
  631. expect(tree.expand(node, false)).toBe(true);
  632. expect(tree.list.length).toBe(2);
  633. expect(node.expanded).toBe(false);
  634. expect(tree.expand(node, true)).toBe(true);
  635. expect(node.expanded).toBe(true);
  636. // Assert that the list has been updated
  637. expect(tree.list).toHaveLength(3);
  638. expect(tree.list[2]).toBe(node.children[0]);
  639. });
  640. it('collapses a node and updates the list', () => {
  641. const tree = TraceTree.FromTrace(
  642. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  643. );
  644. const node = tree.list[1];
  645. tree.expand(node, true);
  646. expect(tree.list.length).toBe(3);
  647. expect(tree.expand(node, false)).toBe(true);
  648. expect(node.expanded).toBe(false);
  649. // Assert that the list has been updated
  650. expect(tree.list).toHaveLength(2);
  651. expect(tree.list[1]).toBe(node);
  652. });
  653. it('preserves children expanded state', () => {
  654. const tree = TraceTree.FromTrace(
  655. makeTrace({
  656. transactions: [
  657. makeTransaction({
  658. children: [
  659. makeTransaction({children: [makeTransaction({start_timestamp: 1000})]}),
  660. makeTransaction({start_timestamp: 5}),
  661. ],
  662. }),
  663. ],
  664. })
  665. );
  666. expect(tree.expand(tree.list[2], false)).toBe(true);
  667. // Assert that the list has been updated
  668. expect(tree.list).toHaveLength(4);
  669. expect(tree.expand(tree.list[2], true)).toBe(true);
  670. expect(tree.list.length).toBe(5);
  671. expect(tree.list[tree.list.length - 1].value).toEqual(
  672. makeTransaction({start_timestamp: 5})
  673. );
  674. });
  675. it('expanding or collapsing a zoomed in node doesnt do anything', async () => {
  676. const organization = OrganizationFixture();
  677. const api = new MockApiClient();
  678. const tree = TraceTree.FromTrace(
  679. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  680. );
  681. const node = tree.list[0];
  682. const request = MockApiClient.addMockResponse({
  683. url: '/organizations/org-slug/events/undefined:undefined/',
  684. method: 'GET',
  685. body: makeEvent(),
  686. });
  687. tree.zoomIn(node, true, {api, organization});
  688. await waitFor(() => {
  689. expect(node.zoomedIn).toBe(true);
  690. });
  691. expect(request).toHaveBeenCalled();
  692. expect(tree.expand(node, true)).toBe(false);
  693. });
  694. });
  695. describe('zooming', () => {
  696. it('marks node as zoomed in', async () => {
  697. const organization = OrganizationFixture();
  698. const api = new MockApiClient();
  699. const tree = TraceTree.FromTrace(
  700. makeTrace({
  701. transactions: [
  702. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  703. ],
  704. })
  705. );
  706. const request = MockApiClient.addMockResponse({
  707. url: '/organizations/org-slug/events/project:event_id/',
  708. method: 'GET',
  709. body: makeEvent(),
  710. });
  711. const node = tree.list[1];
  712. expect(node.zoomedIn).toBe(false);
  713. tree.zoomIn(node, true, {api, organization});
  714. await waitFor(() => {
  715. expect(node.zoomedIn).toBe(true);
  716. });
  717. expect(request).toHaveBeenCalled();
  718. });
  719. it('fetches spans for node when zooming in', async () => {
  720. const tree = TraceTree.FromTrace(
  721. makeTrace({
  722. transactions: [
  723. makeTransaction({
  724. transaction: 'txn',
  725. project_slug: 'project',
  726. event_id: 'event_id',
  727. }),
  728. ],
  729. })
  730. );
  731. const request = MockApiClient.addMockResponse({
  732. url: '/organizations/org-slug/events/project:event_id/',
  733. method: 'GET',
  734. body: makeEvent({}, [makeSpan()]),
  735. });
  736. const node = tree.list[1];
  737. expect(node.children).toHaveLength(0);
  738. tree.zoomIn(node, true, {
  739. api: new MockApiClient(),
  740. organization: OrganizationFixture(),
  741. });
  742. expect(request).toHaveBeenCalled();
  743. await waitFor(() => {
  744. expect(node.children).toHaveLength(1);
  745. });
  746. // Assert that the children have been updated
  747. assertTransactionNode(node.children[0].parent);
  748. expect(node.children[0].parent.value.transaction).toBe('txn');
  749. expect(node.children[0].depth).toBe(node.depth + 1);
  750. });
  751. it('zooms out', async () => {
  752. const tree = TraceTree.FromTrace(
  753. makeTrace({
  754. transactions: [
  755. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  756. ],
  757. })
  758. );
  759. MockApiClient.addMockResponse({
  760. url: '/organizations/org-slug/events/project:event_id/',
  761. method: 'GET',
  762. body: makeEvent({}, [makeSpan({span_id: 'span1', description: 'span1'})]),
  763. });
  764. tree.zoomIn(tree.list[1], true, {
  765. api: new MockApiClient(),
  766. organization: OrganizationFixture(),
  767. });
  768. await waitFor(() => {
  769. assertSpanNode(tree.list[1].children[0]);
  770. expect(tree.list[1].children[0].value.description).toBe('span1');
  771. });
  772. tree.zoomIn(tree.list[1], false, {
  773. api: new MockApiClient(),
  774. organization: OrganizationFixture(),
  775. });
  776. await waitFor(() => {
  777. // Assert child no longer points to children
  778. expect(tree.list[1].zoomedIn).toBe(false);
  779. expect(tree.list[1].children[0]).toBe(undefined);
  780. expect(tree.list[2]).toBe(undefined);
  781. });
  782. });
  783. it('zooms in and out', async () => {
  784. const tree = TraceTree.FromTrace(
  785. makeTrace({
  786. transactions: [
  787. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  788. ],
  789. })
  790. );
  791. MockApiClient.addMockResponse({
  792. url: '/organizations/org-slug/events/project:event_id/',
  793. method: 'GET',
  794. body: makeEvent({}, [makeSpan({span_id: 'span 1', description: 'span1'})]),
  795. });
  796. // Zoom in
  797. tree.zoomIn(tree.list[1], true, {
  798. api: new MockApiClient(),
  799. organization: OrganizationFixture(),
  800. });
  801. await waitFor(() => {
  802. assertSpanNode(tree.list[1].children[0]);
  803. expect(tree.list[1].children[0].value.description).toBe('span1');
  804. });
  805. // Zoom out
  806. tree.zoomIn(tree.list[1], false, {
  807. api: new MockApiClient(),
  808. organization: OrganizationFixture(),
  809. });
  810. await waitFor(() => {
  811. expect(tree.list[2]).toBe(undefined);
  812. });
  813. // Zoom in
  814. tree.zoomIn(tree.list[1], true, {
  815. api: new MockApiClient(),
  816. organization: OrganizationFixture(),
  817. });
  818. await waitFor(() => {
  819. assertSpanNode(tree.list[1].children[0]);
  820. expect(tree.list[1].children[0].value?.description).toBe('span1');
  821. });
  822. });
  823. it('zooms in and out preserving siblings', async () => {
  824. const tree = TraceTree.FromTrace(
  825. makeTrace({
  826. transactions: [
  827. makeTransaction({
  828. project_slug: 'project',
  829. event_id: 'event_id',
  830. start_timestamp: 0,
  831. children: [
  832. makeTransaction({
  833. start_timestamp: 1,
  834. timestamp: 2,
  835. project_slug: 'other_project',
  836. event_id: 'event_id',
  837. }),
  838. makeTransaction({start_timestamp: 2, timestamp: 3}),
  839. ],
  840. }),
  841. ],
  842. })
  843. );
  844. const request = MockApiClient.addMockResponse({
  845. url: '/organizations/org-slug/events/other_project:event_id/',
  846. method: 'GET',
  847. body: makeEvent({}, [makeSpan({description: 'span1'})]),
  848. });
  849. tree.expand(tree.list[1], true);
  850. tree.zoomIn(tree.list[2], true, {
  851. api: new MockApiClient(),
  852. organization: OrganizationFixture(),
  853. });
  854. expect(request).toHaveBeenCalled();
  855. // Zoom in
  856. await waitFor(() => {
  857. expect(tree.list.length).toBe(5);
  858. });
  859. // Zoom out
  860. tree.zoomIn(tree.list[2], false, {
  861. api: new MockApiClient(),
  862. organization: OrganizationFixture(),
  863. });
  864. await waitFor(() => {
  865. expect(tree.list.length).toBe(4);
  866. });
  867. });
  868. it('preserves expanded state when zooming in and out', async () => {
  869. const tree = TraceTree.FromTrace(
  870. makeTrace({
  871. transactions: [
  872. makeTransaction({
  873. project_slug: 'project',
  874. event_id: 'event_id',
  875. children: [
  876. makeTransaction({project_slug: 'other_project', event_id: 'event_id'}),
  877. ],
  878. }),
  879. ],
  880. })
  881. );
  882. MockApiClient.addMockResponse({
  883. url: '/organizations/org-slug/events/project:event_id/',
  884. method: 'GET',
  885. body: makeEvent({}, [
  886. makeSpan({description: 'span1'}),
  887. makeSpan({description: 'span2'}),
  888. ]),
  889. });
  890. tree.expand(tree.list[1], true);
  891. expect(tree.list.length).toBe(3);
  892. tree.zoomIn(tree.list[1], true, {
  893. api: new MockApiClient(),
  894. organization: OrganizationFixture(),
  895. });
  896. await waitFor(() => {
  897. expect(tree.list.length).toBe(4);
  898. });
  899. tree.zoomIn(tree.list[1], false, {
  900. api: new MockApiClient(),
  901. organization: OrganizationFixture(),
  902. });
  903. await waitFor(() => {
  904. expect(tree.list.length).toBe(3);
  905. });
  906. expect(tree.list[1].expanded).toBe(true);
  907. });
  908. });
  909. describe('autogrouping', () => {
  910. it('auto groups sibling spans and preserves tail spans', () => {
  911. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  912. project_slug: '',
  913. event_id: '',
  914. });
  915. for (let i = 0; i < 5; i++) {
  916. root.children.push(
  917. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  918. project_slug: '',
  919. event_id: '',
  920. })
  921. );
  922. }
  923. root.children.push(
  924. new TraceTreeNode(root, makeSpan({description: 'span', op: 'http'}), {
  925. project_slug: '',
  926. event_id: '',
  927. })
  928. );
  929. expect(root.children.length).toBe(6);
  930. TraceTree.AutogroupSiblingSpanNodes(root);
  931. expect(root.children.length).toBe(2);
  932. });
  933. it('autogroups when number of children is exactly 5', () => {
  934. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  935. project_slug: '',
  936. event_id: '',
  937. });
  938. for (let i = 0; i < 5; i++) {
  939. root.children.push(
  940. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  941. project_slug: '',
  942. event_id: '',
  943. })
  944. );
  945. }
  946. expect(root.children.length).toBe(5);
  947. TraceTree.AutogroupSiblingSpanNodes(root);
  948. expect(root.children.length).toBe(1);
  949. });
  950. it('autogroups when number of children is > 5', () => {
  951. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  952. project_slug: '',
  953. event_id: '',
  954. });
  955. for (let i = 0; i < 7; i++) {
  956. root.children.push(
  957. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  958. project_slug: '',
  959. event_id: '',
  960. })
  961. );
  962. }
  963. expect(root.children.length).toBe(7);
  964. TraceTree.AutogroupSiblingSpanNodes(root);
  965. expect(root.children.length).toBe(1);
  966. });
  967. it('autogroups direct children case', () => {
  968. // db db db
  969. // http -> parent autogroup (3) -> parent autogroup (3)
  970. // http http
  971. // http http
  972. // http
  973. const root: TraceTreeNode<TraceTree.Span> = new TraceTreeNode(
  974. null,
  975. makeSpan({
  976. description: `span1`,
  977. span_id: `1`,
  978. op: 'db',
  979. }),
  980. {project_slug: '', event_id: ''}
  981. );
  982. let last: TraceTreeNode<any> = root;
  983. for (let i = 0; i < 3; i++) {
  984. const node = new TraceTreeNode(
  985. last,
  986. makeSpan({
  987. description: `span${i}`,
  988. span_id: `${i}`,
  989. op: 'http',
  990. }),
  991. {
  992. project_slug: '',
  993. event_id: '',
  994. }
  995. );
  996. last.children.push(node);
  997. last = node;
  998. }
  999. if (!root) {
  1000. throw new Error('root is null');
  1001. }
  1002. expect(root.children.length).toBe(1);
  1003. expect(root.children[0].children.length).toBe(1);
  1004. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1005. expect(root.children.length).toBe(1);
  1006. assertAutogroupedNode(root.children[0]);
  1007. expect(root.children[0].children.length).toBe(0);
  1008. root.children[0].expanded = true;
  1009. expect((root.children[0].children[0].value as RawSpanType).description).toBe(
  1010. 'span0'
  1011. );
  1012. });
  1013. it('autogrouping direct children skips rendering intermediary nodes', () => {
  1014. // db db db
  1015. // http autogrouped (3) autogrouped (3)
  1016. // http -> db -> http
  1017. // http http
  1018. // db http
  1019. // db
  1020. const root = new TraceTreeNode(
  1021. null,
  1022. makeSpan({span_id: 'span1', description: 'span1', op: 'db'}),
  1023. {
  1024. project_slug: '',
  1025. event_id: '',
  1026. }
  1027. );
  1028. let last = root;
  1029. for (let i = 0; i < 4; i++) {
  1030. const node = new TraceTreeNode(
  1031. last,
  1032. makeSpan({
  1033. span_id: `span`,
  1034. description: `span`,
  1035. op: i === 3 ? 'db' : 'http',
  1036. }),
  1037. {
  1038. project_slug: '',
  1039. event_id: '',
  1040. }
  1041. );
  1042. last.children.push(node);
  1043. last = node;
  1044. }
  1045. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1046. const autoGroupedNode = root.children[0];
  1047. assertAutogroupedNode(autoGroupedNode);
  1048. expect(autoGroupedNode.children.length).toBe(1);
  1049. expect((autoGroupedNode.children[0].value as RawSpanType).op).toBe('db');
  1050. autoGroupedNode.expanded = true;
  1051. expect(autoGroupedNode.children.length).toBe(1);
  1052. expect((autoGroupedNode.children[0].value as RawSpanType).op).toBe('http');
  1053. });
  1054. it('nested direct autogrouping', () => {
  1055. // db db db
  1056. // http -> parent autogroup (3) -> parent autogroup (3)
  1057. // http db http
  1058. // http parent autogroup (3) http
  1059. // db http
  1060. // http db
  1061. // http parent autogrouped (3)
  1062. // http http
  1063. // http
  1064. // http
  1065. const root = new TraceTreeNode(
  1066. null,
  1067. makeSpan({span_id: 'span', description: 'span', op: 'db'}),
  1068. {
  1069. project_slug: '',
  1070. event_id: '',
  1071. }
  1072. );
  1073. let last = root;
  1074. for (let i = 0; i < 3; i++) {
  1075. if (i === 1) {
  1076. const autogroupBreakingSpan = new TraceTreeNode(
  1077. last,
  1078. makeSpan({span_id: 'span', description: 'span', op: 'db'}),
  1079. {
  1080. project_slug: '',
  1081. event_id: '',
  1082. }
  1083. );
  1084. last.children.push(autogroupBreakingSpan);
  1085. last = autogroupBreakingSpan;
  1086. } else {
  1087. for (let j = 0; j < 3; j++) {
  1088. const node = new TraceTreeNode(
  1089. last,
  1090. makeSpan({span_id: `span${j}`, description: `span${j}`, op: 'http'}),
  1091. {
  1092. project_slug: '',
  1093. event_id: '',
  1094. }
  1095. );
  1096. last.children.push(node);
  1097. last = node;
  1098. }
  1099. }
  1100. }
  1101. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1102. assertAutogroupedNode(root.children[0]);
  1103. assertAutogroupedNode(root.children[0].children[0].children[0]);
  1104. });
  1105. it('sibling autogrouping', () => {
  1106. // db db
  1107. // http sibling autogrouped (5)
  1108. // http
  1109. // http ->
  1110. // http
  1111. // http
  1112. const root = new TraceTreeNode(
  1113. null,
  1114. makeTransaction({start_timestamp: 0, timestamp: 10}),
  1115. {
  1116. project_slug: '',
  1117. event_id: '',
  1118. }
  1119. );
  1120. for (let i = 0; i < 5; i++) {
  1121. root.children.push(
  1122. new TraceTreeNode(root, makeSpan({start_timestamp: i, timestamp: i + 1}), {
  1123. project_slug: '',
  1124. event_id: '',
  1125. })
  1126. );
  1127. }
  1128. TraceTree.AutogroupSiblingSpanNodes(root);
  1129. expect(root.children.length).toBe(1);
  1130. assertAutogroupedNode(root.children[0]);
  1131. });
  1132. it('multiple sibling autogrouping', () => {
  1133. // db db
  1134. // http sibling autogrouped (5)
  1135. // http db
  1136. // http -> sibling autogrouped (5)
  1137. // http
  1138. // http
  1139. // db
  1140. // http
  1141. // http
  1142. // http
  1143. // http
  1144. // http
  1145. const root = new TraceTreeNode(
  1146. null,
  1147. makeTransaction({start_timestamp: 0, timestamp: 10}),
  1148. {
  1149. project_slug: '',
  1150. event_id: '',
  1151. }
  1152. );
  1153. for (let i = 0; i < 10; i++) {
  1154. if (i === 5) {
  1155. root.children.push(
  1156. new TraceTreeNode(
  1157. root,
  1158. makeSpan({start_timestamp: i, timestamp: i + 1, op: 'db'}),
  1159. {
  1160. project_slug: '',
  1161. event_id: '',
  1162. }
  1163. )
  1164. );
  1165. }
  1166. root.children.push(
  1167. new TraceTreeNode(
  1168. root,
  1169. makeSpan({start_timestamp: i, timestamp: i + 1, op: 'http'}),
  1170. {
  1171. project_slug: '',
  1172. event_id: '',
  1173. }
  1174. )
  1175. );
  1176. }
  1177. TraceTree.AutogroupSiblingSpanNodes(root);
  1178. assertAutogroupedNode(root.children[0]);
  1179. expect(root.children).toHaveLength(3);
  1180. assertAutogroupedNode(root.children[2]);
  1181. });
  1182. it('renders children of autogrouped direct children nodes', async () => {
  1183. const tree = TraceTree.FromTrace(
  1184. makeTrace({
  1185. transactions: [
  1186. makeTransaction({
  1187. transaction: '/',
  1188. project_slug: 'project',
  1189. event_id: 'event_id',
  1190. }),
  1191. ],
  1192. })
  1193. );
  1194. MockApiClient.addMockResponse({
  1195. url: '/organizations/org-slug/events/project:event_id/',
  1196. method: 'GET',
  1197. body: makeEvent({}, [
  1198. makeSpan({description: 'parent span', op: 'http', span_id: '1'}),
  1199. makeSpan({description: 'span', op: 'db', span_id: '2', parent_span_id: '1'}),
  1200. makeSpan({description: 'span', op: 'db', span_id: '3', parent_span_id: '2'}),
  1201. makeSpan({description: 'span', op: 'db', span_id: '4', parent_span_id: '3'}),
  1202. makeSpan({description: 'span', op: 'db', span_id: '5', parent_span_id: '4'}),
  1203. makeSpan({
  1204. description: 'span',
  1205. op: 'redis',
  1206. span_id: '6',
  1207. parent_span_id: '5',
  1208. }),
  1209. makeSpan({description: 'span', op: 'https', parent_span_id: '1'}),
  1210. ]),
  1211. });
  1212. expect(tree.list.length).toBe(2);
  1213. tree.zoomIn(tree.list[1], true, {
  1214. api: new MockApiClient(),
  1215. organization: OrganizationFixture(),
  1216. });
  1217. await waitFor(() => {
  1218. expect(tree.list.length).toBe(6);
  1219. });
  1220. const autogroupedNode = tree.list[tree.list.length - 3];
  1221. assertParentAutogroupedNode(autogroupedNode);
  1222. expect('autogrouped_by' in autogroupedNode?.value).toBeTruthy();
  1223. expect(autogroupedNode.groupCount).toBe(4);
  1224. expect(autogroupedNode.head.value.span_id).toBe('2');
  1225. expect(autogroupedNode.tail.value.span_id).toBe('5');
  1226. // Expand autogrouped node
  1227. expect(tree.expand(autogroupedNode, true)).toBe(true);
  1228. expect(tree.list.length).toBe(10);
  1229. // Collapse autogrouped node
  1230. expect(tree.expand(autogroupedNode, false)).toBe(true);
  1231. expect(tree.list.length).toBe(6);
  1232. expect(autogroupedNode.children[0].depth).toBe(4);
  1233. });
  1234. });
  1235. });