traceTree.tsx 34 KB

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