traceTree.tsx 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435
  1. import type {Client} from 'sentry/api';
  2. import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
  3. import type {Organization} from 'sentry/types';
  4. import type {Event, EventTransaction, Measurement} from 'sentry/types/event';
  5. import type {
  6. TraceError as TraceErrorType,
  7. TraceErrorOrIssue,
  8. TraceFullDetailed,
  9. TraceSplitResults,
  10. } from 'sentry/utils/performance/quickTrace/types';
  11. import {isTraceError} from 'sentry/utils/performance/quickTrace/utils';
  12. import {TraceType} from '../traceDetails/newTraceDetailsContent';
  13. import {isRootTransaction} from '../traceDetails/utils';
  14. import {
  15. isAutogroupedNode,
  16. isMissingInstrumentationNode,
  17. isParentAutogroupedNode,
  18. isRootNode,
  19. isSiblingAutogroupedNode,
  20. isSpanNode,
  21. isTraceErrorNode,
  22. isTraceNode,
  23. isTransactionNode,
  24. shouldAddMissingInstrumentationSpan,
  25. } from './guards';
  26. /**
  27. *
  28. * This file implements the tree data structure that is used to represent a trace. We do
  29. * this both for performance reasons as well as flexibility. The requirement for a tree
  30. * is to support incremental patching and updates. This is important because we want to
  31. * be able to fetch more data as the user interacts with the tree, and we want to be able
  32. * efficiently update the tree as we receive more data.
  33. *
  34. * The trace is represented as a tree with different node value types (transaction or span)
  35. * Each tree node contains a reference to its parent and a list of references to its children,
  36. * as well as a reference to the value that the node holds. Each node also contains
  37. * some meta data and state about the node, such as if it is expanded or zoomed in. The benefit
  38. * of abstracting parts of the UI state is that the tree will persist user actions such as expanding
  39. * or collapsing nodes which would have otherwise been lost when individual nodes are remounted in the tree.
  40. *
  41. * Each tree holds a list reference, which is a live reference to a flattened representation
  42. * of the tree (used to render the tree in the UI). Since the list is mutable (and we want to keep it that way for performance
  43. * reasons as we want to support mutations on traces with ~100k+ nodes), callers need to manage reactivity themselves.
  44. *
  45. * An alternative, but not recommended approach is to call build() on the tree after each mutation,
  46. * which will iterate over all of the children and build a fresh list reference.
  47. *
  48. * In most cases, the initial tree is a list of transactions containing other transactions. Each transaction can
  49. * then be expanded into a list of spans which can also in some cases be expanded.
  50. *
  51. * - trace - trace
  52. * |- parent transaction --> when expanding |- parent transaction
  53. * |- child transaction |- span
  54. * |- span this used to be a transaction,
  55. * |- child transaction span <- but is now be a list of spans
  56. * |- span belonging to the transaction
  57. * this results in child txns to be lost,
  58. * which is a confusing user experience
  59. *
  60. * The tree supports autogrouping of spans vertically or as siblings. When that happens, a autogrouped node of either a vertical or
  61. * sibling type is inserted as an intermediary node. In the vertical case, the autogrouped node
  62. * holds the reference to the head and tail of the autogrouped sequence. In the sibling case, the autogrouped node
  63. * holds a reference to the children that are part of the autogrouped sequence. When expanding and collapsing these nodes,
  64. * the tree perform a reference swap to either point to the head (when expanded) or tail (when collapsed) of the autogrouped sequence.
  65. *
  66. * In vertical grouping case, the following happens:
  67. *
  68. * - root - root
  69. * - trace - trace
  70. * |- transaction |- transaction
  71. * |- span 1 <-| these become autogrouped |- autogrouped (head=span1, tail=span3, children points to children of tail)
  72. * |- span 2 |- as they are inserted into |- other span (parent points to autogrouped node)
  73. * |- span 3 <-| the tree.
  74. * |- other span
  75. *
  76. * When the autogrouped node is expanded the UI needs to show the entire collapsed chain, so we swap the tail children to point
  77. * back to the tail, and have autogrouped node point to it's head as the children.
  78. *
  79. * - root - root
  80. * - trace - trace
  81. * |- transaction |- transaction
  82. * |- autogrouped (head=span1, tail=span3) <- when expanding |- autogrouped (head=span1, tail=span3, children points to head)
  83. * | other span (paren points to autogrouped) |- span 1 (head)
  84. * |- span 2
  85. * |- span 3 (tail)
  86. * |- other span (children of tail, parent points to tail)
  87. *
  88. * Notes and improvements:
  89. * - collecting children should be O(n), it is currently O(n^2) as we are missing a proper queue implementation
  90. * - the notion of expanded and zoomed is confusing, they stand for the same idea from a UI pov
  91. * - there is an annoying thing wrt span and transaction nodes where we either store data on _children or _spanChildren
  92. * this is because we want to be able to store both transaction and span nodes in the same tree, but it makes for an
  93. * annoying API. A better design would have been to create an invisible meta node that just points to the correct children
  94. * - connector generation should live in the UI layer, not in the tree. Same with depth calculation. It is more convenient
  95. * to calculate this when rendering the tree, as we can only calculate it only for the visible nodes and avoid an extra tree pass
  96. * - instead of storing span children separately, we should have meta tree nodes that handle pointing to the correct children
  97. */
  98. export declare namespace TraceTree {
  99. type Transaction = TraceFullDetailed;
  100. type Span = RawSpanType & {
  101. childTxn: Transaction | undefined;
  102. event: EventTransaction;
  103. relatedErrors: TraceErrorOrIssue[];
  104. };
  105. type Trace = TraceSplitResults<Transaction>;
  106. type TraceError = TraceErrorType;
  107. interface MissingInstrumentationSpan {
  108. start_timestamp: number;
  109. timestamp: number;
  110. type: 'missing_instrumentation';
  111. }
  112. interface SiblingAutogroup extends RawSpanType {
  113. autogrouped_by: {
  114. description: string;
  115. op: string;
  116. };
  117. }
  118. interface ChildrenAutogroup {
  119. autogrouped_by: {
  120. op: string;
  121. };
  122. }
  123. type NodeValue =
  124. | Trace
  125. | Transaction
  126. | TraceError
  127. | Span
  128. | MissingInstrumentationSpan
  129. | SiblingAutogroup
  130. | ChildrenAutogroup
  131. | null;
  132. type NodePath = `${'txn' | 'span' | 'ag' | 'trace' | 'ms' | 'error'}:${string}`;
  133. type Metadata = {
  134. event_id: string | undefined;
  135. project_slug: string | undefined;
  136. };
  137. type Indicator = {
  138. duration: number;
  139. label: string;
  140. measurement: Measurement;
  141. start: number;
  142. type: 'cls' | 'fcp' | 'fp' | 'lcp' | 'ttfb';
  143. };
  144. }
  145. function cacheKey(organization: Organization, project_slug: string, event_id: string) {
  146. return organization.slug + ':' + project_slug + ':' + event_id;
  147. }
  148. function fetchTransactionSpans(
  149. api: Client,
  150. organization: Organization,
  151. project_slug: string,
  152. event_id: string
  153. ): Promise<EventTransaction> {
  154. return api.requestPromise(
  155. `/organizations/${organization.slug}/events/${project_slug}:${event_id}/`
  156. );
  157. }
  158. function measurementToTimestamp(
  159. start_timestamp: number,
  160. measurement: number,
  161. unit: string
  162. ) {
  163. if (unit === 'second') {
  164. return start_timestamp + measurement;
  165. }
  166. if (unit === 'millisecond') {
  167. return start_timestamp + measurement / 1e3;
  168. }
  169. if (unit === 'nanosecond') {
  170. return start_timestamp + measurement / 1e9;
  171. }
  172. throw new TypeError(`Unsupported measurement unit', ${unit}`);
  173. }
  174. function maybeInsertMissingInstrumentationSpan(
  175. parent: TraceTreeNode<TraceTree.NodeValue>,
  176. node: TraceTreeNode<TraceTree.Span>
  177. ) {
  178. const previousSpan = parent.spanChildren[parent.spanChildren.length - 1];
  179. if (!previousSpan || !isSpanNode(previousSpan)) {
  180. return;
  181. }
  182. if (node.value.start_timestamp - previousSpan.value.timestamp < 0.1) {
  183. return;
  184. }
  185. const missingInstrumentationSpan = new MissingInstrumentationNode(
  186. parent,
  187. {
  188. type: 'missing_instrumentation',
  189. start_timestamp: previousSpan.value.timestamp,
  190. timestamp: node.value.start_timestamp,
  191. },
  192. {
  193. event_id: undefined,
  194. project_slug: undefined,
  195. },
  196. previousSpan,
  197. node
  198. );
  199. parent.spanChildren.push(missingInstrumentationSpan);
  200. }
  201. // cls is not included as it is a cumulative layout shift and not a single point in time
  202. const RENDERABLE_MEASUREMENTS = ['fcp', 'fp', 'lcp', 'ttfb'];
  203. export class TraceTree {
  204. type: 'loading' | 'empty' | 'trace' = 'trace';
  205. root: TraceTreeNode<null> = TraceTreeNode.Root();
  206. indicators: TraceTree.Indicator[] = [];
  207. private _spanPromises: Map<string, Promise<Event>> = new Map();
  208. private _list: TraceTreeNode<TraceTree.NodeValue>[] = [];
  209. static Empty() {
  210. const tree = new TraceTree().build();
  211. tree.type = 'empty';
  212. return tree;
  213. }
  214. static Loading(metadata: TraceTree.Metadata): TraceTree {
  215. const tree = makeExampleTrace(metadata);
  216. tree.type = 'loading';
  217. return tree;
  218. }
  219. static FromTrace(trace: TraceTree.Trace, event?: EventTransaction): TraceTree {
  220. const tree = new TraceTree();
  221. let traceStart = Number.POSITIVE_INFINITY;
  222. let traceEnd = Number.NEGATIVE_INFINITY;
  223. function visit(
  224. parent: TraceTreeNode<TraceTree.NodeValue | null>,
  225. value: TraceTree.Transaction | TraceTree.TraceError
  226. ) {
  227. const node = new TraceTreeNode(parent, value, {
  228. project_slug: value && 'project_slug' in value ? value.project_slug : undefined,
  229. event_id: value && 'event_id' in value ? value.event_id : undefined,
  230. });
  231. node.canFetchData = true;
  232. if (parent) {
  233. parent.children.push(node as TraceTreeNode<TraceTree.NodeValue>);
  234. }
  235. if ('start_timestamp' in value && value.start_timestamp < traceStart) {
  236. traceStart = value.start_timestamp;
  237. }
  238. if ('timestamp' in value && typeof value.timestamp === 'number') {
  239. // Errors don't have 'start_timestamp', so we adjust traceStart
  240. // with an errors 'timestamp'
  241. if (isTraceError(value)) {
  242. traceStart = Math.min(value.timestamp, traceStart);
  243. }
  244. traceEnd = Math.max(value.timestamp, traceEnd);
  245. }
  246. if (value && 'children' in value) {
  247. for (const child of value.children) {
  248. visit(node, child);
  249. }
  250. }
  251. return node;
  252. }
  253. const traceNode = new TraceTreeNode(tree.root, trace, {
  254. event_id: undefined,
  255. project_slug: undefined,
  256. });
  257. // Trace is always expanded by default
  258. tree.root.children.push(traceNode);
  259. for (const transaction of trace.transactions) {
  260. visit(traceNode, transaction);
  261. }
  262. for (const trace_error of trace.orphan_errors) {
  263. visit(traceNode, trace_error);
  264. }
  265. if (event?.measurements) {
  266. const indicators = tree
  267. .collectMeasurements(traceStart, event.measurements)
  268. .sort((a, b) => a.start - b.start);
  269. for (const indicator of indicators) {
  270. if (indicator.start > traceEnd) {
  271. traceEnd = indicator.start;
  272. }
  273. indicator.start *= traceNode.multiplier;
  274. }
  275. tree.indicators = indicators;
  276. }
  277. traceNode.space = [
  278. traceStart * traceNode.multiplier,
  279. (traceEnd - traceStart) * traceNode.multiplier,
  280. ];
  281. tree.root.space = [
  282. traceStart * traceNode.multiplier,
  283. (traceEnd - traceStart) * traceNode.multiplier,
  284. ];
  285. return tree.build();
  286. }
  287. static GetTraceType(root: TraceTreeNode<null>): TraceType {
  288. const trace = root.children[0];
  289. if (!trace || !isTraceNode(trace)) {
  290. throw new TypeError('Not trace node');
  291. }
  292. const {transactions, orphan_errors} = trace.value;
  293. const {roots, orphans} = (transactions ?? []).reduce(
  294. (counts, transaction) => {
  295. if (isRootTransaction(transaction)) {
  296. counts.roots++;
  297. } else {
  298. counts.orphans++;
  299. }
  300. return counts;
  301. },
  302. {roots: 0, orphans: 0}
  303. );
  304. if (roots === 0) {
  305. if (orphans > 0) {
  306. return TraceType.NO_ROOT;
  307. }
  308. if (orphan_errors && orphan_errors.length > 0) {
  309. return TraceType.ONLY_ERRORS;
  310. }
  311. return TraceType.EMPTY_TRACE;
  312. }
  313. if (roots === 1) {
  314. if (orphans > 0) {
  315. return TraceType.BROKEN_SUBTRACES;
  316. }
  317. return TraceType.ONE_ROOT;
  318. }
  319. if (roots > 1) {
  320. return TraceType.MULTIPLE_ROOTS;
  321. }
  322. throw new Error('Unknown trace type');
  323. }
  324. static FromSpans(
  325. parent: TraceTreeNode<TraceTree.NodeValue>,
  326. data: Event,
  327. spans: RawSpanType[],
  328. options: {sdk: string | undefined} | undefined
  329. ): TraceTreeNode<TraceTree.NodeValue> {
  330. parent.invalidate(parent);
  331. const platformHasMissingSpans = shouldAddMissingInstrumentationSpan(options?.sdk);
  332. const parentIsSpan = isSpanNode(parent);
  333. const lookuptable: Record<
  334. RawSpanType['span_id'],
  335. TraceTreeNode<TraceTree.Span | TraceTree.Transaction>
  336. > = {};
  337. if (parent.spanChildren.length > 0) {
  338. parent.zoomedIn = true;
  339. return parent;
  340. }
  341. if (parentIsSpan) {
  342. if (parent.value && 'span_id' in parent.value) {
  343. lookuptable[parent.value.span_id] = parent as TraceTreeNode<TraceTree.Span>;
  344. }
  345. }
  346. const transactionsToSpanMap = new Map<string, TraceTreeNode<TraceTree.Transaction>>();
  347. for (const child of parent.children) {
  348. if (
  349. isTransactionNode(child) &&
  350. 'parent_span_id' in child.value &&
  351. typeof child.value.parent_span_id === 'string'
  352. ) {
  353. transactionsToSpanMap.set(child.value.parent_span_id, child);
  354. }
  355. continue;
  356. }
  357. for (const span of spans) {
  358. const childTxn = transactionsToSpanMap.get(span.span_id);
  359. const spanNodeValue: TraceTree.Span = {
  360. ...span,
  361. event: data as EventTransaction,
  362. relatedErrors: childTxn ? getRelatedErrorsOrIssues(span, childTxn.value) : [],
  363. childTxn: childTxn?.value,
  364. };
  365. const node: TraceTreeNode<TraceTree.Span> = new TraceTreeNode(null, spanNodeValue, {
  366. event_id: undefined,
  367. project_slug: undefined,
  368. });
  369. // This is the case where the current span is the parent of a txn at the
  370. // trace level. When zooming into the parent of the txn, we want to place a copy
  371. // of the txn as a child of the parenting span.
  372. if (childTxn) {
  373. const clonedChildTxn =
  374. childTxn.cloneDeep() as unknown as TraceTreeNode<TraceTree.Span>;
  375. node.spanChildren.push(clonedChildTxn);
  376. clonedChildTxn.parent = node;
  377. }
  378. lookuptable[span.span_id] = node;
  379. if (span.parent_span_id) {
  380. const spanParentNode = lookuptable[span.parent_span_id];
  381. if (spanParentNode) {
  382. node.parent = spanParentNode;
  383. if (platformHasMissingSpans) {
  384. maybeInsertMissingInstrumentationSpan(spanParentNode, node);
  385. }
  386. spanParentNode.spanChildren.push(node);
  387. continue;
  388. }
  389. }
  390. if (platformHasMissingSpans) {
  391. maybeInsertMissingInstrumentationSpan(parent, node);
  392. }
  393. parent.spanChildren.push(node);
  394. node.parent = parent;
  395. }
  396. parent.zoomedIn = true;
  397. TraceTree.AutogroupSiblingSpanNodes(parent);
  398. TraceTree.AutogroupDirectChildrenSpanNodes(parent);
  399. return parent;
  400. }
  401. static AutogroupDirectChildrenSpanNodes(
  402. root: TraceTreeNode<TraceTree.NodeValue>
  403. ): void {
  404. const queue = [root];
  405. while (queue.length > 0) {
  406. const node = queue.pop()!;
  407. if (node.children.length > 1 || !isSpanNode(node)) {
  408. for (const child of node.children) {
  409. queue.push(child);
  410. }
  411. continue;
  412. }
  413. const head = node;
  414. let tail = node;
  415. let groupMatchCount = 0;
  416. while (
  417. tail &&
  418. tail.children.length === 1 &&
  419. isSpanNode(tail.children[0]) &&
  420. tail.children[0].value.op === head.value.op
  421. ) {
  422. groupMatchCount++;
  423. tail = tail.children[0];
  424. }
  425. if (groupMatchCount < 1) {
  426. for (const child of head.children) {
  427. queue.push(child);
  428. }
  429. continue;
  430. }
  431. const autoGroupedNode = new ParentAutogroupNode(
  432. node.parent,
  433. {
  434. ...head.value,
  435. autogrouped_by: {
  436. op: head.value && 'op' in head.value ? head.value.op ?? '' : '',
  437. },
  438. },
  439. {
  440. event_id: undefined,
  441. project_slug: undefined,
  442. },
  443. head,
  444. tail
  445. );
  446. if (!node.parent) {
  447. throw new Error('Parent node is missing, this should be unreachable code');
  448. }
  449. autoGroupedNode.groupCount = groupMatchCount + 1;
  450. autoGroupedNode.space = [
  451. head.value.start_timestamp * autoGroupedNode.multiplier,
  452. (tail.value.timestamp - head.value.start_timestamp) * autoGroupedNode.multiplier,
  453. ];
  454. for (const c of tail.children) {
  455. c.parent = autoGroupedNode;
  456. queue.push(c);
  457. }
  458. const index = node.parent.children.indexOf(node);
  459. node.parent.children[index] = autoGroupedNode;
  460. }
  461. }
  462. static AutogroupSiblingSpanNodes(root: TraceTreeNode<TraceTree.NodeValue>): void {
  463. const queue = [root];
  464. while (queue.length > 0) {
  465. const node = queue.pop()!;
  466. if (node.children.length < 5) {
  467. for (const child of node.children) {
  468. queue.push(child);
  469. }
  470. continue;
  471. }
  472. let index = 0;
  473. let matchCount = 0;
  474. while (index < node.children.length) {
  475. const current = node.children[index] as TraceTreeNode<TraceTree.Span>;
  476. const next = node.children[index + 1] as TraceTreeNode<TraceTree.Span>;
  477. if (
  478. next &&
  479. next.children.length === 0 &&
  480. current.children.length === 0 &&
  481. next.value.op === current.value.op &&
  482. next.value.description === current.value.description
  483. ) {
  484. matchCount++;
  485. // If the next node is the last node in the list, we keep iterating
  486. if (index + 1 < node.children.length) {
  487. index++;
  488. continue;
  489. }
  490. }
  491. if (matchCount >= 4) {
  492. const autoGroupedNode = new SiblingAutogroupNode(
  493. node,
  494. {
  495. ...current.value,
  496. autogrouped_by: {
  497. op: current.value.op ?? '',
  498. description: current.value.description ?? '',
  499. },
  500. },
  501. {
  502. event_id: undefined,
  503. project_slug: undefined,
  504. }
  505. );
  506. autoGroupedNode.groupCount = matchCount + 1;
  507. const start = index - matchCount;
  508. for (let j = start; j < start + matchCount + 1; j++) {
  509. autoGroupedNode.children.push(node.children[j]);
  510. autoGroupedNode.children[autoGroupedNode.children.length - 1].parent =
  511. autoGroupedNode;
  512. }
  513. node.children.splice(start, matchCount + 1, autoGroupedNode);
  514. index = start + 1;
  515. matchCount = 0;
  516. } else {
  517. index++;
  518. matchCount = 0;
  519. }
  520. }
  521. }
  522. }
  523. collectMeasurements(
  524. start_timestamp: number,
  525. measurements: Record<string, Measurement>
  526. ): TraceTree.Indicator[] {
  527. const indicators: TraceTree.Indicator[] = [];
  528. for (const measurement of RENDERABLE_MEASUREMENTS) {
  529. const value = measurements[measurement];
  530. if (!value) {
  531. continue;
  532. }
  533. const timestamp = measurementToTimestamp(
  534. start_timestamp,
  535. value.value,
  536. value.unit ?? 'milliseconds'
  537. );
  538. indicators.push({
  539. start: timestamp,
  540. duration: 0,
  541. measurement: value,
  542. type: measurement as TraceTree.Indicator['type'],
  543. label: measurement.toUpperCase(),
  544. });
  545. }
  546. return indicators;
  547. }
  548. // Returns boolean to indicate if node was updated
  549. expand(node: TraceTreeNode<TraceTree.NodeValue>, expanded: boolean): boolean {
  550. if (expanded === node.expanded) {
  551. return false;
  552. }
  553. // Expanding is not allowed for zoomed in nodes
  554. if (node.zoomedIn) {
  555. return false;
  556. }
  557. if (node instanceof ParentAutogroupNode) {
  558. // In parent autogrouping, we perform a node swap and either point the
  559. // head or tails of the autogrouped sequence to the autogrouped node
  560. if (node.expanded) {
  561. const index = this._list.indexOf(node);
  562. const autogroupedChildren = node.getVisibleChildren();
  563. this._list.splice(index + 1, autogroupedChildren.length);
  564. const newChildren = node.tail.getVisibleChildren();
  565. for (const c of node.tail.children) {
  566. c.parent = node;
  567. }
  568. this._list.splice(index + 1, 0, ...newChildren);
  569. } else {
  570. node.head.parent = node;
  571. const index = this._list.indexOf(node);
  572. const childrenCount = node.getVisibleChildrenCount();
  573. this._list.splice(index + 1, childrenCount);
  574. node.getVisibleChildrenCount();
  575. const newChildren = [node.head].concat(
  576. node.head.getVisibleChildren() as TraceTreeNode<TraceTree.Span>[]
  577. );
  578. for (const c of node.children) {
  579. c.parent = node.tail;
  580. }
  581. this._list.splice(index + 1, 0, ...newChildren);
  582. }
  583. node.invalidate(node);
  584. node.expanded = expanded;
  585. return true;
  586. }
  587. if (node.expanded) {
  588. const index = this._list.indexOf(node);
  589. this._list.splice(index + 1, node.getVisibleChildrenCount());
  590. // Flip expanded after collecting visible children
  591. node.expanded = expanded;
  592. } else {
  593. const index = this._list.indexOf(node);
  594. // Flip expanded so that we can collect visible children
  595. node.expanded = expanded;
  596. this._list.splice(index + 1, 0, ...node.getVisibleChildren());
  597. }
  598. node.expanded = expanded;
  599. return true;
  600. }
  601. zoomIn(
  602. node: TraceTreeNode<TraceTree.NodeValue>,
  603. zoomedIn: boolean,
  604. options: {
  605. api: Client;
  606. organization: Organization;
  607. }
  608. ): Promise<Event | null> {
  609. if (zoomedIn === node.zoomedIn) {
  610. return Promise.resolve(null);
  611. }
  612. if (!zoomedIn) {
  613. const index = this._list.indexOf(node);
  614. const childrenCount = node.getVisibleChildrenCount();
  615. this._list.splice(index + 1, childrenCount);
  616. node.zoomedIn = zoomedIn;
  617. node.invalidate(node);
  618. if (node.expanded) {
  619. this._list.splice(index + 1, 0, ...node.getVisibleChildren());
  620. }
  621. return Promise.resolve(null);
  622. }
  623. const key = cacheKey(
  624. options.organization,
  625. node.metadata.project_slug!,
  626. node.metadata.event_id!
  627. );
  628. const promise =
  629. this._spanPromises.get(key) ??
  630. fetchTransactionSpans(
  631. options.api,
  632. options.organization,
  633. node.metadata.project_slug!,
  634. node.metadata.event_id!
  635. );
  636. promise.then(data => {
  637. const spans = data.entries.find(s => s.type === 'spans');
  638. if (!spans) {
  639. return data;
  640. }
  641. // Remove existing entries from the list
  642. const index = this._list.indexOf(node);
  643. if (node.expanded) {
  644. const childrenCount = node.getVisibleChildrenCount();
  645. this._list.splice(index + 1, childrenCount);
  646. }
  647. // Api response is not sorted
  648. if (spans.data) {
  649. spans.data.sort((a, b) => a.start_timestamp - b.start_timestamp);
  650. }
  651. TraceTree.FromSpans(node, data, spans.data, {sdk: data.sdk?.name});
  652. const spanChildren = node.getVisibleChildren();
  653. this._list.splice(index + 1, 0, ...spanChildren);
  654. return data;
  655. });
  656. this._spanPromises.set(key, promise);
  657. return promise;
  658. }
  659. toList(): TraceTreeNode<TraceTree.NodeValue>[] {
  660. const list: TraceTreeNode<TraceTree.NodeValue>[] = [];
  661. function visit(node: TraceTreeNode<TraceTree.NodeValue>) {
  662. list.push(node);
  663. if (!node.expanded) {
  664. return;
  665. }
  666. for (const child of node.children) {
  667. visit(child);
  668. }
  669. }
  670. for (const child of this.root.children) {
  671. visit(child);
  672. }
  673. return list;
  674. }
  675. get list(): ReadonlyArray<TraceTreeNode<TraceTree.NodeValue>> {
  676. return this._list;
  677. }
  678. /**
  679. * Prints the tree in a human readable format, useful for debugging and testing
  680. */
  681. print() {
  682. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  683. const print = this.list
  684. .map(t => printNode(t, 0))
  685. .filter(Boolean)
  686. .join('\n');
  687. // eslint-disable-next-line no-console
  688. console.log(print);
  689. }
  690. build() {
  691. this._list = this.toList();
  692. return this;
  693. }
  694. }
  695. export class TraceTreeNode<T extends TraceTree.NodeValue> {
  696. parent: TraceTreeNode<TraceTree.NodeValue> | null = null;
  697. value: T;
  698. expanded: boolean = false;
  699. zoomedIn: boolean = false;
  700. canFetchData: boolean = false;
  701. metadata: TraceTree.Metadata = {
  702. project_slug: undefined,
  703. event_id: undefined,
  704. };
  705. space: [number, number] | null = null;
  706. multiplier: number;
  707. private unit: 'milliseconds' = 'milliseconds';
  708. private _depth: number | undefined;
  709. private _children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  710. private _spanChildren: TraceTreeNode<
  711. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  712. >[] = [];
  713. private _connectors: number[] | undefined = undefined;
  714. constructor(
  715. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  716. value: T,
  717. metadata: TraceTree.Metadata
  718. ) {
  719. this.parent = parent ?? null;
  720. this.value = value;
  721. this.metadata = metadata;
  722. this.multiplier = this.unit === 'milliseconds' ? 1e3 : 1;
  723. if (value && 'timestamp' in value && 'start_timestamp' in value) {
  724. this.space = [
  725. value.start_timestamp * this.multiplier,
  726. (value.timestamp - value.start_timestamp) * this.multiplier,
  727. ];
  728. }
  729. if (isTransactionNode(this) || isTraceNode(this) || isSpanNode(this)) {
  730. this.expanded = true;
  731. }
  732. }
  733. cloneDeep(): TraceTreeNode<T> | ParentAutogroupNode | SiblingAutogroupNode {
  734. let node: TraceTreeNode<T> | ParentAutogroupNode | SiblingAutogroupNode;
  735. if (isParentAutogroupedNode(this)) {
  736. node = new ParentAutogroupNode(
  737. this.parent,
  738. this.value,
  739. this.metadata,
  740. this.head,
  741. this.tail
  742. );
  743. node.groupCount = this.groupCount;
  744. } else {
  745. node = new TraceTreeNode(this.parent, this.value, this.metadata);
  746. }
  747. if (!node) {
  748. throw new Error('CloneDeep is not implemented');
  749. }
  750. node.expanded = this.expanded;
  751. node.zoomedIn = this.zoomedIn;
  752. node.canFetchData = this.canFetchData;
  753. node.space = this.space;
  754. node.metadata = this.metadata;
  755. if (isParentAutogroupedNode(node)) {
  756. node.head = node.head.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  757. node.tail = node.tail.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  758. for (const child of node.head.children) {
  759. child.parent = node;
  760. }
  761. for (const child of node.tail.children) {
  762. child.parent = node;
  763. }
  764. node.head.parent = node;
  765. node.tail.parent = node;
  766. } else {
  767. for (const child of this.children) {
  768. const childClone = child.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  769. node.children.push(childClone);
  770. childClone.parent = node;
  771. }
  772. }
  773. return node;
  774. }
  775. get isOrphaned() {
  776. return this.parent?.value && 'orphan_errors' in this.parent.value;
  777. }
  778. get isLastChild() {
  779. if (!this.parent || this.parent.children.length === 0) {
  780. return true;
  781. }
  782. return this.parent.children[this.parent.children.length - 1] === this;
  783. }
  784. /**
  785. * Return a lazily calculated depth of the node in the tree.
  786. * Root node has a value of -1 as it is abstract.
  787. */
  788. get depth(): number {
  789. if (typeof this._depth === 'number') {
  790. return this._depth;
  791. }
  792. let depth = -2;
  793. let node: TraceTreeNode<any> | null = this;
  794. while (node) {
  795. if (typeof node.parent?.depth === 'number') {
  796. this._depth = node.parent.depth + 1;
  797. return this._depth;
  798. }
  799. depth++;
  800. node = node.parent;
  801. }
  802. this._depth = depth;
  803. return this._depth;
  804. }
  805. /**
  806. * Returns the depth levels at which the row should draw vertical connectors
  807. * negative values mean connector points to an orphaned node
  808. */
  809. get connectors(): number[] {
  810. if (this._connectors !== undefined) {
  811. return this._connectors!;
  812. }
  813. this._connectors = [];
  814. if (!this.parent) {
  815. return this._connectors;
  816. }
  817. if (this.parent?.connectors !== undefined) {
  818. this._connectors = [...this.parent.connectors];
  819. if (this.isLastChild || this.value === null) {
  820. return this._connectors;
  821. }
  822. this.connectors.push(this.isOrphaned ? -this.depth : this.depth);
  823. return this._connectors;
  824. }
  825. let node: TraceTreeNode<T> | TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
  826. while (node) {
  827. if (node.value === null) {
  828. break;
  829. }
  830. if (node.isLastChild) {
  831. node = node.parent;
  832. continue;
  833. }
  834. this._connectors.push(node.isOrphaned ? -node.depth : node.depth);
  835. node = node.parent;
  836. }
  837. return this._connectors;
  838. }
  839. /**
  840. * Returns the children that the node currently points to.
  841. * The logic here is a consequence of the tree design, where we want to be able to store
  842. * both transaction and span nodes in the same tree. This results in an annoying API where
  843. * we either store span children separately or transaction children separately. A better design
  844. * would have been to create an invisible meta node that always points to the correct children.
  845. */
  846. get children(): TraceTreeNode<TraceTree.NodeValue>[] {
  847. if (isAutogroupedNode(this)) {
  848. return this._children;
  849. }
  850. if (isSpanNode(this)) {
  851. return this.canFetchData && !this.zoomedIn ? [] : this.spanChildren;
  852. }
  853. if (isTransactionNode(this)) {
  854. return this.zoomedIn ? this._spanChildren : this._children;
  855. }
  856. return this._children;
  857. }
  858. set children(children: TraceTreeNode<TraceTree.NodeValue>[]) {
  859. this._children = children;
  860. }
  861. get spanChildren(): TraceTreeNode<
  862. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  863. >[] {
  864. return this._spanChildren;
  865. }
  866. /**
  867. * Invalidate the visual data used to render the tree, forcing it
  868. * to be recalculated on the next render. This is useful when for example
  869. * the tree is expanded or collapsed, or when the tree is mutated and
  870. * the visual data is no longer valid as the indentation changes
  871. */
  872. invalidate(root?: TraceTreeNode<TraceTree.NodeValue>) {
  873. this._connectors = undefined;
  874. this._depth = undefined;
  875. if (root) {
  876. const queue = [...this.children];
  877. if (isParentAutogroupedNode(this)) {
  878. queue.push(this.head);
  879. }
  880. while (queue.length > 0) {
  881. const next = queue.pop()!;
  882. next.invalidate();
  883. if (isParentAutogroupedNode(next)) {
  884. queue.push(next.head);
  885. }
  886. for (let i = 0; i < next.children.length; i++) {
  887. queue.push(next.children[i]);
  888. }
  889. }
  890. }
  891. }
  892. getVisibleChildrenCount(): number {
  893. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  894. let count = 0;
  895. if (
  896. this.expanded ||
  897. isParentAutogroupedNode(this) ||
  898. isMissingInstrumentationNode(this)
  899. ) {
  900. for (let i = this.children.length - 1; i >= 0; i--) {
  901. stack.push(this.children[i]);
  902. }
  903. }
  904. while (stack.length > 0) {
  905. const node = stack.pop()!;
  906. count++;
  907. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  908. // to ensure they are processed in the original left-to-right order.
  909. if (node.expanded || isParentAutogroupedNode(node)) {
  910. for (let i = node.children.length - 1; i >= 0; i--) {
  911. stack.push(node.children[i]);
  912. }
  913. }
  914. }
  915. return count;
  916. }
  917. getVisibleChildren(): TraceTreeNode<TraceTree.NodeValue>[] {
  918. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  919. const children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  920. if (
  921. this.expanded ||
  922. isParentAutogroupedNode(this) ||
  923. isMissingInstrumentationNode(this)
  924. ) {
  925. for (let i = this.children.length - 1; i >= 0; i--) {
  926. stack.push(this.children[i]);
  927. }
  928. }
  929. while (stack.length > 0) {
  930. const node = stack.pop()!;
  931. children.push(node);
  932. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  933. // to ensure they are processed in the original left-to-right order.
  934. if (node.expanded || isParentAutogroupedNode(node)) {
  935. for (let i = node.children.length - 1; i >= 0; i--) {
  936. stack.push(node.children[i]);
  937. }
  938. }
  939. }
  940. return children;
  941. }
  942. // Returns the min path required to reach the node from the root.
  943. // @TODO: skip nodes that do not require fetching
  944. get path(): TraceTree.NodePath[] {
  945. const nodes: TraceTreeNode<TraceTree.NodeValue>[] = [this];
  946. let current: TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
  947. if (isSpanNode(this) || isAutogroupedNode(this)) {
  948. while (
  949. current &&
  950. (isSpanNode(current) || (isAutogroupedNode(current) && !current.expanded))
  951. ) {
  952. current = current.parent;
  953. }
  954. }
  955. while (current) {
  956. if (isTransactionNode(current)) {
  957. nodes.push(current);
  958. }
  959. if (isSpanNode(current)) {
  960. nodes.push(current);
  961. while (current.parent) {
  962. if (isTransactionNode(current.parent)) {
  963. break;
  964. }
  965. if (isAutogroupedNode(current.parent) && current.parent.expanded) {
  966. break;
  967. }
  968. current = current.parent;
  969. }
  970. }
  971. if (isAutogroupedNode(current)) {
  972. nodes.push(current);
  973. }
  974. current = current.parent;
  975. }
  976. return nodes.map(nodeToId);
  977. }
  978. print() {
  979. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  980. const offset = this.depth === -1 ? 1 : 0;
  981. const nodes = [this, ...this.getVisibleChildren()];
  982. const print = nodes
  983. .map(t => printNode(t, offset))
  984. .filter(Boolean)
  985. .join('\n');
  986. // eslint-disable-next-line no-console
  987. console.log(print);
  988. }
  989. static Find(
  990. root: TraceTreeNode<TraceTree.NodeValue>,
  991. predicate: (node: TraceTreeNode<TraceTree.NodeValue>) => boolean
  992. ): TraceTreeNode<TraceTree.NodeValue> | null {
  993. const queue = [root];
  994. while (queue.length > 0) {
  995. const next = queue.pop()!;
  996. if (predicate(next)) return next;
  997. for (const child of next.children) {
  998. queue.push(child);
  999. }
  1000. }
  1001. return null;
  1002. }
  1003. static Root() {
  1004. return new TraceTreeNode(null, null, {
  1005. event_id: undefined,
  1006. project_slug: undefined,
  1007. });
  1008. }
  1009. }
  1010. export class MissingInstrumentationNode extends TraceTreeNode<TraceTree.MissingInstrumentationSpan> {
  1011. next: TraceTreeNode<TraceTree.Span>;
  1012. previous: TraceTreeNode<TraceTree.Span>;
  1013. constructor(
  1014. parent: TraceTreeNode<TraceTree.NodeValue>,
  1015. node: TraceTree.MissingInstrumentationSpan,
  1016. metadata: TraceTree.Metadata,
  1017. previous: TraceTreeNode<TraceTree.Span>,
  1018. next: TraceTreeNode<TraceTree.Span>
  1019. ) {
  1020. super(parent, node, metadata);
  1021. this.next = next;
  1022. this.previous = previous;
  1023. }
  1024. }
  1025. export class ParentAutogroupNode extends TraceTreeNode<TraceTree.ChildrenAutogroup> {
  1026. head: TraceTreeNode<TraceTree.Span>;
  1027. tail: TraceTreeNode<TraceTree.Span>;
  1028. groupCount: number = 0;
  1029. constructor(
  1030. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  1031. node: TraceTree.ChildrenAutogroup,
  1032. metadata: TraceTree.Metadata,
  1033. head: TraceTreeNode<TraceTree.Span>,
  1034. tail: TraceTreeNode<TraceTree.Span>
  1035. ) {
  1036. super(parent, node, metadata);
  1037. this.expanded = false;
  1038. this.head = head;
  1039. this.tail = tail;
  1040. }
  1041. get children() {
  1042. if (this.expanded) {
  1043. return [this.head];
  1044. }
  1045. return this.tail.children;
  1046. }
  1047. }
  1048. export class SiblingAutogroupNode extends TraceTreeNode<TraceTree.SiblingAutogroup> {
  1049. groupCount: number = 0;
  1050. constructor(
  1051. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  1052. node: TraceTree.SiblingAutogroup,
  1053. metadata: TraceTree.Metadata
  1054. ) {
  1055. super(parent, node, metadata);
  1056. this.expanded = false;
  1057. }
  1058. }
  1059. function partialTransaction(
  1060. partial: Partial<TraceTree.Transaction>
  1061. ): TraceTree.Transaction {
  1062. return {
  1063. start_timestamp: 0,
  1064. timestamp: 0,
  1065. errors: [],
  1066. performance_issues: [],
  1067. parent_span_id: '',
  1068. span_id: '',
  1069. parent_event_id: '',
  1070. project_id: 0,
  1071. 'transaction.duration': 0,
  1072. 'transaction.op': 'db',
  1073. 'transaction.status': 'ok',
  1074. generation: 0,
  1075. project_slug: '',
  1076. event_id: `event_id`,
  1077. transaction: `transaction`,
  1078. children: [],
  1079. ...partial,
  1080. };
  1081. }
  1082. export function makeExampleTrace(metadata: TraceTree.Metadata): TraceTree {
  1083. const trace: TraceTree.Trace = {
  1084. transactions: [],
  1085. orphan_errors: [],
  1086. };
  1087. function randomBetween(min: number, max: number) {
  1088. return Math.floor(Math.random() * (max - min + 1) + min);
  1089. }
  1090. let start = new Date().getTime();
  1091. const root = partialTransaction({
  1092. ...metadata,
  1093. generation: 0,
  1094. start_timestamp: start,
  1095. transaction: 'root transaction',
  1096. timestamp: start + randomBetween(100, 200),
  1097. });
  1098. trace.transactions.push(root);
  1099. for (let i = 0; i < 25; i++) {
  1100. const end = start + randomBetween(100, 200);
  1101. const nest = i > 0 && Math.random() > 0.33;
  1102. if (nest) {
  1103. const parent = root.children[root.children.length - 1];
  1104. parent.children.push(
  1105. partialTransaction({
  1106. ...metadata,
  1107. generation: 0,
  1108. start_timestamp: start,
  1109. transaction: `parent transaction ${i}`,
  1110. timestamp: end,
  1111. })
  1112. );
  1113. parent.timestamp = end;
  1114. } else {
  1115. root.children.push(
  1116. partialTransaction({
  1117. ...metadata,
  1118. generation: 0,
  1119. start_timestamp: start,
  1120. transaction: 'loading...',
  1121. ['transaction.op']: 'loading',
  1122. timestamp: end,
  1123. })
  1124. );
  1125. }
  1126. start = end;
  1127. }
  1128. const tree = TraceTree.FromTrace(trace);
  1129. return tree;
  1130. }
  1131. function nodeToId(n: TraceTreeNode<TraceTree.NodeValue>): TraceTree.NodePath {
  1132. if (isTransactionNode(n)) {
  1133. return `txn:${n.value.event_id}`;
  1134. }
  1135. if (isSpanNode(n)) {
  1136. return `span:${n.value.span_id}`;
  1137. }
  1138. if (isAutogroupedNode(n)) {
  1139. if (isParentAutogroupedNode(n)) {
  1140. return `ag:${n.head.value.span_id}`;
  1141. }
  1142. if (isSiblingAutogroupedNode(n)) {
  1143. const child = n.children[0];
  1144. if (isSpanNode(child)) {
  1145. return `ag:${child.value.span_id}`;
  1146. }
  1147. }
  1148. }
  1149. if (isTraceNode(n)) {
  1150. return `trace:root`;
  1151. }
  1152. if (isTraceErrorNode(n)) {
  1153. return `error:${n.value.event_id}`;
  1154. }
  1155. if (isRootNode(n)) {
  1156. throw new Error('A path to root node does not exist as the node is virtual');
  1157. }
  1158. if (isMissingInstrumentationNode(n)) {
  1159. if (n.previous) {
  1160. return `ms:${n.previous.value.span_id}`;
  1161. }
  1162. if (n.next) {
  1163. return `ms:${n.next.value.span_id}`;
  1164. }
  1165. throw new Error('Missing instrumentation node must have a previous or next node');
  1166. }
  1167. throw new Error('Not implemented');
  1168. }
  1169. function getRelatedErrorsOrIssues(
  1170. span: RawSpanType,
  1171. currentEvent: TraceTree.Transaction
  1172. ): TraceErrorOrIssue[] {
  1173. const performanceIssues = currentEvent.performance_issues.filter(
  1174. issue =>
  1175. issue.span.some(id => id === span.span_id) ||
  1176. issue.suspect_spans.some(suspectSpanId => suspectSpanId === span.span_id)
  1177. );
  1178. return [
  1179. ...currentEvent.errors.filter(error => error.span === span.span_id),
  1180. ...performanceIssues, // Spans can be shown when embedded in performance issues
  1181. ];
  1182. }
  1183. function printNode(t: TraceTreeNode<TraceTree.NodeValue>, offset: number): string {
  1184. // +1 because we may be printing from the root which is -1 indexed
  1185. const padding = ' '.repeat(t.depth + offset);
  1186. if (isAutogroupedNode(t)) {
  1187. if (isParentAutogroupedNode(t)) {
  1188. return padding + `parent autogroup (${t.groupCount})`;
  1189. }
  1190. if (isSiblingAutogroupedNode(t)) {
  1191. return padding + `sibling autogroup (${t.groupCount})`;
  1192. }
  1193. return padding + 'autogroup';
  1194. }
  1195. if (isSpanNode(t)) {
  1196. return padding + (t.value.op || t.value.span_id || 'unknown span');
  1197. }
  1198. if (isTransactionNode(t)) {
  1199. return padding + (t.value.transaction || 'unknown transaction');
  1200. }
  1201. if (isMissingInstrumentationNode(t)) {
  1202. return padding + 'missing_instrumentation';
  1203. }
  1204. if (isRootNode(t)) {
  1205. return padding + 'Root';
  1206. }
  1207. if (isTraceNode(t)) {
  1208. return padding + 'Trace';
  1209. }
  1210. if (isTraceErrorNode(t)) {
  1211. return padding + t.value.event_id || 'unknown trace error';
  1212. }
  1213. return 'unknown node';
  1214. }