traceTree.spec.tsx 59 KB

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