traceTree.tsx 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  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. function visit(
  184. parent: TraceTreeNode<TraceTree.NodeValue | null>,
  185. value: TraceTree.NodeValue
  186. ) {
  187. const node = new TraceTreeNode(parent, value, {
  188. project_slug: value && 'project_slug' in value ? value.project_slug : undefined,
  189. event_id: value && 'event_id' in value ? value.event_id : undefined,
  190. });
  191. node.canFetchData = true;
  192. if (parent) {
  193. parent.children.push(node as TraceTreeNode<TraceTree.NodeValue>);
  194. }
  195. if (value && 'children' in value) {
  196. for (const child of value.children) {
  197. visit(node, child);
  198. }
  199. }
  200. return node;
  201. }
  202. const traceNode = new TraceTreeNode(tree.root, trace, {
  203. event_id: undefined,
  204. project_slug: undefined,
  205. });
  206. // Trace is always expanded by default
  207. tree.root.children.push(traceNode);
  208. let traceStart = Number.POSITIVE_INFINITY;
  209. let traceEnd = Number.NEGATIVE_INFINITY;
  210. for (const transaction of trace.transactions) {
  211. if (transaction.start_timestamp < traceStart) {
  212. traceStart = transaction.start_timestamp;
  213. }
  214. if (transaction.timestamp > traceEnd) {
  215. traceEnd = transaction.timestamp;
  216. }
  217. visit(traceNode, transaction);
  218. }
  219. for (const trace_error of trace.orphan_errors) {
  220. visit(traceNode, trace_error);
  221. }
  222. traceNode.space = [traceStart, traceEnd - traceStart];
  223. tree.root.space = [traceStart, traceEnd - traceStart];
  224. return tree.build();
  225. }
  226. static GetTraceType(root: TraceTreeNode<null>): TraceType {
  227. const trace = root.children[0];
  228. if (!trace || !isTraceNode(trace)) {
  229. throw new TypeError('Not trace node');
  230. }
  231. const {transactions, orphan_errors} = trace.value;
  232. const {roots, orphans} = (transactions ?? []).reduce(
  233. (counts, transaction) => {
  234. if (isRootTransaction(transaction)) {
  235. counts.roots++;
  236. } else {
  237. counts.orphans++;
  238. }
  239. return counts;
  240. },
  241. {roots: 0, orphans: 0}
  242. );
  243. if (roots === 0) {
  244. if (orphans > 0) {
  245. return TraceType.NO_ROOT;
  246. }
  247. if (orphan_errors && orphan_errors.length > 0) {
  248. return TraceType.ONLY_ERRORS;
  249. }
  250. return TraceType.EMPTY_TRACE;
  251. }
  252. if (roots === 1) {
  253. if (orphans > 0) {
  254. return TraceType.BROKEN_SUBTRACES;
  255. }
  256. return TraceType.ONE_ROOT;
  257. }
  258. if (roots > 1) {
  259. return TraceType.MULTIPLE_ROOTS;
  260. }
  261. throw new Error('Unknown trace type');
  262. }
  263. static FromSpans(
  264. parent: TraceTreeNode<TraceTree.NodeValue>,
  265. spans: RawSpanType[]
  266. ): TraceTreeNode<TraceTree.NodeValue> {
  267. const parentIsSpan = isSpanNode(parent);
  268. const lookuptable: Record<
  269. RawSpanType['span_id'],
  270. TraceTreeNode<TraceTree.Span | TraceTree.Transaction>
  271. > = {};
  272. if (parent.spanChildren.length > 0) {
  273. parent.zoomedIn = true;
  274. return parent;
  275. }
  276. if (parentIsSpan) {
  277. if (parent.value && 'span_id' in parent.value) {
  278. lookuptable[parent.value.span_id] = parent as TraceTreeNode<TraceTree.Span>;
  279. }
  280. }
  281. const transactionsToSpanMap = new Map<string, TraceTreeNode<TraceTree.Transaction>>();
  282. for (const child of parent.children) {
  283. if (
  284. isTransactionNode(child) &&
  285. 'parent_span_id' in child.value &&
  286. typeof child.value.parent_span_id === 'string'
  287. ) {
  288. transactionsToSpanMap.set(child.value.parent_span_id, child);
  289. }
  290. continue;
  291. }
  292. for (const span of spans) {
  293. const parentNode = transactionsToSpanMap.get(span.span_id);
  294. let node: TraceTreeNode<TraceTree.Span>;
  295. if (parentNode) {
  296. node = parentNode.clone() as unknown as TraceTreeNode<TraceTree.Span>;
  297. } else {
  298. node = new TraceTreeNode(null, span, {
  299. event_id: undefined,
  300. project_slug: undefined,
  301. });
  302. }
  303. node.canFetchData = !!parentNode;
  304. if (parentNode) {
  305. node.metadata = parentNode.metadata;
  306. }
  307. lookuptable[span.span_id] = node;
  308. if (span.parent_span_id) {
  309. const spanParentNode = lookuptable[span.parent_span_id];
  310. if (spanParentNode) {
  311. node.parent = spanParentNode;
  312. maybeInsertMissingInstrumentationSpan(spanParentNode, node);
  313. spanParentNode.spanChildren.push(node);
  314. continue;
  315. }
  316. }
  317. // Orphaned span
  318. maybeInsertMissingInstrumentationSpan(parent, node);
  319. parent.spanChildren.push(node);
  320. node.parent = parent as TraceTreeNode<TraceTree.Span>;
  321. }
  322. parent.zoomedIn = true;
  323. TraceTree.AutogroupSiblingSpanNodes(parent);
  324. TraceTree.AutogroupDirectChildrenSpanNodes(parent);
  325. return parent;
  326. }
  327. get list(): ReadonlyArray<TraceTreeNode<TraceTree.NodeValue>> {
  328. return this._list;
  329. }
  330. static AutogroupDirectChildrenSpanNodes(
  331. root: TraceTreeNode<TraceTree.NodeValue>
  332. ): void {
  333. const queue = [root];
  334. while (queue.length > 0) {
  335. const node = queue.pop()!;
  336. if (node.children.length > 1 || !isSpanNode(node)) {
  337. for (const child of node.children) {
  338. queue.push(child);
  339. }
  340. continue;
  341. }
  342. const head = node;
  343. let tail = node;
  344. let groupMatchCount = 0;
  345. while (
  346. tail &&
  347. tail.children.length === 1 &&
  348. isSpanNode(tail.children[0]) &&
  349. tail.children[0].value.op === head.value.op
  350. ) {
  351. groupMatchCount++;
  352. tail = tail.children[0];
  353. }
  354. if (groupMatchCount < 1) {
  355. for (const child of head.children) {
  356. queue.push(child);
  357. }
  358. continue;
  359. }
  360. const autoGroupedNode = new ParentAutogroupNode(
  361. node.parent,
  362. {
  363. ...head.value,
  364. autogrouped_by: {
  365. op: head.value && 'op' in head.value ? head.value.op ?? '' : '',
  366. },
  367. },
  368. {
  369. event_id: undefined,
  370. project_slug: undefined,
  371. },
  372. head,
  373. tail
  374. );
  375. if (!node.parent) {
  376. throw new Error('Parent node is missing, this should be unreachable code');
  377. }
  378. autoGroupedNode.groupCount = groupMatchCount + 1;
  379. autoGroupedNode.space = [
  380. head.value.start_timestamp,
  381. tail.value.timestamp - head.value.start_timestamp,
  382. ];
  383. for (const c of tail.children) {
  384. c.parent = autoGroupedNode;
  385. queue.push(c);
  386. }
  387. const index = node.parent.children.indexOf(node);
  388. node.parent.children[index] = autoGroupedNode;
  389. }
  390. }
  391. static AutogroupSiblingSpanNodes(root: TraceTreeNode<TraceTree.NodeValue>): void {
  392. const queue = [root];
  393. while (queue.length > 0) {
  394. const node = queue.pop()!;
  395. if (node.children.length < 5) {
  396. for (const child of node.children) {
  397. queue.push(child);
  398. }
  399. continue;
  400. }
  401. let index = 0;
  402. let matchCount = 0;
  403. while (index < node.children.length) {
  404. const current = node.children[index] as TraceTreeNode<TraceTree.Span>;
  405. const next = node.children[index + 1] as TraceTreeNode<TraceTree.Span>;
  406. if (
  407. next &&
  408. next.children.length === 0 &&
  409. current.children.length === 0 &&
  410. next.value.op === current.value.op &&
  411. next.value.description === current.value.description
  412. ) {
  413. matchCount++;
  414. // If the next node is the last node in the list, we keep iterating
  415. if (index + 1 < node.children.length) {
  416. index++;
  417. continue;
  418. }
  419. }
  420. if (matchCount >= 4) {
  421. const autoGroupedNode = new SiblingAutogroupNode(
  422. node,
  423. {
  424. ...current.value,
  425. autogrouped_by: {
  426. op: current.value.op ?? '',
  427. description: current.value.description ?? '',
  428. },
  429. },
  430. {
  431. event_id: undefined,
  432. project_slug: undefined,
  433. }
  434. );
  435. autoGroupedNode.groupCount = matchCount + 1;
  436. const start = index - matchCount;
  437. for (let j = start; j < index - 1; j++) {
  438. autoGroupedNode.children.push(node.children[j]);
  439. autoGroupedNode.children[autoGroupedNode.children.length - 1].parent =
  440. autoGroupedNode;
  441. }
  442. node.children.splice(start, matchCount + 1, autoGroupedNode);
  443. index = start + 1;
  444. matchCount = 0;
  445. } else {
  446. index++;
  447. matchCount = 0;
  448. }
  449. }
  450. }
  451. }
  452. // Returns boolean to indicate if node was updated
  453. expand(node: TraceTreeNode<TraceTree.NodeValue>, expanded: boolean): boolean {
  454. if (expanded === node.expanded) {
  455. return false;
  456. }
  457. // Expanding is not allowed for zoomed in nodes
  458. if (node.zoomedIn) {
  459. return false;
  460. }
  461. if (node instanceof ParentAutogroupNode) {
  462. // In parent autogrouping, we perform a node swap and either point the
  463. // head or tails of the autogrouped sequence to the autogrouped node
  464. if (node.expanded) {
  465. const index = this._list.indexOf(node);
  466. const autogroupedChildren = node.getVisibleChildren();
  467. this._list.splice(index + 1, autogroupedChildren.length);
  468. const newChildren = node.tail.getVisibleChildren();
  469. for (const c of node.tail.children) {
  470. c.parent = node;
  471. }
  472. this._list.splice(index + 1, 0, ...newChildren);
  473. } else {
  474. node.head.parent = node;
  475. const index = this._list.indexOf(node);
  476. const childrenCount = node.getVisibleChildrenCount();
  477. this._list.splice(index + 1, childrenCount);
  478. node.getVisibleChildrenCount();
  479. const newChildren = [node.head].concat(
  480. node.head.getVisibleChildren() as TraceTreeNode<TraceTree.Span>[]
  481. );
  482. for (const c of node.children) {
  483. c.parent = node.tail;
  484. }
  485. this._list.splice(index + 1, 0, ...newChildren);
  486. }
  487. node.invalidate(node);
  488. node.expanded = expanded;
  489. return true;
  490. }
  491. if (node.expanded) {
  492. const index = this._list.indexOf(node);
  493. this._list.splice(index + 1, node.getVisibleChildrenCount());
  494. // Flip expanded after collecting visible children
  495. node.expanded = expanded;
  496. } else {
  497. const index = this._list.indexOf(node);
  498. // Flip expanded so that we can collect visible children
  499. node.expanded = expanded;
  500. this._list.splice(index + 1, 0, ...node.getVisibleChildren());
  501. }
  502. node.expanded = expanded;
  503. return true;
  504. }
  505. zoomIn(
  506. node: TraceTreeNode<TraceTree.NodeValue>,
  507. zoomedIn: boolean,
  508. options: {
  509. api: Client;
  510. organization: Organization;
  511. }
  512. ): Promise<Event | null> {
  513. if (zoomedIn === node.zoomedIn) {
  514. return Promise.resolve(null);
  515. }
  516. if (!zoomedIn) {
  517. const index = this._list.indexOf(node);
  518. const childrenCount = node.getVisibleChildrenCount();
  519. this._list.splice(index + 1, childrenCount);
  520. node.zoomedIn = zoomedIn;
  521. if (node.expanded) {
  522. this._list.splice(index + 1, 0, ...node.getVisibleChildren());
  523. }
  524. return Promise.resolve(null);
  525. }
  526. const promise =
  527. this._spanPromises.get(node) ??
  528. fetchTransactionSpans(
  529. options.api,
  530. options.organization,
  531. node.metadata.project_slug!,
  532. node.metadata.event_id!
  533. );
  534. promise.then(data => {
  535. const spans = data.entries.find(s => s.type === 'spans');
  536. if (!spans) {
  537. return data;
  538. }
  539. // Remove existing entries from the list
  540. const index = this._list.indexOf(node);
  541. if (node.expanded) {
  542. const childrenCount = node.getVisibleChildrenCount();
  543. this._list.splice(index + 1, childrenCount);
  544. }
  545. // Api response is not sorted
  546. if (spans.data) {
  547. spans.data.sort((a, b) => a.start_timestamp - b.start_timestamp);
  548. }
  549. TraceTree.FromSpans(node, spans.data);
  550. const spanChildren = node.getVisibleChildren();
  551. this._list.splice(index + 1, 0, ...spanChildren);
  552. return data;
  553. });
  554. this._spanPromises.set(node, promise);
  555. return promise;
  556. }
  557. toList(): TraceTreeNode<TraceTree.NodeValue>[] {
  558. const list: TraceTreeNode<TraceTree.NodeValue>[] = [];
  559. function visit(node: TraceTreeNode<TraceTree.NodeValue>) {
  560. list.push(node);
  561. if (!node.expanded) {
  562. return;
  563. }
  564. for (const child of node.children) {
  565. visit(child);
  566. }
  567. }
  568. for (const child of this.root.children) {
  569. visit(child);
  570. }
  571. return list;
  572. }
  573. /**
  574. * Prints the tree in a human readable format, useful for debugging and testing
  575. */
  576. print() {
  577. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  578. const print = this.list
  579. .map(t => printNode(t, 0))
  580. .filter(Boolean)
  581. .join('\n');
  582. // eslint-disable-next-line no-console
  583. console.log(print);
  584. }
  585. build() {
  586. this._list = this.toList();
  587. return this;
  588. }
  589. }
  590. export class TraceTreeNode<T extends TraceTree.NodeValue> {
  591. parent: TraceTreeNode<TraceTree.NodeValue> | null = null;
  592. value: T;
  593. expanded: boolean = false;
  594. zoomedIn: boolean = false;
  595. canFetchData: boolean = false;
  596. metadata: TraceTree.Metadata = {
  597. project_slug: undefined,
  598. event_id: undefined,
  599. };
  600. space: [number, number] | null = null;
  601. private _depth: number | undefined;
  602. private _children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  603. private _spanChildren: TraceTreeNode<
  604. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  605. >[] = [];
  606. private _connectors: number[] | undefined = undefined;
  607. constructor(
  608. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  609. value: T,
  610. metadata: TraceTree.Metadata
  611. ) {
  612. this.parent = parent ?? null;
  613. this.value = value;
  614. this.metadata = metadata;
  615. if (value && 'timestamp' in value && 'start_timestamp' in value) {
  616. this.space = [value.start_timestamp, value.timestamp - value.start_timestamp];
  617. }
  618. if (isTransactionNode(this) || isTraceNode(this) || isSpanNode(this)) {
  619. this.expanded = true;
  620. }
  621. }
  622. clone(): TraceTreeNode<T> {
  623. const node = new TraceTreeNode(this.parent, this.value, this.metadata);
  624. node.expanded = this.expanded;
  625. node.zoomedIn = this.zoomedIn;
  626. node.canFetchData = this.canFetchData;
  627. node.space = this.space;
  628. node.children = this.children;
  629. return node;
  630. }
  631. get isOrphaned() {
  632. return this.parent?.value && 'orphan_errors' in this.parent.value;
  633. }
  634. get isLastChild() {
  635. return this.parent?.children[this.parent.children.length - 1] === this;
  636. }
  637. /**
  638. * Return a lazily calculated depth of the node in the tree.
  639. * Root node has a value of -1 as it is abstract.
  640. */
  641. get depth(): number {
  642. if (typeof this._depth === 'number') {
  643. return this._depth;
  644. }
  645. let depth = -2;
  646. let node: TraceTreeNode<any> | null = this;
  647. while (node) {
  648. if (typeof node.parent?.depth === 'number') {
  649. this._depth = node.parent.depth + 1;
  650. return this._depth;
  651. }
  652. depth++;
  653. node = node.parent;
  654. }
  655. this._depth = depth;
  656. return this._depth;
  657. }
  658. /**
  659. * Returns the depth levels at which the row should draw vertical connectors
  660. * negative values mean connector points to an orphaned node
  661. */
  662. get connectors(): number[] {
  663. if (this._connectors !== undefined) {
  664. return this._connectors!;
  665. }
  666. this._connectors = [];
  667. if (this.parent?.connectors !== undefined) {
  668. this._connectors = [...this.parent.connectors];
  669. if (this.isLastChild || this.value === null) {
  670. return this._connectors;
  671. }
  672. this.connectors.push(this.isOrphaned ? -this.depth : this.depth);
  673. return this._connectors;
  674. }
  675. let node: TraceTreeNode<T> | TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
  676. while (node) {
  677. if (node.value === null) {
  678. break;
  679. }
  680. if (node.isLastChild) {
  681. node = node.parent;
  682. continue;
  683. }
  684. this._connectors.push(node.isOrphaned ? -node.depth : node.depth);
  685. node = node.parent;
  686. }
  687. return this._connectors;
  688. }
  689. /**
  690. * Returns the children that the node currently points to.
  691. * The logic here is a consequence of the tree design, where we want to be able to store
  692. * both transaction and span nodes in the same tree. This results in an annoying API where
  693. * we either store span children separately or transaction children separately. A better design
  694. * would have been to create an invisible meta node that always points to the correct children.
  695. */
  696. get children(): TraceTreeNode<TraceTree.NodeValue>[] {
  697. if (isAutogroupedNode(this)) {
  698. return this._children;
  699. }
  700. if (isSpanNode(this)) {
  701. return this.canFetchData && !this.zoomedIn ? [] : this.spanChildren;
  702. }
  703. // if a node is zoomed in, return span children, else return transaction children
  704. return this.zoomedIn ? this._spanChildren : this._children;
  705. }
  706. set children(children: TraceTreeNode<TraceTree.NodeValue>[]) {
  707. this._children = children;
  708. }
  709. get spanChildren(): TraceTreeNode<
  710. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  711. >[] {
  712. return this._spanChildren;
  713. }
  714. /**
  715. * Invalidate the visual data used to render the tree, forcing it
  716. * to be recalculated on the next render. This is useful when for example
  717. * the tree is expanded or collapsed, or when the tree is mutated and
  718. * the visual data is no longer valid as the indentation changes
  719. */
  720. invalidate(root?: TraceTreeNode<TraceTree.NodeValue>) {
  721. this._connectors = undefined;
  722. this._depth = undefined;
  723. if (root) {
  724. const queue = [...this.children];
  725. while (queue.length > 0) {
  726. const next = queue.pop()!;
  727. next.invalidate();
  728. for (let i = 0; i < next.children.length; i++) {
  729. queue.push(next.children[i]);
  730. }
  731. }
  732. }
  733. }
  734. getVisibleChildrenCount(): number {
  735. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  736. let count = 0;
  737. if (
  738. this.expanded ||
  739. isParentAutogroupedNode(this) ||
  740. isMissingInstrumentationNode(this)
  741. ) {
  742. for (let i = this.children.length - 1; i >= 0; i--) {
  743. stack.push(this.children[i]);
  744. }
  745. }
  746. while (stack.length > 0) {
  747. const node = stack.pop()!;
  748. count++;
  749. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  750. // to ensure they are processed in the original left-to-right order.
  751. if (node.expanded || isParentAutogroupedNode(node)) {
  752. for (let i = node.children.length - 1; i >= 0; i--) {
  753. stack.push(node.children[i]);
  754. }
  755. }
  756. }
  757. return count;
  758. }
  759. getVisibleChildren(): TraceTreeNode<TraceTree.NodeValue>[] {
  760. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  761. const children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  762. if (
  763. this.expanded ||
  764. isParentAutogroupedNode(this) ||
  765. isMissingInstrumentationNode(this)
  766. ) {
  767. for (let i = this.children.length - 1; i >= 0; i--) {
  768. stack.push(this.children[i]);
  769. }
  770. }
  771. while (stack.length > 0) {
  772. const node = stack.pop()!;
  773. children.push(node);
  774. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  775. // to ensure they are processed in the original left-to-right order.
  776. if (node.expanded || isParentAutogroupedNode(node)) {
  777. for (let i = node.children.length - 1; i >= 0; i--) {
  778. stack.push(node.children[i]);
  779. }
  780. }
  781. }
  782. return children;
  783. }
  784. print() {
  785. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  786. const offset = this.depth === -1 ? 1 : 0;
  787. const nodes = [this, ...this.getVisibleChildren()];
  788. const print = nodes
  789. .map(t => printNode(t, offset))
  790. .filter(Boolean)
  791. .join('\n');
  792. // eslint-disable-next-line no-console
  793. console.log(print);
  794. }
  795. static Root() {
  796. return new TraceTreeNode(null, null, {
  797. event_id: undefined,
  798. project_slug: undefined,
  799. });
  800. }
  801. }
  802. export class ParentAutogroupNode extends TraceTreeNode<TraceTree.ChildrenAutogroup> {
  803. head: TraceTreeNode<TraceTree.Span>;
  804. tail: TraceTreeNode<TraceTree.Span>;
  805. groupCount: number = 0;
  806. constructor(
  807. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  808. node: TraceTree.ChildrenAutogroup,
  809. metadata: TraceTree.Metadata,
  810. head: TraceTreeNode<TraceTree.Span>,
  811. tail: TraceTreeNode<TraceTree.Span>
  812. ) {
  813. super(parent, node, metadata);
  814. this.head = head;
  815. this.tail = tail;
  816. }
  817. get children() {
  818. if (this.expanded) {
  819. return [this.head];
  820. }
  821. return this.tail.children;
  822. }
  823. }
  824. export class SiblingAutogroupNode extends TraceTreeNode<TraceTree.SiblingAutogroup> {
  825. groupCount: number = 0;
  826. constructor(
  827. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  828. node: TraceTree.SiblingAutogroup,
  829. metadata: TraceTree.Metadata
  830. ) {
  831. super(parent, node, metadata);
  832. }
  833. }
  834. function partialTransaction(
  835. partial: Partial<TraceTree.Transaction>
  836. ): TraceTree.Transaction {
  837. return {
  838. start_timestamp: 0,
  839. timestamp: 0,
  840. errors: [],
  841. performance_issues: [],
  842. parent_span_id: '',
  843. span_id: '',
  844. parent_event_id: '',
  845. project_id: 0,
  846. 'transaction.duration': 0,
  847. 'transaction.op': 'db',
  848. 'transaction.status': 'ok',
  849. generation: 0,
  850. project_slug: '',
  851. event_id: `event_id`,
  852. transaction: `transaction`,
  853. children: [],
  854. ...partial,
  855. };
  856. }
  857. export function makeExampleTrace(metadata: TraceTree.Metadata): TraceTree {
  858. const trace: TraceTree.Trace = {
  859. transactions: [],
  860. orphan_errors: [],
  861. };
  862. function randomBetween(min: number, max: number) {
  863. return Math.floor(Math.random() * (max - min + 1) + min);
  864. }
  865. let start = new Date().getTime();
  866. for (let i = 0; i < 25; i++) {
  867. const end = start + randomBetween(100, 200);
  868. const nest = i > 0 && Math.random() > 0.5;
  869. if (nest) {
  870. const parent = trace.transactions[trace.transactions.length - 1];
  871. parent.children.push(
  872. partialTransaction({
  873. ...metadata,
  874. generation: 0,
  875. start_timestamp: start,
  876. transaction: `parent transaction ${i}`,
  877. timestamp: end,
  878. })
  879. );
  880. parent.timestamp = end;
  881. } else {
  882. trace.transactions.push(
  883. partialTransaction({
  884. ...metadata,
  885. generation: 0,
  886. start_timestamp: start,
  887. transaction: 'loading...',
  888. ['transaction.op']: 'loading',
  889. timestamp: end,
  890. })
  891. );
  892. }
  893. start = end;
  894. }
  895. const tree = TraceTree.FromTrace(trace);
  896. return tree;
  897. }
  898. function printNode(t: TraceTreeNode<TraceTree.NodeValue>, offset: number): string {
  899. // +1 because we may be printing from the root which is -1 indexed
  900. const padding = ' '.repeat(t.depth + offset);
  901. if (isAutogroupedNode(t)) {
  902. if (isParentAutogroupedNode(t)) {
  903. return padding + `parent autogroup (${t.groupCount})`;
  904. }
  905. if (isSiblingAutogroupedNode(t)) {
  906. return padding + `sibling autogroup (${t.groupCount})`;
  907. }
  908. return padding + 'autogroup';
  909. }
  910. if (isSpanNode(t)) {
  911. return padding + t.value?.op ?? 'unknown span op';
  912. }
  913. if (isTransactionNode(t)) {
  914. return padding + t.value.transaction ?? 'unknown transaction';
  915. }
  916. if (isMissingInstrumentationNode(t)) {
  917. return padding + 'missing_instrumentation';
  918. }
  919. if (isRootNode(t)) {
  920. return padding + 'Root';
  921. }
  922. if (isTraceNode(t)) {
  923. return padding + 'Trace';
  924. }
  925. throw new Error('Not implemented');
  926. }