traceTree.tsx 32 KB

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