traceTree.tsx 30 KB

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