traceTree.tsx 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099
  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} from 'sentry/types/event';
  5. import type {
  6. TraceError as TraceErrorType,
  7. TraceFullDetailed,
  8. TraceSplitResults,
  9. } from 'sentry/utils/performance/quickTrace/types';
  10. import {TraceType} from '../traceDetails/newTraceDetailsContent';
  11. import {isRootTransaction} from '../traceDetails/utils';
  12. import {
  13. isAutogroupedNode,
  14. isMissingInstrumentationNode,
  15. isParentAutogroupedNode,
  16. isRootNode,
  17. isSiblingAutogroupedNode,
  18. isSpanNode,
  19. isTraceNode,
  20. isTransactionNode,
  21. } from './guards';
  22. /**
  23. *
  24. * This file implements the tree data structure that is used to represent a trace. We do
  25. * this both for performance reasons as well as flexibility. The requirement for a tree
  26. * is to support incremental patching and updates. This is important because we want to
  27. * be able to fetch more data as the user interacts with the tree, and we want to be able
  28. * efficiently update the tree as we receive more data.
  29. *
  30. * The trace is represented as a tree with different node value types (transaction or span)
  31. * Each tree node contains a reference to its parent and a list of references to its children,
  32. * as well as a reference to the value that the node holds. Each node also contains
  33. * some meta data and state about the node, such as if it is expanded or zoomed in. The benefit
  34. * of abstracting parts of the UI state is that the tree will persist user actions such as expanding
  35. * or collapsing nodes which would have otherwise been lost when individual nodes are remounted in the tree.
  36. *
  37. * Each tree holds a list reference, which is a live reference to a flattened representation
  38. * 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
  39. * reasons as we want to support mutations on traces with ~100k+ nodes), callers need to manage reactivity themselves.
  40. *
  41. * An alternative, but not recommended approach is to call build() on the tree after each mutation,
  42. * which will iterate over all of the children and build a fresh list reference.
  43. *
  44. * In most cases, the initial tree is a list of transactions containing other transactions. Each transaction can
  45. * then be expanded into a list of spans which can also in some cases be expanded.
  46. *
  47. * - trace - trace
  48. * |- parent transaction --> when expanding |- parent transaction
  49. * |- child transaction |- span
  50. * |- span this used to be a transaction,
  51. * |- child transaction span <- but is now be a list of spans
  52. * |- span belonging to the transaction
  53. * this results in child txns to be lost,
  54. * which is a confusing user experience
  55. *
  56. * The tree supports autogrouping of spans vertically or as siblings. When that happens, a autogrouped node of either a vertical or
  57. * sibling type is inserted as an intermediary node. In the vertical case, the autogrouped node
  58. * holds the reference to the head and tail of the autogrouped sequence. In the sibling case, the autogrouped node
  59. * holds a reference to the children that are part of the autogrouped sequence. When expanding and collapsing these nodes,
  60. * the tree perform a reference swap to either point to the head (when expanded) or tail (when collapsed) of the autogrouped sequence.
  61. *
  62. * In vertical grouping case, the following happens:
  63. *
  64. * - root - root
  65. * - trace - trace
  66. * |- transaction |- transaction
  67. * |- span 1 <-| these become autogrouped |- autogrouped (head=span1, tail=span3, children points to children of tail)
  68. * |- span 2 |- as they are inserted into |- other span (parent points to autogrouped node)
  69. * |- span 3 <-| the tree.
  70. * |- other span
  71. *
  72. * When the autogrouped node is expanded the UI needs to show the entire collapsed chain, so we swap the tail children to point
  73. * back to the tail, and have autogrouped node point to it's head as the children.
  74. *
  75. * - root - root
  76. * - trace - trace
  77. * |- transaction |- transaction
  78. * |- autogrouped (head=span1, tail=span3) <- when expanding |- autogrouped (head=span1, tail=span3, children points to head)
  79. * | other span (paren points to autogrouped) |- span 1 (head)
  80. * |- span 2
  81. * |- span 3 (tail)
  82. * |- other span (children of tail, parent points to tail)
  83. *
  84. * Notes and improvements:
  85. * - collecting children should be O(n), it is currently O(n^2) as we are missing a proper queue implementation
  86. * - the notion of expanded and zoomed is confusing, they stand for the same idea from a UI pov
  87. * - there is an annoying thing wrt span and transaction nodes where we either store data on _children or _spanChildren
  88. * this is because we want to be able to store both transaction and span nodes in the same tree, but it makes for an
  89. * annoying API. A better design would have been to create an invisible meta node that just points to the correct children
  90. * - connector generation should live in the UI layer, not in the tree. Same with depth calculation. It is more convenient
  91. * to calculate this when rendering the tree, as we can only calculate it only for the visible nodes and avoid an extra tree pass
  92. * - instead of storing span children separately, we should have meta tree nodes that handle pointing to the correct children
  93. */
  94. export declare namespace TraceTree {
  95. type Transaction = TraceFullDetailed;
  96. type Span = RawSpanType;
  97. type Trace = TraceSplitResults<Transaction>;
  98. type TraceError = TraceErrorType;
  99. interface MissingInstrumentationSpan {
  100. start_timestamp: number;
  101. timestamp: number;
  102. type: 'missing_instrumentation';
  103. }
  104. interface SiblingAutogroup extends RawSpanType {
  105. autogrouped_by: {
  106. description: string;
  107. op: string;
  108. };
  109. }
  110. interface ChildrenAutogroup {
  111. autogrouped_by: {
  112. op: string;
  113. };
  114. }
  115. type NodeValue =
  116. | Trace
  117. | Transaction
  118. | TraceError
  119. | Span
  120. | MissingInstrumentationSpan
  121. | SiblingAutogroup
  122. | ChildrenAutogroup
  123. | null;
  124. type Metadata = {
  125. event_id: string | undefined;
  126. project_slug: string | undefined;
  127. };
  128. }
  129. function fetchTransactionSpans(
  130. api: Client,
  131. organization: Organization,
  132. project_slug: string,
  133. event_id: string
  134. ): Promise<EventTransaction> {
  135. return api.requestPromise(
  136. `/organizations/${organization.slug}/events/${project_slug}:${event_id}/`
  137. );
  138. }
  139. function maybeInsertMissingInstrumentationSpan(
  140. parent: TraceTreeNode<TraceTree.NodeValue>,
  141. node: TraceTreeNode<TraceTree.Span>
  142. ) {
  143. const lastInsertedSpan = parent.spanChildren[parent.spanChildren.length - 1];
  144. if (!lastInsertedSpan) {
  145. return;
  146. }
  147. if (node.value.start_timestamp - lastInsertedSpan.value.timestamp < 0.1) {
  148. return;
  149. }
  150. const missingInstrumentationSpan =
  151. new TraceTreeNode<TraceTree.MissingInstrumentationSpan>(
  152. parent,
  153. {
  154. type: 'missing_instrumentation',
  155. start_timestamp: lastInsertedSpan.value.timestamp,
  156. timestamp: node.value.start_timestamp,
  157. },
  158. {
  159. event_id: undefined,
  160. project_slug: undefined,
  161. }
  162. );
  163. parent.spanChildren.push(missingInstrumentationSpan);
  164. }
  165. export class TraceTree {
  166. type: 'loading' | 'trace' = 'trace';
  167. root: TraceTreeNode<null> = TraceTreeNode.Root();
  168. private _spanPromises: Map<TraceTreeNode<TraceTree.NodeValue>, Promise<Event>> =
  169. new Map();
  170. private _list: TraceTreeNode<TraceTree.NodeValue>[] = [];
  171. static Empty() {
  172. const tree = new TraceTree().build();
  173. tree.type = 'trace';
  174. return tree;
  175. }
  176. static Loading(metadata: TraceTree.Metadata): TraceTree {
  177. const tree = makeExampleTrace(metadata);
  178. tree.type = 'loading';
  179. return tree;
  180. }
  181. static FromTrace(trace: TraceTree.Trace): TraceTree {
  182. const tree = new TraceTree();
  183. let traceStart = Number.POSITIVE_INFINITY;
  184. let traceEnd = Number.NEGATIVE_INFINITY;
  185. function visit(
  186. parent: TraceTreeNode<TraceTree.NodeValue | null>,
  187. value: TraceTree.Transaction | TraceTree.TraceError
  188. ) {
  189. const node = new TraceTreeNode(parent, value, {
  190. project_slug: value && 'project_slug' in value ? value.project_slug : undefined,
  191. event_id: value && 'event_id' in value ? value.event_id : undefined,
  192. });
  193. node.canFetchData = true;
  194. if (parent) {
  195. parent.children.push(node as TraceTreeNode<TraceTree.NodeValue>);
  196. }
  197. if ('start_timestamp' in value && 'timestamp' in value) {
  198. if (value.start_timestamp < traceStart) {
  199. traceStart = value.start_timestamp;
  200. }
  201. if (value.timestamp > traceEnd) {
  202. traceEnd = value.timestamp;
  203. }
  204. } else if ('timestamp' in value && typeof value.timestamp === 'number') {
  205. if (value.timestamp > traceEnd) {
  206. traceEnd = value.timestamp;
  207. }
  208. }
  209. if (value && 'children' in value) {
  210. for (const child of value.children) {
  211. visit(node, child);
  212. }
  213. }
  214. return node;
  215. }
  216. const traceNode = new TraceTreeNode(tree.root, trace, {
  217. event_id: undefined,
  218. project_slug: undefined,
  219. });
  220. // Trace is always expanded by default
  221. tree.root.children.push(traceNode);
  222. for (const transaction of trace.transactions) {
  223. visit(traceNode, transaction);
  224. }
  225. for (const trace_error of trace.orphan_errors) {
  226. visit(traceNode, trace_error);
  227. }
  228. traceNode.space = [traceStart, traceEnd - traceStart];
  229. tree.root.space = [traceStart, traceEnd - traceStart];
  230. return tree.build();
  231. }
  232. static GetTraceType(root: TraceTreeNode<null>): TraceType {
  233. const trace = root.children[0];
  234. if (!trace || !isTraceNode(trace)) {
  235. throw new TypeError('Not trace node');
  236. }
  237. const {transactions, orphan_errors} = trace.value;
  238. const {roots, orphans} = (transactions ?? []).reduce(
  239. (counts, transaction) => {
  240. if (isRootTransaction(transaction)) {
  241. counts.roots++;
  242. } else {
  243. counts.orphans++;
  244. }
  245. return counts;
  246. },
  247. {roots: 0, orphans: 0}
  248. );
  249. if (roots === 0) {
  250. if (orphans > 0) {
  251. return TraceType.NO_ROOT;
  252. }
  253. if (orphan_errors && orphan_errors.length > 0) {
  254. return TraceType.ONLY_ERRORS;
  255. }
  256. return TraceType.EMPTY_TRACE;
  257. }
  258. if (roots === 1) {
  259. if (orphans > 0) {
  260. return TraceType.BROKEN_SUBTRACES;
  261. }
  262. return TraceType.ONE_ROOT;
  263. }
  264. if (roots > 1) {
  265. return TraceType.MULTIPLE_ROOTS;
  266. }
  267. throw new Error('Unknown trace type');
  268. }
  269. static FromSpans(
  270. parent: TraceTreeNode<TraceTree.NodeValue>,
  271. spans: RawSpanType[]
  272. ): TraceTreeNode<TraceTree.NodeValue> {
  273. const parentIsSpan = isSpanNode(parent);
  274. const lookuptable: Record<
  275. RawSpanType['span_id'],
  276. TraceTreeNode<TraceTree.Span | TraceTree.Transaction>
  277. > = {};
  278. if (parent.spanChildren.length > 0) {
  279. parent.zoomedIn = true;
  280. return parent;
  281. }
  282. if (parentIsSpan) {
  283. if (parent.value && 'span_id' in parent.value) {
  284. lookuptable[parent.value.span_id] = parent as TraceTreeNode<TraceTree.Span>;
  285. }
  286. }
  287. const transactionsToSpanMap = new Map<string, TraceTreeNode<TraceTree.Transaction>>();
  288. for (const child of parent.children) {
  289. if (
  290. isTransactionNode(child) &&
  291. 'parent_span_id' in child.value &&
  292. typeof child.value.parent_span_id === 'string'
  293. ) {
  294. transactionsToSpanMap.set(child.value.parent_span_id, child);
  295. }
  296. continue;
  297. }
  298. for (const span of spans) {
  299. const parentNode = transactionsToSpanMap.get(span.span_id);
  300. let node: TraceTreeNode<TraceTree.Span>;
  301. if (parentNode) {
  302. node = parentNode.clone() as unknown as TraceTreeNode<TraceTree.Span>;
  303. } else {
  304. node = new TraceTreeNode(null, span, {
  305. event_id: undefined,
  306. project_slug: undefined,
  307. });
  308. }
  309. node.canFetchData = !!parentNode;
  310. if (parentNode) {
  311. node.metadata = parentNode.metadata;
  312. }
  313. lookuptable[span.span_id] = node;
  314. if (span.parent_span_id) {
  315. const spanParentNode = lookuptable[span.parent_span_id];
  316. if (spanParentNode) {
  317. node.parent = spanParentNode;
  318. maybeInsertMissingInstrumentationSpan(spanParentNode, node);
  319. spanParentNode.spanChildren.push(node);
  320. continue;
  321. }
  322. }
  323. // Orphaned span
  324. maybeInsertMissingInstrumentationSpan(parent, node);
  325. parent.spanChildren.push(node);
  326. node.parent = parent as TraceTreeNode<TraceTree.Span>;
  327. }
  328. parent.zoomedIn = true;
  329. TraceTree.AutogroupSiblingSpanNodes(parent);
  330. TraceTree.AutogroupDirectChildrenSpanNodes(parent);
  331. return parent;
  332. }
  333. get list(): ReadonlyArray<TraceTreeNode<TraceTree.NodeValue>> {
  334. return this._list;
  335. }
  336. static AutogroupDirectChildrenSpanNodes(
  337. root: TraceTreeNode<TraceTree.NodeValue>
  338. ): void {
  339. const queue = [root];
  340. while (queue.length > 0) {
  341. const node = queue.pop()!;
  342. if (node.children.length > 1 || !isSpanNode(node)) {
  343. for (const child of node.children) {
  344. queue.push(child);
  345. }
  346. continue;
  347. }
  348. const head = node;
  349. let tail = node;
  350. let groupMatchCount = 0;
  351. while (
  352. tail &&
  353. tail.children.length === 1 &&
  354. isSpanNode(tail.children[0]) &&
  355. tail.children[0].value.op === head.value.op
  356. ) {
  357. groupMatchCount++;
  358. tail = tail.children[0];
  359. }
  360. if (groupMatchCount < 1) {
  361. for (const child of head.children) {
  362. queue.push(child);
  363. }
  364. continue;
  365. }
  366. const autoGroupedNode = new ParentAutogroupNode(
  367. node.parent,
  368. {
  369. ...head.value,
  370. autogrouped_by: {
  371. op: head.value && 'op' in head.value ? head.value.op ?? '' : '',
  372. },
  373. },
  374. {
  375. event_id: undefined,
  376. project_slug: undefined,
  377. },
  378. head,
  379. tail
  380. );
  381. if (!node.parent) {
  382. throw new Error('Parent node is missing, this should be unreachable code');
  383. }
  384. autoGroupedNode.groupCount = groupMatchCount + 1;
  385. autoGroupedNode.space = [
  386. head.value.start_timestamp,
  387. tail.value.timestamp - head.value.start_timestamp,
  388. ];
  389. for (const c of tail.children) {
  390. c.parent = autoGroupedNode;
  391. queue.push(c);
  392. }
  393. const index = node.parent.children.indexOf(node);
  394. node.parent.children[index] = autoGroupedNode;
  395. }
  396. }
  397. static AutogroupSiblingSpanNodes(root: TraceTreeNode<TraceTree.NodeValue>): void {
  398. const queue = [root];
  399. while (queue.length > 0) {
  400. const node = queue.pop()!;
  401. if (node.children.length < 5) {
  402. for (const child of node.children) {
  403. queue.push(child);
  404. }
  405. continue;
  406. }
  407. let index = 0;
  408. let matchCount = 0;
  409. while (index < node.children.length) {
  410. const current = node.children[index] as TraceTreeNode<TraceTree.Span>;
  411. const next = node.children[index + 1] as TraceTreeNode<TraceTree.Span>;
  412. if (
  413. next &&
  414. next.children.length === 0 &&
  415. current.children.length === 0 &&
  416. next.value.op === current.value.op &&
  417. next.value.description === current.value.description
  418. ) {
  419. matchCount++;
  420. // If the next node is the last node in the list, we keep iterating
  421. if (index + 1 < node.children.length) {
  422. index++;
  423. continue;
  424. }
  425. }
  426. if (matchCount >= 4) {
  427. const autoGroupedNode = new SiblingAutogroupNode(
  428. node,
  429. {
  430. ...current.value,
  431. autogrouped_by: {
  432. op: current.value.op ?? '',
  433. description: current.value.description ?? '',
  434. },
  435. },
  436. {
  437. event_id: undefined,
  438. project_slug: undefined,
  439. }
  440. );
  441. autoGroupedNode.groupCount = matchCount + 1;
  442. const start = index - matchCount;
  443. for (let j = start; j < index - 1; j++) {
  444. autoGroupedNode.children.push(node.children[j]);
  445. autoGroupedNode.children[autoGroupedNode.children.length - 1].parent =
  446. autoGroupedNode;
  447. }
  448. node.children.splice(start, matchCount + 1, autoGroupedNode);
  449. index = start + 1;
  450. matchCount = 0;
  451. } else {
  452. index++;
  453. matchCount = 0;
  454. }
  455. }
  456. }
  457. }
  458. // Returns boolean to indicate if node was updated
  459. expand(node: TraceTreeNode<TraceTree.NodeValue>, expanded: boolean): boolean {
  460. if (expanded === node.expanded) {
  461. return false;
  462. }
  463. // Expanding is not allowed for zoomed in nodes
  464. if (node.zoomedIn) {
  465. return false;
  466. }
  467. if (node instanceof ParentAutogroupNode) {
  468. // In parent autogrouping, we perform a node swap and either point the
  469. // head or tails of the autogrouped sequence to the autogrouped node
  470. if (node.expanded) {
  471. const index = this._list.indexOf(node);
  472. const autogroupedChildren = node.getVisibleChildren();
  473. this._list.splice(index + 1, autogroupedChildren.length);
  474. const newChildren = node.tail.getVisibleChildren();
  475. for (const c of node.tail.children) {
  476. c.parent = node;
  477. }
  478. this._list.splice(index + 1, 0, ...newChildren);
  479. } else {
  480. node.head.parent = node;
  481. const index = this._list.indexOf(node);
  482. const childrenCount = node.getVisibleChildrenCount();
  483. this._list.splice(index + 1, childrenCount);
  484. node.getVisibleChildrenCount();
  485. const newChildren = [node.head].concat(
  486. node.head.getVisibleChildren() as TraceTreeNode<TraceTree.Span>[]
  487. );
  488. for (const c of node.children) {
  489. c.parent = node.tail;
  490. }
  491. this._list.splice(index + 1, 0, ...newChildren);
  492. }
  493. node.invalidate(node);
  494. node.expanded = expanded;
  495. return true;
  496. }
  497. if (node.expanded) {
  498. const index = this._list.indexOf(node);
  499. this._list.splice(index + 1, node.getVisibleChildrenCount());
  500. // Flip expanded after collecting visible children
  501. node.expanded = expanded;
  502. } else {
  503. const index = this._list.indexOf(node);
  504. // Flip expanded so that we can collect visible children
  505. node.expanded = expanded;
  506. this._list.splice(index + 1, 0, ...node.getVisibleChildren());
  507. }
  508. node.expanded = expanded;
  509. return true;
  510. }
  511. zoomIn(
  512. node: TraceTreeNode<TraceTree.NodeValue>,
  513. zoomedIn: boolean,
  514. options: {
  515. api: Client;
  516. organization: Organization;
  517. }
  518. ): Promise<Event | null> {
  519. if (zoomedIn === node.zoomedIn) {
  520. return Promise.resolve(null);
  521. }
  522. if (!zoomedIn) {
  523. const index = this._list.indexOf(node);
  524. const childrenCount = node.getVisibleChildrenCount();
  525. this._list.splice(index + 1, childrenCount);
  526. node.zoomedIn = zoomedIn;
  527. if (node.expanded) {
  528. this._list.splice(index + 1, 0, ...node.getVisibleChildren());
  529. }
  530. return Promise.resolve(null);
  531. }
  532. const promise =
  533. this._spanPromises.get(node) ??
  534. fetchTransactionSpans(
  535. options.api,
  536. options.organization,
  537. node.metadata.project_slug!,
  538. node.metadata.event_id!
  539. );
  540. promise.then(data => {
  541. const spans = data.entries.find(s => s.type === 'spans');
  542. if (!spans) {
  543. return data;
  544. }
  545. // Remove existing entries from the list
  546. const index = this._list.indexOf(node);
  547. if (node.expanded) {
  548. const childrenCount = node.getVisibleChildrenCount();
  549. this._list.splice(index + 1, childrenCount);
  550. }
  551. // Api response is not sorted
  552. if (spans.data) {
  553. spans.data.sort((a, b) => a.start_timestamp - b.start_timestamp);
  554. }
  555. TraceTree.FromSpans(node, spans.data);
  556. const spanChildren = node.getVisibleChildren();
  557. this._list.splice(index + 1, 0, ...spanChildren);
  558. return data;
  559. });
  560. this._spanPromises.set(node, promise);
  561. return promise;
  562. }
  563. toList(): TraceTreeNode<TraceTree.NodeValue>[] {
  564. const list: TraceTreeNode<TraceTree.NodeValue>[] = [];
  565. function visit(node: TraceTreeNode<TraceTree.NodeValue>) {
  566. list.push(node);
  567. if (!node.expanded) {
  568. return;
  569. }
  570. for (const child of node.children) {
  571. visit(child);
  572. }
  573. }
  574. for (const child of this.root.children) {
  575. visit(child);
  576. }
  577. return list;
  578. }
  579. /**
  580. * Prints the tree in a human readable format, useful for debugging and testing
  581. */
  582. print() {
  583. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  584. const print = this.list
  585. .map(t => printNode(t, 0))
  586. .filter(Boolean)
  587. .join('\n');
  588. // eslint-disable-next-line no-console
  589. console.log(print);
  590. }
  591. build() {
  592. this._list = this.toList();
  593. return this;
  594. }
  595. }
  596. export class TraceTreeNode<T extends TraceTree.NodeValue> {
  597. parent: TraceTreeNode<TraceTree.NodeValue> | null = null;
  598. value: T;
  599. expanded: boolean = false;
  600. zoomedIn: boolean = false;
  601. canFetchData: boolean = false;
  602. metadata: TraceTree.Metadata = {
  603. project_slug: undefined,
  604. event_id: undefined,
  605. };
  606. space: [number, number] | null = null;
  607. private _depth: number | undefined;
  608. private _children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  609. private _spanChildren: TraceTreeNode<
  610. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  611. >[] = [];
  612. private _connectors: number[] | undefined = undefined;
  613. constructor(
  614. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  615. value: T,
  616. metadata: TraceTree.Metadata
  617. ) {
  618. this.parent = parent ?? null;
  619. this.value = value;
  620. this.metadata = metadata;
  621. if (value && 'timestamp' in value && 'start_timestamp' in value) {
  622. this.space = [value.start_timestamp, value.timestamp - value.start_timestamp];
  623. }
  624. if (isTransactionNode(this) || isTraceNode(this) || isSpanNode(this)) {
  625. this.expanded = true;
  626. }
  627. }
  628. clone(): TraceTreeNode<T> {
  629. const node = new TraceTreeNode(this.parent, this.value, this.metadata);
  630. node.expanded = this.expanded;
  631. node.zoomedIn = this.zoomedIn;
  632. node.canFetchData = this.canFetchData;
  633. node.space = this.space;
  634. node.children = this.children;
  635. return node;
  636. }
  637. get isOrphaned() {
  638. return this.parent?.value && 'orphan_errors' in this.parent.value;
  639. }
  640. get isLastChild() {
  641. return this.parent?.children[this.parent.children.length - 1] === this;
  642. }
  643. /**
  644. * Return a lazily calculated depth of the node in the tree.
  645. * Root node has a value of -1 as it is abstract.
  646. */
  647. get depth(): number {
  648. if (typeof this._depth === 'number') {
  649. return this._depth;
  650. }
  651. let depth = -2;
  652. let node: TraceTreeNode<any> | null = this;
  653. while (node) {
  654. if (typeof node.parent?.depth === 'number') {
  655. this._depth = node.parent.depth + 1;
  656. return this._depth;
  657. }
  658. depth++;
  659. node = node.parent;
  660. }
  661. this._depth = depth;
  662. return this._depth;
  663. }
  664. /**
  665. * Returns the depth levels at which the row should draw vertical connectors
  666. * negative values mean connector points to an orphaned node
  667. */
  668. get connectors(): number[] {
  669. if (this._connectors !== undefined) {
  670. return this._connectors!;
  671. }
  672. this._connectors = [];
  673. if (this.parent?.connectors !== undefined) {
  674. this._connectors = [...this.parent.connectors];
  675. if (this.isLastChild || this.value === null) {
  676. return this._connectors;
  677. }
  678. this.connectors.push(this.isOrphaned ? -this.depth : this.depth);
  679. return this._connectors;
  680. }
  681. let node: TraceTreeNode<T> | TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
  682. while (node) {
  683. if (node.value === null) {
  684. break;
  685. }
  686. if (node.isLastChild) {
  687. node = node.parent;
  688. continue;
  689. }
  690. this._connectors.push(node.isOrphaned ? -node.depth : node.depth);
  691. node = node.parent;
  692. }
  693. return this._connectors;
  694. }
  695. /**
  696. * Returns the children that the node currently points to.
  697. * The logic here is a consequence of the tree design, where we want to be able to store
  698. * both transaction and span nodes in the same tree. This results in an annoying API where
  699. * we either store span children separately or transaction children separately. A better design
  700. * would have been to create an invisible meta node that always points to the correct children.
  701. */
  702. get children(): TraceTreeNode<TraceTree.NodeValue>[] {
  703. if (isAutogroupedNode(this)) {
  704. return this._children;
  705. }
  706. if (isSpanNode(this)) {
  707. return this.canFetchData && !this.zoomedIn ? [] : this.spanChildren;
  708. }
  709. // if a node is zoomed in, return span children, else return transaction children
  710. return this.zoomedIn ? this._spanChildren : this._children;
  711. }
  712. set children(children: TraceTreeNode<TraceTree.NodeValue>[]) {
  713. this._children = children;
  714. }
  715. get spanChildren(): TraceTreeNode<
  716. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  717. >[] {
  718. return this._spanChildren;
  719. }
  720. /**
  721. * Invalidate the visual data used to render the tree, forcing it
  722. * to be recalculated on the next render. This is useful when for example
  723. * the tree is expanded or collapsed, or when the tree is mutated and
  724. * the visual data is no longer valid as the indentation changes
  725. */
  726. invalidate(root?: TraceTreeNode<TraceTree.NodeValue>) {
  727. this._connectors = undefined;
  728. this._depth = undefined;
  729. if (root) {
  730. const queue = [...this.children];
  731. while (queue.length > 0) {
  732. const next = queue.pop()!;
  733. next.invalidate();
  734. for (let i = 0; i < next.children.length; i++) {
  735. queue.push(next.children[i]);
  736. }
  737. }
  738. }
  739. }
  740. getVisibleChildrenCount(): number {
  741. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  742. let count = 0;
  743. if (
  744. this.expanded ||
  745. isParentAutogroupedNode(this) ||
  746. isMissingInstrumentationNode(this)
  747. ) {
  748. for (let i = this.children.length - 1; i >= 0; i--) {
  749. stack.push(this.children[i]);
  750. }
  751. }
  752. while (stack.length > 0) {
  753. const node = stack.pop()!;
  754. count++;
  755. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  756. // to ensure they are processed in the original left-to-right order.
  757. if (node.expanded || isParentAutogroupedNode(node)) {
  758. for (let i = node.children.length - 1; i >= 0; i--) {
  759. stack.push(node.children[i]);
  760. }
  761. }
  762. }
  763. return count;
  764. }
  765. getVisibleChildren(): TraceTreeNode<TraceTree.NodeValue>[] {
  766. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  767. const children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  768. if (
  769. this.expanded ||
  770. isParentAutogroupedNode(this) ||
  771. isMissingInstrumentationNode(this)
  772. ) {
  773. for (let i = this.children.length - 1; i >= 0; i--) {
  774. stack.push(this.children[i]);
  775. }
  776. }
  777. while (stack.length > 0) {
  778. const node = stack.pop()!;
  779. children.push(node);
  780. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  781. // to ensure they are processed in the original left-to-right order.
  782. if (node.expanded || isParentAutogroupedNode(node)) {
  783. for (let i = node.children.length - 1; i >= 0; i--) {
  784. stack.push(node.children[i]);
  785. }
  786. }
  787. }
  788. return children;
  789. }
  790. print() {
  791. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  792. const offset = this.depth === -1 ? 1 : 0;
  793. const nodes = [this, ...this.getVisibleChildren()];
  794. const print = nodes
  795. .map(t => printNode(t, offset))
  796. .filter(Boolean)
  797. .join('\n');
  798. // eslint-disable-next-line no-console
  799. console.log(print);
  800. }
  801. static Root() {
  802. return new TraceTreeNode(null, null, {
  803. event_id: undefined,
  804. project_slug: undefined,
  805. });
  806. }
  807. }
  808. export class ParentAutogroupNode extends TraceTreeNode<TraceTree.ChildrenAutogroup> {
  809. head: TraceTreeNode<TraceTree.Span>;
  810. tail: TraceTreeNode<TraceTree.Span>;
  811. groupCount: number = 0;
  812. constructor(
  813. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  814. node: TraceTree.ChildrenAutogroup,
  815. metadata: TraceTree.Metadata,
  816. head: TraceTreeNode<TraceTree.Span>,
  817. tail: TraceTreeNode<TraceTree.Span>
  818. ) {
  819. super(parent, node, metadata);
  820. this.head = head;
  821. this.tail = tail;
  822. }
  823. get children() {
  824. if (this.expanded) {
  825. return [this.head];
  826. }
  827. return this.tail.children;
  828. }
  829. }
  830. export class SiblingAutogroupNode extends TraceTreeNode<TraceTree.SiblingAutogroup> {
  831. groupCount: number = 0;
  832. constructor(
  833. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  834. node: TraceTree.SiblingAutogroup,
  835. metadata: TraceTree.Metadata
  836. ) {
  837. super(parent, node, metadata);
  838. }
  839. }
  840. function partialTransaction(
  841. partial: Partial<TraceTree.Transaction>
  842. ): TraceTree.Transaction {
  843. return {
  844. start_timestamp: 0,
  845. timestamp: 0,
  846. errors: [],
  847. performance_issues: [],
  848. parent_span_id: '',
  849. span_id: '',
  850. parent_event_id: '',
  851. project_id: 0,
  852. 'transaction.duration': 0,
  853. 'transaction.op': 'db',
  854. 'transaction.status': 'ok',
  855. generation: 0,
  856. project_slug: '',
  857. event_id: `event_id`,
  858. transaction: `transaction`,
  859. children: [],
  860. ...partial,
  861. };
  862. }
  863. export function makeExampleTrace(metadata: TraceTree.Metadata): TraceTree {
  864. const trace: TraceTree.Trace = {
  865. transactions: [],
  866. orphan_errors: [],
  867. };
  868. function randomBetween(min: number, max: number) {
  869. return Math.floor(Math.random() * (max - min + 1) + min);
  870. }
  871. let start = new Date().getTime();
  872. for (let i = 0; i < 25; i++) {
  873. const end = start + randomBetween(100, 200);
  874. const nest = i > 0 && Math.random() > 0.5;
  875. if (nest) {
  876. const parent = trace.transactions[trace.transactions.length - 1];
  877. parent.children.push(
  878. partialTransaction({
  879. ...metadata,
  880. generation: 0,
  881. start_timestamp: start,
  882. transaction: `parent transaction ${i}`,
  883. timestamp: end,
  884. })
  885. );
  886. parent.timestamp = end;
  887. } else {
  888. trace.transactions.push(
  889. partialTransaction({
  890. ...metadata,
  891. generation: 0,
  892. start_timestamp: start,
  893. transaction: 'loading...',
  894. ['transaction.op']: 'loading',
  895. timestamp: end,
  896. })
  897. );
  898. }
  899. start = end;
  900. }
  901. const tree = TraceTree.FromTrace(trace);
  902. return tree;
  903. }
  904. function printNode(t: TraceTreeNode<TraceTree.NodeValue>, offset: number): string {
  905. // +1 because we may be printing from the root which is -1 indexed
  906. const padding = ' '.repeat(t.depth + offset);
  907. if (isAutogroupedNode(t)) {
  908. if (isParentAutogroupedNode(t)) {
  909. return padding + `parent autogroup (${t.groupCount})`;
  910. }
  911. if (isSiblingAutogroupedNode(t)) {
  912. return padding + `sibling autogroup (${t.groupCount})`;
  913. }
  914. return padding + 'autogroup';
  915. }
  916. if (isSpanNode(t)) {
  917. return padding + t.value?.op ?? 'unknown span op';
  918. }
  919. if (isTransactionNode(t)) {
  920. return padding + t.value.transaction ?? 'unknown transaction';
  921. }
  922. if (isMissingInstrumentationNode(t)) {
  923. return padding + 'missing_instrumentation';
  924. }
  925. if (isRootNode(t)) {
  926. return padding + 'Root';
  927. }
  928. if (isTraceNode(t)) {
  929. return padding + 'Trace';
  930. }
  931. throw new Error('Not implemented');
  932. }