traceTree.spec.tsx 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252
  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, type EventTransaction} 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. isTraceErrorNode,
  15. isTransactionNode,
  16. } from './guards';
  17. import {
  18. ParentAutogroupNode,
  19. type SiblingAutogroupNode,
  20. TraceTree,
  21. TraceTreeNode,
  22. } from './traceTree';
  23. function makeTrace(
  24. overrides: Partial<TraceSplitResults<TraceFullDetailed>>
  25. ): TraceSplitResults<TraceFullDetailed> {
  26. return {
  27. transactions: [],
  28. orphan_errors: [],
  29. ...overrides,
  30. } as TraceSplitResults<TraceFullDetailed>;
  31. }
  32. function makeTransaction(overrides: Partial<TraceFullDetailed> = {}): TraceFullDetailed {
  33. return {
  34. children: [],
  35. start_timestamp: 0,
  36. timestamp: 1,
  37. transaction: 'transaction',
  38. 'transaction.op': '',
  39. 'transaction.status': '',
  40. performance_issues: [],
  41. errors: [],
  42. ...overrides,
  43. } as TraceFullDetailed;
  44. }
  45. function makeSpan(overrides: Partial<RawSpanType> = {}): TraceTree.Span {
  46. return {
  47. op: '',
  48. description: '',
  49. span_id: '',
  50. start_timestamp: 0,
  51. timestamp: 10,
  52. event: makeEvent(),
  53. errors: [],
  54. performance_issues: [],
  55. childTransaction: undefined,
  56. ...overrides,
  57. } as TraceTree.Span;
  58. }
  59. function makeTraceError(
  60. overrides: Partial<TraceTree.TraceError> = {}
  61. ): TraceTree.TraceError {
  62. return {
  63. title: 'MaybeEncodingError: Error sending result',
  64. level: 'error',
  65. event_type: 'error',
  66. data: {},
  67. ...overrides,
  68. } as TraceTree.TraceError;
  69. }
  70. function makeEvent(overrides: Partial<Event> = {}, spans: RawSpanType[] = []): Event {
  71. return {
  72. entries: [{type: EntryType.SPANS, data: spans}],
  73. ...overrides,
  74. } as Event;
  75. }
  76. function assertSpanNode(
  77. node: TraceTreeNode<TraceTree.NodeValue>
  78. ): asserts node is TraceTreeNode<TraceTree.Span> {
  79. if (!isSpanNode(node)) {
  80. throw new Error('node is not a span');
  81. }
  82. }
  83. // function assertTraceNode(
  84. // node: TraceTreeNode<TraceTree.NodeValue>
  85. // ): asserts node is TraceTreeNode<TraceTree.Trace> {
  86. // if (!isTraceNode(node)) {
  87. // throw new Error('node is not a trace');
  88. // }
  89. // }
  90. function assertTransactionNode(
  91. node: TraceTreeNode<TraceTree.NodeValue> | null
  92. ): asserts node is TraceTreeNode<TraceTree.Transaction> {
  93. if (!node || !isTransactionNode(node)) {
  94. throw new Error('node is not a transaction');
  95. }
  96. }
  97. function assertMissingInstrumentationNode(
  98. node: TraceTreeNode<TraceTree.NodeValue>
  99. ): asserts node is TraceTreeNode<TraceTree.MissingInstrumentationSpan> {
  100. if (!isMissingInstrumentationNode(node)) {
  101. throw new Error('node is not a missing instrumentation node');
  102. }
  103. }
  104. function assertTraceErrorNode(
  105. node: TraceTreeNode<TraceTree.NodeValue>
  106. ): asserts node is TraceTreeNode<TraceTree.TraceError> {
  107. if (!isTraceErrorNode(node)) {
  108. throw new Error('node is not a trace error node');
  109. }
  110. }
  111. function assertAutogroupedNode(
  112. node: TraceTreeNode<TraceTree.NodeValue>
  113. ): asserts node is ParentAutogroupNode | SiblingAutogroupNode {
  114. if (!isAutogroupedNode(node)) {
  115. throw new Error('node is not a autogrouped node');
  116. }
  117. }
  118. function assertParentAutogroupedNode(
  119. node: TraceTreeNode<TraceTree.NodeValue>
  120. ): asserts node is ParentAutogroupNode {
  121. if (!(node instanceof ParentAutogroupNode)) {
  122. throw new Error('node is not a parent autogrouped node');
  123. }
  124. }
  125. // function _assertSiblingAutogroupedNode(
  126. // node: TraceTreeNode<TraceTree.NodeValue>
  127. // ): asserts node is ParentAutogroupNode {
  128. // if (!(node instanceof SiblingAutogroupNode)) {
  129. // throw new Error('node is not a parent node');
  130. // }
  131. // }
  132. describe('TreeNode', () => {
  133. it('expands transaction nodes by default', () => {
  134. const node = new TraceTreeNode(null, makeTransaction(), {
  135. project_slug: '',
  136. event_id: '',
  137. });
  138. expect(node.expanded).toBe(true);
  139. });
  140. it('points parent to node', () => {
  141. const root = new TraceTreeNode(null, makeTransaction(), {
  142. project_slug: '',
  143. event_id: '',
  144. });
  145. const child = new TraceTreeNode(root, makeTransaction(), {
  146. project_slug: '',
  147. event_id: '',
  148. });
  149. expect(child.parent).toBe(root);
  150. });
  151. it('depth', () => {
  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. const grandChild = new TraceTreeNode(child, makeTransaction(), {
  161. project_slug: '',
  162. event_id: '',
  163. });
  164. expect(grandChild.depth).toBe(1);
  165. });
  166. it('getVisibleChildren', () => {
  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.getVisibleChildren()).toHaveLength(1);
  177. expect(root.getVisibleChildren()[0]).toBe(child);
  178. root.expanded = false;
  179. expect(root.getVisibleChildren()).toHaveLength(0);
  180. });
  181. it('getVisibleChildrenCount', () => {
  182. const root = new TraceTreeNode(null, makeTransaction(), {
  183. project_slug: '',
  184. event_id: '',
  185. });
  186. const child = new TraceTreeNode(root, makeTransaction(), {
  187. project_slug: '',
  188. event_id: '',
  189. });
  190. root.children.push(child);
  191. expect(root.getVisibleChildrenCount()).toBe(1);
  192. root.expanded = false;
  193. expect(root.getVisibleChildrenCount()).toBe(0);
  194. });
  195. describe('indicators', () => {
  196. it('collects indicator', () => {
  197. const tree = TraceTree.FromTrace(
  198. makeTrace({
  199. transactions: [
  200. makeTransaction({
  201. start_timestamp: 0,
  202. timestamp: 1,
  203. }),
  204. ],
  205. }),
  206. {
  207. measurements: {ttfb: {value: 0, unit: 'millisecond'}},
  208. } as unknown as EventTransaction
  209. );
  210. expect(tree.indicators.length).toBe(1);
  211. expect(tree.indicators[0].start).toBe(0);
  212. });
  213. it('converts timestamp to milliseconds', () => {
  214. const tree = TraceTree.FromTrace(
  215. makeTrace({
  216. transactions: [
  217. makeTransaction({
  218. start_timestamp: 0,
  219. timestamp: 1,
  220. }),
  221. ],
  222. }),
  223. {
  224. measurements: {
  225. ttfb: {value: 500, unit: 'millisecond'},
  226. fcp: {value: 0.5, unit: 'second'},
  227. lcp: {value: 500_000_000, unit: 'nanosecond'},
  228. },
  229. } as unknown as EventTransaction
  230. );
  231. expect(tree.indicators[0].start).toBe(500);
  232. expect(tree.indicators[1].start).toBe(500);
  233. expect(tree.indicators[2].start).toBe(500);
  234. });
  235. it('extends end timestamp to include measurement', () => {
  236. const tree = TraceTree.FromTrace(
  237. makeTrace({
  238. transactions: [
  239. makeTransaction({
  240. start_timestamp: 0,
  241. timestamp: 1,
  242. }),
  243. ],
  244. }),
  245. {
  246. measurements: {
  247. ttfb: {value: 2, unit: 'second'},
  248. },
  249. } as unknown as EventTransaction
  250. );
  251. expect(tree.root.space).toEqual([0, 2000]);
  252. });
  253. it('adjusts end and converst timestamp to ms', () => {
  254. const tree = TraceTree.FromTrace(
  255. makeTrace({
  256. transactions: [
  257. makeTransaction({
  258. start_timestamp: 0,
  259. timestamp: 1,
  260. }),
  261. ],
  262. }),
  263. {
  264. measurements: {
  265. ttfb: {value: 2000, unit: 'millisecond'},
  266. },
  267. } as unknown as EventTransaction
  268. );
  269. expect(tree.root.space).toEqual([0, 2000]);
  270. expect(tree.indicators[0].start).toBe(2000);
  271. });
  272. it('sorts measurements by start', () => {
  273. const tree = TraceTree.FromTrace(
  274. makeTrace({
  275. transactions: [
  276. makeTransaction({
  277. start_timestamp: 0,
  278. timestamp: 1,
  279. }),
  280. ],
  281. }),
  282. {
  283. measurements: {
  284. ttfb: {value: 2000, unit: 'millisecond'},
  285. lcp: {value: 1000, unit: 'millisecond'},
  286. },
  287. } as unknown as EventTransaction
  288. );
  289. expect(tree.indicators[0].start).toBe(1000);
  290. expect(tree.indicators[1].start).toBe(2000);
  291. });
  292. });
  293. describe('parent autogrouped node segments', () => {
  294. it('collapses durations', () => {
  295. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  296. project_slug: '',
  297. event_id: '',
  298. });
  299. let parent = root;
  300. for (let i = 0; i < 5; i++) {
  301. const node = new TraceTreeNode(
  302. parent,
  303. makeSpan({
  304. description: 'span',
  305. op: 'db',
  306. start_timestamp: i,
  307. timestamp: i + 1,
  308. span_id: i.toString(),
  309. parent_span_id: parent.value.span_id,
  310. }),
  311. {
  312. project_slug: '',
  313. event_id: '',
  314. }
  315. );
  316. parent.children.push(node);
  317. parent = node;
  318. }
  319. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  320. const autogroupedNode = root.children[0];
  321. assertParentAutogroupedNode(autogroupedNode);
  322. expect(autogroupedNode.autogroupedSegments).toEqual([[0, 5000]]);
  323. });
  324. it('does not collapse durations when there is a gap', () => {
  325. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  326. project_slug: '',
  327. event_id: '',
  328. });
  329. let parent = root;
  330. const ts = [
  331. [0, 1],
  332. [1.5, 2],
  333. [2.5, 3],
  334. [3.5, 4],
  335. [4.5, 5],
  336. ];
  337. for (let i = 0; i < 5; i++) {
  338. const node = new TraceTreeNode(
  339. parent,
  340. makeSpan({
  341. description: 'span',
  342. op: 'db',
  343. start_timestamp: ts[i][0],
  344. timestamp: ts[i][1],
  345. span_id: i.toString(),
  346. parent_span_id: parent.value.span_id,
  347. }),
  348. {
  349. project_slug: '',
  350. event_id: '',
  351. }
  352. );
  353. parent.children.push(node);
  354. parent = node;
  355. }
  356. for (let i = 1; i < ts.length; i++) {
  357. ts[i][0] *= 1000;
  358. ts[i][1] = 0.5 * 1000;
  359. }
  360. ts[0][0] = 0;
  361. ts[0][1] = 1 * 1000;
  362. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  363. const autogroupedNode = root.children[0];
  364. assertParentAutogroupedNode(autogroupedNode);
  365. expect(autogroupedNode.autogroupedSegments).toEqual(ts);
  366. });
  367. });
  368. describe('sibling autogrouped node segments', () => {
  369. it('collapses durations', () => {
  370. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  371. project_slug: '',
  372. event_id: '',
  373. });
  374. for (let i = 0; i < 5; i++) {
  375. root.children.push(
  376. new TraceTreeNode(
  377. root,
  378. makeSpan({
  379. description: 'span',
  380. op: 'db',
  381. start_timestamp: i,
  382. timestamp: i + 1,
  383. }),
  384. {
  385. project_slug: '',
  386. event_id: '',
  387. }
  388. )
  389. );
  390. }
  391. TraceTree.AutogroupSiblingSpanNodes(root);
  392. const autogroupedNode = root.children[0];
  393. assertAutogroupedNode(autogroupedNode);
  394. expect(autogroupedNode.autogroupedSegments).toEqual([[0, 5000]]);
  395. });
  396. it('does not collapse durations when there is a gap', () => {
  397. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  398. project_slug: '',
  399. event_id: '',
  400. });
  401. const ts = [
  402. [0, 1],
  403. [1.5, 2],
  404. [2.5, 3],
  405. [3.5, 4],
  406. [4.5, 5],
  407. ];
  408. for (let i = 0; i < 5; i++) {
  409. root.children.push(
  410. new TraceTreeNode(
  411. root,
  412. makeSpan({
  413. description: 'span',
  414. op: 'db',
  415. start_timestamp: ts[i][0],
  416. timestamp: ts[i][1],
  417. }),
  418. {
  419. project_slug: '',
  420. event_id: '',
  421. }
  422. )
  423. );
  424. }
  425. for (let i = 0; i < ts.length; i++) {
  426. ts[i][0] *= 1000;
  427. ts[i][1] = 0.5 * 1000;
  428. }
  429. ts[0][0] = 0;
  430. ts[0][1] = 1 * 1000;
  431. TraceTree.AutogroupSiblingSpanNodes(root);
  432. const autogroupedNode = root.children[0];
  433. assertAutogroupedNode(autogroupedNode);
  434. expect(autogroupedNode.autogroupedSegments).toEqual(ts);
  435. });
  436. });
  437. describe('path', () => {
  438. describe('nested transactions', () => {
  439. let child: any = null;
  440. for (let i = 0; i < 3; i++) {
  441. const node = new TraceTreeNode(
  442. child,
  443. makeTransaction({
  444. event_id: i === 0 ? 'parent' : i === 1 ? 'child' : 'grandchild',
  445. }),
  446. {
  447. project_slug: '',
  448. event_id: '',
  449. }
  450. );
  451. child = node;
  452. }
  453. it('first txn node', () => {
  454. expect(child.parent.parent.path).toEqual(['txn:parent']);
  455. });
  456. it('leafmost node', () => {
  457. expect(child.path).toEqual(['txn:grandchild', 'txn:child', 'txn:parent']);
  458. });
  459. });
  460. it('orphan errors', () => {
  461. const tree = TraceTree.FromTrace(
  462. makeTrace({
  463. transactions: [],
  464. orphan_errors: [makeTraceError({event_id: 'error_id'})],
  465. })
  466. );
  467. expect(tree.list[1].path).toEqual(['error:error_id']);
  468. });
  469. describe('spans', () => {
  470. const tree = TraceTree.FromTrace(
  471. makeTrace({
  472. transactions: [
  473. makeTransaction({
  474. transaction: '/',
  475. project_slug: 'project',
  476. event_id: 'event_id',
  477. }),
  478. ],
  479. })
  480. );
  481. MockApiClient.addMockResponse({
  482. url: '/organizations/org-slug/events/project:event_id/',
  483. method: 'GET',
  484. body: makeEvent({}, [
  485. makeSpan({
  486. description: 'span',
  487. op: 'db',
  488. span_id: 'span',
  489. start_timestamp: 0,
  490. timestamp: 1,
  491. }),
  492. makeSpan({
  493. description: 'span',
  494. op: 'db',
  495. span_id: 'span',
  496. start_timestamp: 1.5,
  497. timestamp: 2,
  498. }),
  499. ]),
  500. });
  501. tree.zoomIn(tree.list[1], true, {
  502. api: new MockApiClient(),
  503. organization: OrganizationFixture(),
  504. });
  505. it('when span is a child of a txn', async () => {
  506. await waitFor(() => {
  507. expect(tree.list.length).toBe(5);
  508. });
  509. expect(tree.list[tree.list.length - 1].path).toEqual([
  510. 'span:span',
  511. 'txn:event_id',
  512. ]);
  513. });
  514. it('missing instrumentation', () => {
  515. expect(tree.list[3].path).toEqual(['ms:span', 'txn:event_id']);
  516. });
  517. });
  518. describe('autogrouped children', () => {
  519. const tree = TraceTree.FromTrace(
  520. makeTrace({
  521. transactions: [
  522. makeTransaction({
  523. transaction: '/',
  524. project_slug: 'project',
  525. event_id: 'event_id',
  526. }),
  527. ],
  528. })
  529. );
  530. MockApiClient.addMockResponse({
  531. url: '/organizations/org-slug/events/project:event_id/',
  532. method: 'GET',
  533. body: makeEvent({}, [
  534. makeSpan({description: 'span', op: 'db', span_id: '2'}),
  535. makeSpan({description: 'span', op: 'db', span_id: '3', parent_span_id: '2'}),
  536. makeSpan({description: 'span', op: 'db', span_id: '4', parent_span_id: '3'}),
  537. makeSpan({description: 'span', op: 'db', span_id: '5', parent_span_id: '4'}),
  538. ]),
  539. });
  540. tree.zoomIn(tree.list[1], true, {
  541. api: new MockApiClient(),
  542. organization: OrganizationFixture(),
  543. });
  544. it('autogrouped node', async () => {
  545. await waitFor(() => {
  546. expect(tree.list.length).toBe(3);
  547. });
  548. tree.expand(tree.list[2], true);
  549. assertAutogroupedNode(tree.list[2]);
  550. expect(tree.list[2].path).toEqual(['ag:2', 'txn:event_id']);
  551. });
  552. it('child is part of autogrouping', () => {
  553. expect(tree.list[tree.list.length - 1].path).toEqual([
  554. 'span:5',
  555. 'ag:2',
  556. 'txn:event_id',
  557. ]);
  558. });
  559. });
  560. describe('collapses some node by default', () => {
  561. it('android okhttp', async () => {
  562. const tree = TraceTree.FromTrace(
  563. makeTrace({
  564. transactions: [
  565. makeTransaction({
  566. transaction: '/',
  567. project_slug: 'project',
  568. event_id: 'event_id',
  569. }),
  570. ],
  571. })
  572. );
  573. MockApiClient.addMockResponse({
  574. url: '/organizations/org-slug/events/project:event_id/',
  575. method: 'GET',
  576. body: makeEvent({}, [
  577. makeSpan({
  578. description: 'span',
  579. span_id: '2',
  580. op: 'http.client',
  581. origin: 'auto.http.okhttp',
  582. }),
  583. makeSpan({
  584. description: 'span',
  585. op: 'http.client.tls',
  586. span_id: '3',
  587. parent_span_id: '2',
  588. }),
  589. ]),
  590. });
  591. tree.zoomIn(tree.list[1], true, {
  592. api: new MockApiClient(),
  593. organization: OrganizationFixture(),
  594. });
  595. await waitFor(() => {
  596. // trace
  597. // - transaction
  598. // - http.client
  599. // ^ child of http.client is not visible
  600. expect(tree.list.length).toBe(3);
  601. });
  602. });
  603. });
  604. describe('non expanded direct children autogrouped path', () => {
  605. const tree = TraceTree.FromTrace(
  606. makeTrace({
  607. transactions: [
  608. makeTransaction({
  609. transaction: '/',
  610. project_slug: 'project',
  611. event_id: 'event_id',
  612. }),
  613. ],
  614. })
  615. );
  616. MockApiClient.addMockResponse({
  617. url: '/organizations/org-slug/events/project:event_id/',
  618. method: 'GET',
  619. body: makeEvent({}, [
  620. makeSpan({description: 'span', op: 'db', span_id: '2'}),
  621. makeSpan({description: 'span', op: 'db', span_id: '3', parent_span_id: '2'}),
  622. makeSpan({description: 'span', op: 'db', span_id: '4', parent_span_id: '3'}),
  623. makeSpan({description: 'span', op: 'db', span_id: '5', parent_span_id: '4'}),
  624. makeSpan({description: 'span', op: '6', span_id: '6', parent_span_id: '5'}),
  625. ]),
  626. });
  627. tree.zoomIn(tree.list[1], true, {
  628. api: new MockApiClient(),
  629. organization: OrganizationFixture(),
  630. });
  631. it('autogrouped node', async () => {
  632. await waitFor(() => {
  633. expect(tree.list.length).toBe(4);
  634. });
  635. assertAutogroupedNode(tree.list[2]);
  636. expect(tree.list[2].path).toEqual(['ag:2', 'txn:event_id']);
  637. });
  638. it('span node skips autogrouped node because it is not expanded', async () => {
  639. await waitFor(() => {
  640. expect(tree.list.length).toBe(4);
  641. });
  642. expect(tree.list[tree.list.length - 1].path).toEqual(['span:6', 'txn:event_id']);
  643. });
  644. });
  645. });
  646. });
  647. describe('TraceTree', () => {
  648. beforeEach(() => {
  649. MockApiClient.clearMockResponses();
  650. });
  651. it('builds from transactions', () => {
  652. const tree = TraceTree.FromTrace(
  653. makeTrace({
  654. transactions: [
  655. makeTransaction({
  656. children: [],
  657. }),
  658. makeTransaction({
  659. children: [],
  660. }),
  661. ],
  662. })
  663. );
  664. expect(tree.list).toHaveLength(3);
  665. });
  666. it('builds orphan errors', () => {
  667. const tree = TraceTree.FromTrace(
  668. makeTrace({
  669. transactions: [
  670. makeTransaction({
  671. children: [],
  672. }),
  673. makeTransaction({
  674. children: [],
  675. }),
  676. ],
  677. orphan_errors: [makeTraceError()],
  678. })
  679. );
  680. expect(tree.list).toHaveLength(4);
  681. });
  682. it('processes orphan errors without timestamps', () => {
  683. const tree = TraceTree.FromTrace(
  684. makeTrace({
  685. transactions: [
  686. makeTransaction({
  687. children: [],
  688. }),
  689. ],
  690. orphan_errors: [
  691. {
  692. level: 'error',
  693. title: 'Error',
  694. event_type: 'error',
  695. } as TraceTree.TraceError,
  696. ],
  697. })
  698. );
  699. expect(tree.list).toHaveLength(3);
  700. });
  701. it('sorts orphan errors', () => {
  702. const tree = TraceTree.FromTrace(
  703. makeTrace({
  704. transactions: [
  705. makeTransaction({
  706. start_timestamp: 0,
  707. timestamp: 1,
  708. }),
  709. makeTransaction({
  710. start_timestamp: 2,
  711. timestamp: 3,
  712. }),
  713. ],
  714. orphan_errors: [makeTraceError({timestamp: 1, level: 'error'})],
  715. })
  716. );
  717. expect(tree.list).toHaveLength(4);
  718. assertTraceErrorNode(tree.list[2]);
  719. });
  720. it('adjusts trace bounds by orphan error timestamp as well', () => {
  721. const tree = TraceTree.FromTrace(
  722. makeTrace({
  723. transactions: [
  724. makeTransaction({
  725. start_timestamp: 0.1,
  726. timestamp: 0.15,
  727. children: [],
  728. }),
  729. makeTransaction({
  730. start_timestamp: 0.2,
  731. timestamp: 0.25,
  732. children: [],
  733. }),
  734. ],
  735. orphan_errors: [
  736. makeTraceError({timestamp: 0.05}),
  737. makeTraceError({timestamp: 0.3}),
  738. ],
  739. })
  740. );
  741. expect(tree.list).toHaveLength(5);
  742. expect(tree.root.space).toStrictEqual([
  743. 0.05 * tree.root.multiplier,
  744. (0.3 - 0.05) * tree.root.multiplier,
  745. ]);
  746. });
  747. it('calculates correct trace type', () => {
  748. let tree = TraceTree.FromTrace(
  749. makeTrace({
  750. transactions: [],
  751. orphan_errors: [],
  752. })
  753. );
  754. expect(tree.shape).toBe(TraceType.EMPTY_TRACE);
  755. tree = TraceTree.FromTrace(
  756. makeTrace({
  757. transactions: [
  758. makeTransaction({
  759. children: [],
  760. }),
  761. makeTransaction({
  762. children: [],
  763. }),
  764. ],
  765. orphan_errors: [],
  766. })
  767. );
  768. expect(tree.shape).toBe(TraceType.NO_ROOT);
  769. tree = TraceTree.FromTrace(
  770. makeTrace({
  771. transactions: [
  772. makeTransaction({
  773. parent_span_id: null,
  774. children: [],
  775. }),
  776. ],
  777. orphan_errors: [],
  778. })
  779. );
  780. expect(tree.shape).toBe(TraceType.ONE_ROOT);
  781. tree = TraceTree.FromTrace(
  782. makeTrace({
  783. transactions: [
  784. makeTransaction({
  785. parent_span_id: null,
  786. children: [],
  787. }),
  788. makeTransaction({
  789. children: [],
  790. }),
  791. ],
  792. orphan_errors: [],
  793. })
  794. );
  795. expect(tree.shape).toBe(TraceType.BROKEN_SUBTRACES);
  796. tree = TraceTree.FromTrace(
  797. makeTrace({
  798. transactions: [
  799. makeTransaction({
  800. parent_span_id: null,
  801. children: [],
  802. }),
  803. makeTransaction({
  804. parent_span_id: null,
  805. children: [],
  806. }),
  807. ],
  808. orphan_errors: [],
  809. })
  810. );
  811. expect(tree.shape).toBe(TraceType.MULTIPLE_ROOTS);
  812. tree = TraceTree.FromTrace(
  813. makeTrace({
  814. transactions: [],
  815. orphan_errors: [makeTraceError()],
  816. })
  817. );
  818. expect(tree.shape).toBe(TraceType.ONLY_ERRORS);
  819. });
  820. it('builds from spans when root is a transaction node', () => {
  821. const root = new TraceTreeNode(
  822. null,
  823. makeTransaction({
  824. children: [],
  825. }),
  826. {project_slug: '', event_id: ''}
  827. );
  828. const node = TraceTree.FromSpans(
  829. root,
  830. makeEvent(),
  831. [
  832. makeSpan({start_timestamp: 0, op: '1', span_id: '1'}),
  833. makeSpan({start_timestamp: 1, op: '2', span_id: '2', parent_span_id: '1'}),
  834. makeSpan({start_timestamp: 2, op: '3', span_id: '3', parent_span_id: '2'}),
  835. makeSpan({start_timestamp: 3, op: '4', span_id: '4', parent_span_id: '1'}),
  836. ],
  837. {sdk: undefined}
  838. );
  839. if (!isSpanNode(node.children[0])) {
  840. throw new Error('Child needs to be a span');
  841. }
  842. expect(node.children[0].value.span_id).toBe('1');
  843. expect(node.children[0].value.start_timestamp).toBe(0);
  844. expect(node.children.length).toBe(1);
  845. assertSpanNode(node.children[0].children[0]);
  846. assertSpanNode(node.children[0].children[0].children[0]);
  847. assertSpanNode(node.children[0].children[1]);
  848. expect(node.children[0].children[0].value.start_timestamp).toBe(1);
  849. expect(node.children[0].children[0].children[0].value.start_timestamp).toBe(2);
  850. expect(node.children[0].children[1].value.start_timestamp).toBe(3);
  851. });
  852. it('builds from spans and copies txn nodes', () => {
  853. // transaction transaction
  854. // - child transaction -> - span
  855. // - span
  856. // - child-transaction
  857. // - span
  858. const root = new TraceTreeNode(
  859. null,
  860. makeTransaction({
  861. children: [],
  862. }),
  863. {project_slug: '', event_id: ''}
  864. );
  865. root.children.push(
  866. new TraceTreeNode(
  867. root,
  868. makeTransaction({
  869. parent_span_id: 'child-transaction',
  870. }),
  871. {project_slug: '', event_id: ''}
  872. )
  873. );
  874. const node = TraceTree.FromSpans(
  875. root,
  876. makeEvent(),
  877. [
  878. makeSpan({start_timestamp: 0, timestamp: 0.1, op: 'span', span_id: 'none'}),
  879. makeSpan({
  880. start_timestamp: 0.1,
  881. timestamp: 0.2,
  882. op: 'child-transaction',
  883. span_id: 'child-transaction',
  884. }),
  885. makeSpan({start_timestamp: 0.2, timestamp: 0.25, op: 'span', span_id: 'none'}),
  886. ],
  887. {sdk: undefined}
  888. );
  889. assertSpanNode(node.children[1]);
  890. assertTransactionNode(node.children[1].children[0]);
  891. });
  892. it('builds from spans and copies txn nodes to nested children', () => {
  893. // parent transaction parent transaction
  894. // - child transaction -> - span
  895. // - grandchild transaction -> - child-transaction
  896. // - grandchild-transaction
  897. //
  898. const root = new TraceTreeNode(
  899. null,
  900. makeTransaction({
  901. span_id: 'parent-transaction',
  902. children: [],
  903. }),
  904. {project_slug: '', event_id: ''}
  905. );
  906. let start: TraceTreeNode<TraceTree.NodeValue> = root;
  907. for (let i = 0; i < 2; i++) {
  908. const node = new TraceTreeNode(
  909. start,
  910. makeTransaction({
  911. transaction: `${i === 0 ? 'child' : 'grandchild'}-transaction`,
  912. parent_span_id: `${i === 0 ? 'child' : 'grandchild'}-transaction`,
  913. }),
  914. {project_slug: '', event_id: ''}
  915. );
  916. start.children.push(node);
  917. start = node;
  918. }
  919. const node = TraceTree.FromSpans(
  920. root,
  921. makeEvent(),
  922. [
  923. makeSpan({start_timestamp: 0, timestamp: 0.1, op: 'span', span_id: 'none'}),
  924. makeSpan({
  925. start_timestamp: 0.1,
  926. timestamp: 0.2,
  927. op: 'child-transaction',
  928. span_id: 'child-transaction',
  929. }),
  930. ],
  931. {sdk: undefined}
  932. );
  933. assertSpanNode(node.children[1]);
  934. assertTransactionNode(node.children[1].children[0]);
  935. assertTransactionNode(node.children[1].children[0].children[0]);
  936. });
  937. it('injects missing spans', () => {
  938. const root = new TraceTreeNode(
  939. null,
  940. makeTransaction({
  941. children: [],
  942. }),
  943. {project_slug: '', event_id: ''}
  944. );
  945. const date = new Date().getTime();
  946. const node = TraceTree.FromSpans(
  947. root,
  948. makeEvent(),
  949. [
  950. makeSpan({
  951. start_timestamp: date,
  952. timestamp: date + 1,
  953. span_id: '1',
  954. op: 'span 1',
  955. }),
  956. makeSpan({
  957. start_timestamp: date + 2,
  958. timestamp: date + 4,
  959. op: 'span 2',
  960. span_id: '2',
  961. }),
  962. ],
  963. {sdk: undefined}
  964. );
  965. assertSpanNode(node.children[0]);
  966. assertMissingInstrumentationNode(node.children[1]);
  967. assertSpanNode(node.children[2]);
  968. expect(node.children.length).toBe(3);
  969. expect(node.children[0].value.op).toBe('span 1');
  970. expect(node.children[1].value.type).toBe('missing_instrumentation');
  971. expect(node.children[2].value.op).toBe('span 2');
  972. });
  973. it('does not inject missing spans for javascript platform', () => {
  974. const root = new TraceTreeNode(
  975. null,
  976. makeTransaction({
  977. children: [],
  978. }),
  979. {project_slug: '', event_id: ''}
  980. );
  981. const date = new Date().getTime();
  982. const node = TraceTree.FromSpans(
  983. root,
  984. makeEvent(),
  985. [
  986. makeSpan({
  987. start_timestamp: date,
  988. timestamp: date + 1,
  989. span_id: '1',
  990. op: 'span 1',
  991. }),
  992. makeSpan({
  993. start_timestamp: date + 2,
  994. timestamp: date + 4,
  995. op: 'span 2',
  996. span_id: '2',
  997. }),
  998. ],
  999. {sdk: 'sentry.javascript.browser'}
  1000. );
  1001. assertSpanNode(node.children[0]);
  1002. assertSpanNode(node.children[1]);
  1003. expect(node.children.length).toBe(2);
  1004. expect(node.children[0].value.op).toBe('span 1');
  1005. expect(node.children[1].value.op).toBe('span 2');
  1006. });
  1007. it('builds and preserves list order', async () => {
  1008. const organization = OrganizationFixture();
  1009. const api = new MockApiClient();
  1010. const tree = TraceTree.FromTrace(
  1011. makeTrace({
  1012. transactions: [
  1013. makeTransaction({
  1014. transaction: 'txn 1',
  1015. start_timestamp: 0,
  1016. children: [makeTransaction({start_timestamp: 1, transaction: 'txn 2'})],
  1017. }),
  1018. ],
  1019. })
  1020. );
  1021. tree.expand(tree.list[0], true);
  1022. const node = tree.list[1];
  1023. const request = MockApiClient.addMockResponse({
  1024. url: '/organizations/org-slug/events/undefined:undefined/',
  1025. method: 'GET',
  1026. body: makeEvent({startTimestamp: 0}, [
  1027. makeSpan({start_timestamp: 1, op: 'span 1', span_id: '1'}),
  1028. makeSpan({
  1029. start_timestamp: 2,
  1030. op: 'span 2',
  1031. span_id: '2',
  1032. parent_span_id: '1',
  1033. }),
  1034. makeSpan({start_timestamp: 3, op: 'span 3', parent_span_id: '2'}),
  1035. makeSpan({start_timestamp: 4, op: 'span 4', parent_span_id: '1'}),
  1036. ]),
  1037. });
  1038. // 0
  1039. // 1
  1040. // 2
  1041. // 3
  1042. // 4
  1043. tree.zoomIn(node, true, {api, organization});
  1044. await waitFor(() => {
  1045. expect(node.zoomedIn).toBe(true);
  1046. });
  1047. expect(request).toHaveBeenCalled();
  1048. expect(tree.list.length).toBe(6);
  1049. assertTransactionNode(tree.list[1]);
  1050. assertSpanNode(tree.list[2]);
  1051. assertSpanNode(tree.list[3]);
  1052. expect(tree.list[1].value.start_timestamp).toBe(0);
  1053. expect(tree.list[2].value.start_timestamp).toBe(1);
  1054. expect(tree.list[3].value.start_timestamp).toBe(2);
  1055. });
  1056. it('preserves input order', () => {
  1057. const firstChild = makeTransaction({
  1058. start_timestamp: 0,
  1059. timestamp: 1,
  1060. children: [],
  1061. });
  1062. const secondChild = makeTransaction({
  1063. start_timestamp: 1,
  1064. timestamp: 2,
  1065. children: [],
  1066. });
  1067. const tree = TraceTree.FromTrace(
  1068. makeTrace({
  1069. transactions: [
  1070. makeTransaction({
  1071. start_timestamp: 0,
  1072. timestamp: 2,
  1073. children: [firstChild, secondChild],
  1074. }),
  1075. makeTransaction({
  1076. start_timestamp: 2,
  1077. timestamp: 4,
  1078. }),
  1079. ],
  1080. })
  1081. );
  1082. expect(tree.list).toHaveLength(5);
  1083. expect(tree.expand(tree.list[1], false)).toBe(true);
  1084. expect(tree.list).toHaveLength(3);
  1085. expect(tree.expand(tree.list[1], true)).toBe(true);
  1086. expect(tree.list).toHaveLength(5);
  1087. expect(tree.list[2].value).toBe(firstChild);
  1088. expect(tree.list[3].value).toBe(secondChild);
  1089. });
  1090. it('creates children -> parent references', () => {
  1091. const tree = TraceTree.FromTrace(
  1092. makeTrace({
  1093. transactions: [
  1094. makeTransaction({
  1095. start_timestamp: 0,
  1096. timestamp: 2,
  1097. children: [makeTransaction({start_timestamp: 1, timestamp: 2})],
  1098. }),
  1099. makeTransaction({
  1100. start_timestamp: 2,
  1101. timestamp: 4,
  1102. }),
  1103. ],
  1104. })
  1105. );
  1106. expect(tree.list).toHaveLength(4);
  1107. expect(tree.list[2].parent?.value).toBe(tree.list[1].value);
  1108. });
  1109. it('establishes parent-child relationships', () => {
  1110. const tree = TraceTree.FromTrace(
  1111. makeTrace({
  1112. transactions: [
  1113. makeTransaction({
  1114. children: [makeTransaction()],
  1115. }),
  1116. ],
  1117. })
  1118. );
  1119. expect(tree.root.children).toHaveLength(1);
  1120. expect(tree.root.children[0].children).toHaveLength(1);
  1121. });
  1122. it('isLastChild', () => {
  1123. const tree = TraceTree.FromTrace(
  1124. makeTrace({
  1125. transactions: [
  1126. makeTransaction({
  1127. children: [makeTransaction(), makeTransaction()],
  1128. }),
  1129. makeTransaction(),
  1130. ],
  1131. orphan_errors: [],
  1132. })
  1133. );
  1134. tree.expand(tree.list[1], true);
  1135. expect(tree.list[0].isLastChild).toBe(true);
  1136. expect(tree.list[1].isLastChild).toBe(false);
  1137. expect(tree.list[2].isLastChild).toBe(false);
  1138. expect(tree.list[3].isLastChild).toBe(true);
  1139. expect(tree.list[4].isLastChild).toBe(true);
  1140. });
  1141. describe('connectors', () => {
  1142. it('computes transaction connectors', () => {
  1143. const tree = TraceTree.FromTrace(
  1144. makeTrace({
  1145. transactions: [
  1146. makeTransaction({
  1147. transaction: 'sibling',
  1148. children: [
  1149. makeTransaction({transaction: 'child'}),
  1150. makeTransaction({transaction: 'child'}),
  1151. ],
  1152. }),
  1153. makeTransaction({transaction: 'sibling'}),
  1154. ],
  1155. })
  1156. );
  1157. // -1 root
  1158. // ------ list begins here
  1159. // 0 transaction
  1160. // 0 |- sibling
  1161. // -1, 2| | - child
  1162. // -1| | - child
  1163. // 0 |- sibling
  1164. tree.expand(tree.list[1], true);
  1165. expect(tree.list.length).toBe(5);
  1166. expect(tree.list[0].connectors.length).toBe(0);
  1167. expect(tree.list[1].connectors.length).toBe(1);
  1168. expect(tree.list[1].connectors[0]).toBe(-1);
  1169. expect(tree.list[2].connectors[0]).toBe(-1);
  1170. expect(tree.list[2].connectors[1]).toBe(2);
  1171. expect(tree.list[2].connectors.length).toBe(2);
  1172. expect(tree.list[3].connectors[0]).toBe(-1);
  1173. expect(tree.list[3].connectors.length).toBe(1);
  1174. expect(tree.list[4].connectors.length).toBe(0);
  1175. });
  1176. it('computes span connectors', async () => {
  1177. const tree = TraceTree.FromTrace(
  1178. makeTrace({
  1179. transactions: [
  1180. makeTransaction({
  1181. project_slug: 'project',
  1182. event_id: 'event_id',
  1183. transaction: 'transaction',
  1184. children: [],
  1185. }),
  1186. ],
  1187. })
  1188. );
  1189. // root
  1190. // |- node1 []
  1191. // |- node2 []
  1192. MockApiClient.addMockResponse({
  1193. url: '/organizations/org-slug/events/project:event_id/',
  1194. method: 'GET',
  1195. body: makeEvent({}, [makeSpan({start_timestamp: 0, op: 'span', span_id: '1'})]),
  1196. });
  1197. expect(tree.list.length).toBe(2);
  1198. tree.zoomIn(tree.list[1], true, {
  1199. api: new MockApiClient(),
  1200. organization: OrganizationFixture(),
  1201. });
  1202. await waitFor(() => {
  1203. expect(tree.list.length).toBe(3);
  1204. });
  1205. // root
  1206. // |- node1 []
  1207. // |- node2 []
  1208. // |- span1 []
  1209. const span = tree.list[tree.list.length - 1];
  1210. expect(span.connectors.length).toBe(0);
  1211. });
  1212. });
  1213. describe('expanding', () => {
  1214. it('expands a node and updates the list', () => {
  1215. const tree = TraceTree.FromTrace(
  1216. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  1217. );
  1218. const node = tree.list[1];
  1219. expect(tree.expand(node, false)).toBe(true);
  1220. expect(tree.list.length).toBe(2);
  1221. expect(node.expanded).toBe(false);
  1222. expect(tree.expand(node, true)).toBe(true);
  1223. expect(node.expanded).toBe(true);
  1224. // Assert that the list has been updated
  1225. expect(tree.list).toHaveLength(3);
  1226. expect(tree.list[2]).toBe(node.children[0]);
  1227. });
  1228. it('collapses a node and updates the list', () => {
  1229. const tree = TraceTree.FromTrace(
  1230. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  1231. );
  1232. const node = tree.list[1];
  1233. tree.expand(node, true);
  1234. expect(tree.list.length).toBe(3);
  1235. expect(tree.expand(node, false)).toBe(true);
  1236. expect(node.expanded).toBe(false);
  1237. // Assert that the list has been updated
  1238. expect(tree.list).toHaveLength(2);
  1239. expect(tree.list[1]).toBe(node);
  1240. });
  1241. it('preserves children expanded state', () => {
  1242. const tree = TraceTree.FromTrace(
  1243. makeTrace({
  1244. transactions: [
  1245. makeTransaction({
  1246. children: [
  1247. makeTransaction({children: [makeTransaction({start_timestamp: 1000})]}),
  1248. makeTransaction({start_timestamp: 5}),
  1249. ],
  1250. }),
  1251. ],
  1252. })
  1253. );
  1254. expect(tree.expand(tree.list[2], false)).toBe(true);
  1255. // Assert that the list has been updated
  1256. expect(tree.list).toHaveLength(4);
  1257. expect(tree.expand(tree.list[2], true)).toBe(true);
  1258. expect(tree.list.length).toBe(5);
  1259. expect(tree.list[tree.list.length - 1].value).toEqual(
  1260. makeTransaction({start_timestamp: 5})
  1261. );
  1262. });
  1263. it('expanding or collapsing a zoomed in node doesnt do anything', async () => {
  1264. const organization = OrganizationFixture();
  1265. const api = new MockApiClient();
  1266. const tree = TraceTree.FromTrace(
  1267. makeTrace({transactions: [makeTransaction({children: [makeTransaction()]})]})
  1268. );
  1269. const node = tree.list[0];
  1270. const request = MockApiClient.addMockResponse({
  1271. url: '/organizations/org-slug/events/undefined:undefined/',
  1272. method: 'GET',
  1273. body: makeEvent(),
  1274. });
  1275. tree.zoomIn(node, true, {api, organization});
  1276. await waitFor(() => {
  1277. expect(node.zoomedIn).toBe(true);
  1278. });
  1279. expect(request).toHaveBeenCalled();
  1280. expect(tree.expand(node, true)).toBe(false);
  1281. });
  1282. });
  1283. describe('zooming', () => {
  1284. it('marks node as zoomed in', async () => {
  1285. const organization = OrganizationFixture();
  1286. const api = new MockApiClient();
  1287. const tree = TraceTree.FromTrace(
  1288. makeTrace({
  1289. transactions: [
  1290. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  1291. ],
  1292. })
  1293. );
  1294. const request = MockApiClient.addMockResponse({
  1295. url: '/organizations/org-slug/events/project:event_id/',
  1296. method: 'GET',
  1297. body: makeEvent(),
  1298. });
  1299. const node = tree.list[1];
  1300. expect(node.zoomedIn).toBe(false);
  1301. tree.zoomIn(node, true, {api, organization});
  1302. await waitFor(() => {
  1303. expect(node.zoomedIn).toBe(true);
  1304. });
  1305. expect(request).toHaveBeenCalled();
  1306. });
  1307. it('fetches spans for node when zooming in', async () => {
  1308. const tree = TraceTree.FromTrace(
  1309. makeTrace({
  1310. transactions: [
  1311. makeTransaction({
  1312. transaction: 'txn',
  1313. project_slug: 'project',
  1314. event_id: 'event_id',
  1315. }),
  1316. ],
  1317. })
  1318. );
  1319. const request = MockApiClient.addMockResponse({
  1320. url: '/organizations/org-slug/events/project:event_id/',
  1321. method: 'GET',
  1322. body: makeEvent({}, [makeSpan()]),
  1323. });
  1324. const node = tree.list[1];
  1325. expect(node.children).toHaveLength(0);
  1326. tree.zoomIn(node, true, {
  1327. api: new MockApiClient(),
  1328. organization: OrganizationFixture(),
  1329. });
  1330. expect(request).toHaveBeenCalled();
  1331. await waitFor(() => {
  1332. expect(node.children).toHaveLength(1);
  1333. });
  1334. // Assert that the children have been updated
  1335. assertTransactionNode(node.children[0].parent);
  1336. expect(node.children[0].parent.value.transaction).toBe('txn');
  1337. expect(node.children[0].depth).toBe(node.depth + 1);
  1338. });
  1339. it('handles bottom up zooming', async () => {
  1340. const tree = TraceTree.FromTrace(
  1341. makeTrace({
  1342. transactions: [
  1343. makeTransaction({
  1344. transaction: 'transaction',
  1345. project_slug: 'project',
  1346. event_id: 'event_id',
  1347. children: [
  1348. makeTransaction({
  1349. parent_span_id: 'span',
  1350. transaction: 'child transaction',
  1351. project_slug: 'child_project',
  1352. event_id: 'child_event_id',
  1353. }),
  1354. ],
  1355. }),
  1356. ],
  1357. })
  1358. );
  1359. const first_request = MockApiClient.addMockResponse({
  1360. url: '/organizations/org-slug/events/project:event_id/',
  1361. method: 'GET',
  1362. body: makeEvent({}, [makeSpan({op: 'db', span_id: 'span'})]),
  1363. });
  1364. const second_request = MockApiClient.addMockResponse({
  1365. url: '/organizations/org-slug/events/child_project:child_event_id/',
  1366. method: 'GET',
  1367. body: makeEvent({}, [
  1368. makeSpan({op: 'db', span_id: 'span'}),
  1369. makeSpan({op: 'db', span_id: 'span 1', parent_span_id: 'span'}),
  1370. makeSpan({op: 'db', span_id: 'span 2', parent_span_id: 'span 1'}),
  1371. makeSpan({op: 'db', span_id: 'span 3', parent_span_id: 'span 2'}),
  1372. makeSpan({op: 'db', span_id: 'span 4', parent_span_id: 'span 3'}),
  1373. makeSpan({op: 'db', span_id: 'span 5', parent_span_id: 'span 4'}),
  1374. ]),
  1375. });
  1376. tree.zoomIn(tree.list[2], true, {
  1377. api: new MockApiClient(),
  1378. organization: OrganizationFixture(),
  1379. });
  1380. await waitFor(() => {
  1381. expect(second_request).toHaveBeenCalled();
  1382. });
  1383. assertParentAutogroupedNode(tree.list[tree.list.length - 1]);
  1384. tree.zoomIn(tree.list[1], true, {
  1385. api: new MockApiClient(),
  1386. organization: OrganizationFixture(),
  1387. });
  1388. await waitFor(() => {
  1389. expect(first_request).toHaveBeenCalled();
  1390. });
  1391. assertParentAutogroupedNode(tree.list[tree.list.length - 1]);
  1392. });
  1393. it('zooms out', async () => {
  1394. const tree = TraceTree.FromTrace(
  1395. makeTrace({
  1396. transactions: [
  1397. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  1398. ],
  1399. })
  1400. );
  1401. MockApiClient.addMockResponse({
  1402. url: '/organizations/org-slug/events/project:event_id/',
  1403. method: 'GET',
  1404. body: makeEvent({}, [makeSpan({span_id: 'span1', description: 'span1'})]),
  1405. });
  1406. tree.zoomIn(tree.list[1], true, {
  1407. api: new MockApiClient(),
  1408. organization: OrganizationFixture(),
  1409. });
  1410. await waitFor(() => {
  1411. assertSpanNode(tree.list[1].children[0]);
  1412. expect(tree.list[1].children[0].value.description).toBe('span1');
  1413. });
  1414. tree.zoomIn(tree.list[1], false, {
  1415. api: new MockApiClient(),
  1416. organization: OrganizationFixture(),
  1417. });
  1418. await waitFor(() => {
  1419. // Assert child no longer points to children
  1420. expect(tree.list[1].zoomedIn).toBe(false);
  1421. expect(tree.list[1].children[0]).toBe(undefined);
  1422. expect(tree.list[2]).toBe(undefined);
  1423. });
  1424. });
  1425. it('zooms in and out', async () => {
  1426. const tree = TraceTree.FromTrace(
  1427. makeTrace({
  1428. transactions: [
  1429. makeTransaction({project_slug: 'project', event_id: 'event_id'}),
  1430. ],
  1431. })
  1432. );
  1433. MockApiClient.addMockResponse({
  1434. url: '/organizations/org-slug/events/project:event_id/',
  1435. method: 'GET',
  1436. body: makeEvent({}, [makeSpan({span_id: 'span 1', description: 'span1'})]),
  1437. });
  1438. // Zoom in
  1439. tree.zoomIn(tree.list[1], true, {
  1440. api: new MockApiClient(),
  1441. organization: OrganizationFixture(),
  1442. });
  1443. await waitFor(() => {
  1444. expect(tree.list[1].zoomedIn).toBe(true);
  1445. assertSpanNode(tree.list[1].children[0]);
  1446. expect(tree.list[1].children[0].value.description).toBe('span1');
  1447. });
  1448. // Zoom out
  1449. tree.zoomIn(tree.list[1], false, {
  1450. api: new MockApiClient(),
  1451. organization: OrganizationFixture(),
  1452. });
  1453. await waitFor(() => {
  1454. expect(tree.list[2]).toBe(undefined);
  1455. });
  1456. // Zoom in
  1457. tree.zoomIn(tree.list[1], true, {
  1458. api: new MockApiClient(),
  1459. organization: OrganizationFixture(),
  1460. });
  1461. await waitFor(() => {
  1462. assertSpanNode(tree.list[1].children[0]);
  1463. expect(tree.list[1].children[0].value?.description).toBe('span1');
  1464. });
  1465. });
  1466. it('zooms in and out preserving siblings', async () => {
  1467. const tree = TraceTree.FromTrace(
  1468. makeTrace({
  1469. transactions: [
  1470. makeTransaction({
  1471. project_slug: 'project',
  1472. event_id: 'event_id',
  1473. start_timestamp: 0,
  1474. children: [
  1475. makeTransaction({
  1476. start_timestamp: 1,
  1477. timestamp: 2,
  1478. project_slug: 'other_project',
  1479. event_id: 'event_id',
  1480. }),
  1481. makeTransaction({start_timestamp: 2, timestamp: 3}),
  1482. ],
  1483. }),
  1484. ],
  1485. })
  1486. );
  1487. const request = MockApiClient.addMockResponse({
  1488. url: '/organizations/org-slug/events/other_project:event_id/',
  1489. method: 'GET',
  1490. body: makeEvent({}, [makeSpan({description: 'span1'})]),
  1491. });
  1492. tree.expand(tree.list[1], true);
  1493. tree.zoomIn(tree.list[2], true, {
  1494. api: new MockApiClient(),
  1495. organization: OrganizationFixture(),
  1496. });
  1497. expect(request).toHaveBeenCalled();
  1498. // Zoom in
  1499. await waitFor(() => {
  1500. expect(tree.list.length).toBe(5);
  1501. });
  1502. // Zoom out
  1503. tree.zoomIn(tree.list[2], false, {
  1504. api: new MockApiClient(),
  1505. organization: OrganizationFixture(),
  1506. });
  1507. await waitFor(() => {
  1508. expect(tree.list.length).toBe(4);
  1509. });
  1510. });
  1511. it('preserves expanded state when zooming in and out', async () => {
  1512. const tree = TraceTree.FromTrace(
  1513. makeTrace({
  1514. transactions: [
  1515. makeTransaction({
  1516. project_slug: 'project',
  1517. event_id: 'event_id',
  1518. children: [
  1519. makeTransaction({project_slug: 'other_project', event_id: 'event_id'}),
  1520. ],
  1521. }),
  1522. ],
  1523. })
  1524. );
  1525. MockApiClient.addMockResponse({
  1526. url: '/organizations/org-slug/events/project:event_id/',
  1527. method: 'GET',
  1528. body: makeEvent({}, [
  1529. makeSpan({description: 'span1'}),
  1530. makeSpan({description: 'span2'}),
  1531. ]),
  1532. });
  1533. tree.expand(tree.list[1], true);
  1534. expect(tree.list.length).toBe(3);
  1535. tree.zoomIn(tree.list[1], true, {
  1536. api: new MockApiClient(),
  1537. organization: OrganizationFixture(),
  1538. });
  1539. await waitFor(() => {
  1540. expect(tree.list.length).toBe(4);
  1541. });
  1542. tree.zoomIn(tree.list[1], false, {
  1543. api: new MockApiClient(),
  1544. organization: OrganizationFixture(),
  1545. });
  1546. await waitFor(() => {
  1547. expect(tree.list.length).toBe(3);
  1548. });
  1549. expect(tree.list[1].expanded).toBe(true);
  1550. });
  1551. });
  1552. describe('autogrouping', () => {
  1553. it('auto groups sibling spans and preserves tail spans', () => {
  1554. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  1555. project_slug: '',
  1556. event_id: '',
  1557. });
  1558. for (let i = 0; i < 5; i++) {
  1559. root.children.push(
  1560. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  1561. project_slug: '',
  1562. event_id: '',
  1563. })
  1564. );
  1565. }
  1566. root.children.push(
  1567. new TraceTreeNode(root, makeSpan({description: 'span', op: 'http'}), {
  1568. project_slug: '',
  1569. event_id: '',
  1570. })
  1571. );
  1572. expect(root.children.length).toBe(6);
  1573. TraceTree.AutogroupSiblingSpanNodes(root);
  1574. expect(root.children.length).toBe(2);
  1575. });
  1576. it('autogroups when number of children is exactly 5', () => {
  1577. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  1578. project_slug: '',
  1579. event_id: '',
  1580. });
  1581. for (let i = 0; i < 5; i++) {
  1582. root.children.push(
  1583. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  1584. project_slug: '',
  1585. event_id: '',
  1586. })
  1587. );
  1588. }
  1589. expect(root.children.length).toBe(5);
  1590. TraceTree.AutogroupSiblingSpanNodes(root);
  1591. expect(root.children.length).toBe(1);
  1592. });
  1593. it('collects errored children for sibling autogrouped node', () => {
  1594. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  1595. project_slug: '',
  1596. event_id: '',
  1597. });
  1598. for (let i = 0; i < 5; i++) {
  1599. const node = new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  1600. project_slug: '',
  1601. event_id: '',
  1602. });
  1603. node.value.errors = [makeTraceError()];
  1604. root.children.push(node);
  1605. }
  1606. expect(root.children.length).toBe(5);
  1607. TraceTree.AutogroupSiblingSpanNodes(root);
  1608. expect(root.children.length).toBe(1);
  1609. assertAutogroupedNode(root.children[0]);
  1610. expect(root.children[0].has_error).toBe(true);
  1611. expect(root.children[0].errored_children).toHaveLength(5);
  1612. });
  1613. it('adds autogrouped siblings as children under autogrouped node', () => {
  1614. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  1615. project_slug: '',
  1616. event_id: '',
  1617. });
  1618. for (let i = 0; i < 5; i++) {
  1619. root.children.push(
  1620. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  1621. project_slug: '',
  1622. event_id: '',
  1623. })
  1624. );
  1625. }
  1626. expect(root.children.length).toBe(5);
  1627. TraceTree.AutogroupSiblingSpanNodes(root);
  1628. expect(root.children.length).toBe(1);
  1629. const autoGroupedNode = root.children[0];
  1630. assertAutogroupedNode(autoGroupedNode);
  1631. expect(autoGroupedNode.groupCount).toBe(5);
  1632. expect(autoGroupedNode.children.length).toBe(5);
  1633. });
  1634. it('autogroups when number of children is > 5', () => {
  1635. const root = new TraceTreeNode(null, makeSpan({description: 'span1'}), {
  1636. project_slug: '',
  1637. event_id: '',
  1638. });
  1639. for (let i = 0; i < 7; i++) {
  1640. root.children.push(
  1641. new TraceTreeNode(root, makeSpan({description: 'span', op: 'db'}), {
  1642. project_slug: '',
  1643. event_id: '',
  1644. })
  1645. );
  1646. }
  1647. expect(root.children.length).toBe(7);
  1648. TraceTree.AutogroupSiblingSpanNodes(root);
  1649. expect(root.children.length).toBe(1);
  1650. });
  1651. it('autogroups direct children case', () => {
  1652. // db db db
  1653. // http -> parent autogroup (3) -> parent autogroup (3)
  1654. // http http
  1655. // http http
  1656. // http
  1657. const root: TraceTreeNode<TraceTree.Span> = new TraceTreeNode(
  1658. null,
  1659. makeSpan({
  1660. description: `span1`,
  1661. span_id: `1`,
  1662. op: 'db',
  1663. }),
  1664. {project_slug: '', event_id: ''}
  1665. );
  1666. let last: TraceTreeNode<any> = root;
  1667. for (let i = 0; i < 3; i++) {
  1668. const node = new TraceTreeNode(
  1669. last,
  1670. makeSpan({
  1671. description: `span${i}`,
  1672. span_id: `${i}`,
  1673. op: 'http',
  1674. }),
  1675. {
  1676. project_slug: '',
  1677. event_id: '',
  1678. }
  1679. );
  1680. last.children.push(node);
  1681. last = node;
  1682. }
  1683. if (!root) {
  1684. throw new Error('root is null');
  1685. }
  1686. expect(root.children.length).toBe(1);
  1687. expect(root.children[0].children.length).toBe(1);
  1688. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1689. expect(root.children.length).toBe(1);
  1690. assertAutogroupedNode(root.children[0]);
  1691. expect(root.children[0].children.length).toBe(0);
  1692. root.children[0].expanded = true;
  1693. expect((root.children[0].children[0].value as RawSpanType).description).toBe(
  1694. 'span0'
  1695. );
  1696. });
  1697. it('collects errored children for parent autogrouped node', () => {
  1698. // db db db
  1699. // http -> parent autogroup (3) -> parent autogroup (3)
  1700. // http http
  1701. // http http
  1702. // http
  1703. const root: TraceTreeNode<TraceTree.Span> = new TraceTreeNode(
  1704. null,
  1705. makeSpan({
  1706. description: `span1`,
  1707. span_id: `1`,
  1708. op: 'db',
  1709. }),
  1710. {project_slug: '', event_id: ''}
  1711. );
  1712. let last: TraceTreeNode<any> = root;
  1713. for (let i = 0; i < 3; i++) {
  1714. const node = new TraceTreeNode(
  1715. last,
  1716. makeSpan({
  1717. description: `span${i}`,
  1718. span_id: `${i}`,
  1719. op: 'http',
  1720. }),
  1721. {
  1722. project_slug: '',
  1723. event_id: '',
  1724. }
  1725. );
  1726. node.value.errors = [makeTraceError()];
  1727. last.children.push(node);
  1728. last = node;
  1729. }
  1730. if (!root) {
  1731. throw new Error('root is null');
  1732. }
  1733. expect(root.children.length).toBe(1);
  1734. expect(root.children[0].children.length).toBe(1);
  1735. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1736. expect(root.children.length).toBe(1);
  1737. assertAutogroupedNode(root.children[0]);
  1738. expect(root.children[0].has_error).toBe(true);
  1739. expect(root.children[0].errored_children).toHaveLength(1);
  1740. });
  1741. it('autogrouping direct children skips rendering intermediary nodes', () => {
  1742. // db db db
  1743. // http autogrouped (3) autogrouped (3)
  1744. // http -> db -> http
  1745. // http http
  1746. // db http
  1747. // db
  1748. const root = new TraceTreeNode(
  1749. null,
  1750. makeSpan({span_id: 'span1', description: 'span1', op: 'db'}),
  1751. {
  1752. project_slug: '',
  1753. event_id: '',
  1754. }
  1755. );
  1756. let last = root;
  1757. for (let i = 0; i < 4; i++) {
  1758. const node = new TraceTreeNode(
  1759. last,
  1760. makeSpan({
  1761. span_id: `span`,
  1762. description: `span`,
  1763. op: i === 3 ? 'db' : 'http',
  1764. }),
  1765. {
  1766. project_slug: '',
  1767. event_id: '',
  1768. }
  1769. );
  1770. last.children.push(node);
  1771. last = node;
  1772. }
  1773. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1774. const autoGroupedNode = root.children[0];
  1775. assertAutogroupedNode(autoGroupedNode);
  1776. expect(autoGroupedNode.children.length).toBe(1);
  1777. expect((autoGroupedNode.children[0].value as RawSpanType).op).toBe('db');
  1778. autoGroupedNode.expanded = true;
  1779. expect(autoGroupedNode.children.length).toBe(1);
  1780. expect((autoGroupedNode.children[0].value as RawSpanType).op).toBe('http');
  1781. });
  1782. it('nested direct autogrouping', () => {
  1783. // db db db
  1784. // http -> parent autogroup (3) -> parent autogroup (3)
  1785. // http db http
  1786. // http parent autogroup (3) http
  1787. // db http
  1788. // http db
  1789. // http parent autogrouped (3)
  1790. // http http
  1791. // http
  1792. // http
  1793. const root = new TraceTreeNode(
  1794. null,
  1795. makeSpan({span_id: 'span', description: 'span', op: 'db'}),
  1796. {
  1797. project_slug: '',
  1798. event_id: '',
  1799. }
  1800. );
  1801. let last = root;
  1802. for (let i = 0; i < 3; i++) {
  1803. if (i === 1) {
  1804. const autogroupBreakingSpan = new TraceTreeNode(
  1805. last,
  1806. makeSpan({span_id: 'span', description: 'span', op: 'db'}),
  1807. {
  1808. project_slug: '',
  1809. event_id: '',
  1810. }
  1811. );
  1812. last.children.push(autogroupBreakingSpan);
  1813. last = autogroupBreakingSpan;
  1814. } else {
  1815. for (let j = 0; j < 3; j++) {
  1816. const node = new TraceTreeNode(
  1817. last,
  1818. makeSpan({span_id: `span${j}`, description: `span${j}`, op: 'http'}),
  1819. {
  1820. project_slug: '',
  1821. event_id: '',
  1822. }
  1823. );
  1824. last.children.push(node);
  1825. last = node;
  1826. }
  1827. }
  1828. }
  1829. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1830. assertAutogroupedNode(root.children[0]);
  1831. assertAutogroupedNode(root.children[0].children[0].children[0]);
  1832. });
  1833. it('sibling autogrouping', () => {
  1834. // db db
  1835. // http sibling autogrouped (5)
  1836. // http
  1837. // http ->
  1838. // http
  1839. // http
  1840. const root = new TraceTreeNode(
  1841. null,
  1842. makeTransaction({start_timestamp: 0, timestamp: 10}),
  1843. {
  1844. project_slug: '',
  1845. event_id: '',
  1846. }
  1847. );
  1848. for (let i = 0; i < 5; i++) {
  1849. root.children.push(
  1850. new TraceTreeNode(root, makeSpan({start_timestamp: i, timestamp: i + 1}), {
  1851. project_slug: '',
  1852. event_id: '',
  1853. })
  1854. );
  1855. }
  1856. TraceTree.AutogroupSiblingSpanNodes(root);
  1857. expect(root.children.length).toBe(1);
  1858. assertAutogroupedNode(root.children[0]);
  1859. });
  1860. it('multiple sibling autogrouping', () => {
  1861. // db db
  1862. // http sibling autogrouped (5)
  1863. // http db
  1864. // http -> sibling autogrouped (5)
  1865. // http
  1866. // http
  1867. // db
  1868. // http
  1869. // http
  1870. // http
  1871. // http
  1872. // http
  1873. const root = new TraceTreeNode(
  1874. null,
  1875. makeTransaction({start_timestamp: 0, timestamp: 10}),
  1876. {
  1877. project_slug: '',
  1878. event_id: '',
  1879. }
  1880. );
  1881. for (let i = 0; i < 10; i++) {
  1882. if (i === 5) {
  1883. root.children.push(
  1884. new TraceTreeNode(
  1885. root,
  1886. makeSpan({start_timestamp: i, timestamp: i + 1, op: 'db'}),
  1887. {
  1888. project_slug: '',
  1889. event_id: '',
  1890. }
  1891. )
  1892. );
  1893. }
  1894. root.children.push(
  1895. new TraceTreeNode(
  1896. root,
  1897. makeSpan({start_timestamp: i, timestamp: i + 1, op: 'http'}),
  1898. {
  1899. project_slug: '',
  1900. event_id: '',
  1901. }
  1902. )
  1903. );
  1904. }
  1905. TraceTree.AutogroupSiblingSpanNodes(root);
  1906. assertAutogroupedNode(root.children[0]);
  1907. expect(root.children).toHaveLength(3);
  1908. assertAutogroupedNode(root.children[2]);
  1909. });
  1910. it('renders children of autogrouped direct children nodes', async () => {
  1911. const tree = TraceTree.FromTrace(
  1912. makeTrace({
  1913. transactions: [
  1914. makeTransaction({
  1915. transaction: '/',
  1916. project_slug: 'project',
  1917. event_id: 'event_id',
  1918. }),
  1919. ],
  1920. })
  1921. );
  1922. MockApiClient.addMockResponse({
  1923. url: '/organizations/org-slug/events/project:event_id/',
  1924. method: 'GET',
  1925. body: makeEvent({}, [
  1926. makeSpan({description: 'parent span', op: 'http', span_id: '1'}),
  1927. makeSpan({description: 'span', op: 'db', span_id: '2', parent_span_id: '1'}),
  1928. makeSpan({description: 'span', op: 'db', span_id: '3', parent_span_id: '2'}),
  1929. makeSpan({description: 'span', op: 'db', span_id: '4', parent_span_id: '3'}),
  1930. makeSpan({description: 'span', op: 'db', span_id: '5', parent_span_id: '4'}),
  1931. makeSpan({
  1932. description: 'span',
  1933. op: 'redis',
  1934. span_id: '6',
  1935. parent_span_id: '5',
  1936. }),
  1937. makeSpan({description: 'span', op: 'https', parent_span_id: '1'}),
  1938. ]),
  1939. });
  1940. expect(tree.list.length).toBe(2);
  1941. tree.zoomIn(tree.list[1], true, {
  1942. api: new MockApiClient(),
  1943. organization: OrganizationFixture(),
  1944. });
  1945. await waitFor(() => {
  1946. expect(tree.list.length).toBe(6);
  1947. });
  1948. const autogroupedNode = tree.list[tree.list.length - 3];
  1949. assertParentAutogroupedNode(autogroupedNode);
  1950. expect('autogrouped_by' in autogroupedNode?.value).toBeTruthy();
  1951. expect(autogroupedNode.groupCount).toBe(4);
  1952. expect(autogroupedNode.head.value.span_id).toBe('2');
  1953. expect(autogroupedNode.tail.value.span_id).toBe('5');
  1954. // Expand autogrouped node
  1955. expect(tree.expand(autogroupedNode, true)).toBe(true);
  1956. expect(tree.list.length).toBe(10);
  1957. // Collapse autogrouped node
  1958. expect(tree.expand(autogroupedNode, false)).toBe(true);
  1959. expect(tree.list.length).toBe(6);
  1960. expect(autogroupedNode.children[0].depth).toBe(4);
  1961. });
  1962. });
  1963. });