traceTree.tsx 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612
  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, Measurement} from 'sentry/types/event';
  5. import type {
  6. TraceError as TraceErrorType,
  7. TraceErrorOrIssue,
  8. TraceFullDetailed,
  9. TraceSplitResults,
  10. } from 'sentry/utils/performance/quickTrace/types';
  11. import {isTraceError} from 'sentry/utils/performance/quickTrace/utils';
  12. import {TraceType} from '../traceDetails/newTraceDetailsContent';
  13. import {isRootTransaction} from '../traceDetails/utils';
  14. import {
  15. isAutogroupedNode,
  16. isMissingInstrumentationNode,
  17. isParentAutogroupedNode,
  18. isRootNode,
  19. isSiblingAutogroupedNode,
  20. isSpanNode,
  21. isTraceErrorNode,
  22. isTraceNode,
  23. isTransactionNode,
  24. shouldAddMissingInstrumentationSpan,
  25. } from './guards';
  26. /**
  27. *
  28. * This file implements the tree data structure that is used to represent a trace. We do
  29. * this both for performance reasons as well as flexibility. The requirement for a tree
  30. * is to support incremental patching and updates. This is important because we want to
  31. * be able to fetch more data as the user interacts with the tree, and we want to be able
  32. * efficiently update the tree as we receive more data.
  33. *
  34. * The trace is represented as a tree with different node value types (transaction or span)
  35. * Each tree node contains a reference to its parent and a list of references to its children,
  36. * as well as a reference to the value that the node holds. Each node also contains
  37. * some meta data and state about the node, such as if it is expanded or zoomed in. The benefit
  38. * of abstracting parts of the UI state is that the tree will persist user actions such as expanding
  39. * or collapsing nodes which would have otherwise been lost when individual nodes are remounted in the tree.
  40. *
  41. * Each tree holds a list reference, which is a live reference to a flattened representation
  42. * 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
  43. * reasons as we want to support mutations on traces with ~100k+ nodes), callers need to manage reactivity themselves.
  44. *
  45. * An alternative, but not recommended approach is to call build() on the tree after each mutation,
  46. * which will iterate over all of the children and build a fresh list reference.
  47. *
  48. * In most cases, the initial tree is a list of transactions containing other transactions. Each transaction can
  49. * then be expanded into a list of spans which can also in some cases be expanded.
  50. *
  51. * - trace - trace
  52. * |- parent transaction --> when expanding |- parent transaction
  53. * |- child transaction |- span
  54. * |- span this used to be a transaction,
  55. * |- child transaction span <- but is now be a list of spans
  56. * |- span belonging to the transaction
  57. * this results in child txns to be lost,
  58. * which is a confusing user experience
  59. *
  60. * The tree supports autogrouping of spans vertically or as siblings. When that happens, a autogrouped node of either a vertical or
  61. * sibling type is inserted as an intermediary node. In the vertical case, the autogrouped node
  62. * holds the reference to the head and tail of the autogrouped sequence. In the sibling case, the autogrouped node
  63. * holds a reference to the children that are part of the autogrouped sequence. When expanding and collapsing these nodes,
  64. * the tree perform a reference swap to either point to the head (when expanded) or tail (when collapsed) of the autogrouped sequence.
  65. *
  66. * In vertical grouping case, the following happens:
  67. *
  68. * - root - root
  69. * - trace - trace
  70. * |- transaction |- transaction
  71. * |- span 1 <-| these become autogrouped |- autogrouped (head=span1, tail=span3, children points to children of tail)
  72. * |- span 2 |- as they are inserted into |- other span (parent points to autogrouped node)
  73. * |- span 3 <-| the tree.
  74. * |- other span
  75. *
  76. * When the autogrouped node is expanded the UI needs to show the entire collapsed chain, so we swap the tail children to point
  77. * back to the tail, and have autogrouped node point to it's head as the children.
  78. *
  79. * - root - root
  80. * - trace - trace
  81. * |- transaction |- transaction
  82. * |- autogrouped (head=span1, tail=span3) <- when expanding |- autogrouped (head=span1, tail=span3, children points to head)
  83. * | other span (paren points to autogrouped) |- span 1 (head)
  84. * |- span 2
  85. * |- span 3 (tail)
  86. * |- other span (children of tail, parent points to tail)
  87. *
  88. * Notes and improvements:
  89. * - collecting children should be O(n), it is currently O(n^2) as we are missing a proper queue implementation
  90. * - the notion of expanded and zoomed is confusing, they stand for the same idea from a UI pov
  91. * - there is an annoying thing wrt span and transaction nodes where we either store data on _children or _spanChildren
  92. * this is because we want to be able to store both transaction and span nodes in the same tree, but it makes for an
  93. * annoying API. A better design would have been to create an invisible meta node that just points to the correct children
  94. * - connector generation should live in the UI layer, not in the tree. Same with depth calculation. It is more convenient
  95. * to calculate this when rendering the tree, as we can only calculate it only for the visible nodes and avoid an extra tree pass
  96. * - instead of storing span children separately, we should have meta tree nodes that handle pointing to the correct children
  97. */
  98. export declare namespace TraceTree {
  99. type Transaction = TraceFullDetailed;
  100. type Span = RawSpanType & {
  101. childTxn: Transaction | undefined;
  102. event: EventTransaction;
  103. relatedErrors: TraceErrorOrIssue[];
  104. };
  105. type Trace = TraceSplitResults<Transaction>;
  106. type TraceError = TraceErrorType;
  107. interface MissingInstrumentationSpan {
  108. start_timestamp: number;
  109. timestamp: number;
  110. type: 'missing_instrumentation';
  111. }
  112. interface SiblingAutogroup extends RawSpanType {
  113. autogrouped_by: {
  114. description: string;
  115. op: string;
  116. };
  117. }
  118. interface ChildrenAutogroup {
  119. autogrouped_by: {
  120. op: string;
  121. };
  122. }
  123. type NodeValue =
  124. | Trace
  125. | Transaction
  126. | TraceError
  127. | Span
  128. | MissingInstrumentationSpan
  129. | SiblingAutogroup
  130. | ChildrenAutogroup
  131. | null;
  132. type NodePath = `${'txn' | 'span' | 'ag' | 'trace' | 'ms' | 'error'}:${string}`;
  133. type Metadata = {
  134. event_id: string | undefined;
  135. project_slug: string | undefined;
  136. };
  137. type Indicator = {
  138. duration: number;
  139. label: string;
  140. measurement: Measurement;
  141. start: number;
  142. type: 'cls' | 'fcp' | 'fp' | 'lcp' | 'ttfb';
  143. };
  144. }
  145. function cacheKey(organization: Organization, project_slug: string, event_id: string) {
  146. return organization.slug + ':' + project_slug + ':' + event_id;
  147. }
  148. function fetchTransactionSpans(
  149. api: Client,
  150. organization: Organization,
  151. project_slug: string,
  152. event_id: string
  153. ): Promise<EventTransaction> {
  154. return api.requestPromise(
  155. `/organizations/${organization.slug}/events/${project_slug}:${event_id}/`
  156. );
  157. }
  158. function measurementToTimestamp(
  159. start_timestamp: number,
  160. measurement: number,
  161. unit: string
  162. ) {
  163. if (unit === 'second') {
  164. return start_timestamp + measurement;
  165. }
  166. if (unit === 'millisecond') {
  167. return start_timestamp + measurement / 1e3;
  168. }
  169. if (unit === 'nanosecond') {
  170. return start_timestamp + measurement / 1e9;
  171. }
  172. throw new TypeError(`Unsupported measurement unit', ${unit}`);
  173. }
  174. function maybeInsertMissingInstrumentationSpan(
  175. parent: TraceTreeNode<TraceTree.NodeValue>,
  176. node: TraceTreeNode<TraceTree.Span>
  177. ) {
  178. const previousSpan = parent.spanChildren[parent.spanChildren.length - 1];
  179. if (!previousSpan || !isSpanNode(previousSpan)) {
  180. return;
  181. }
  182. if (node.value.start_timestamp - previousSpan.value.timestamp < 0.1) {
  183. return;
  184. }
  185. const missingInstrumentationSpan = new MissingInstrumentationNode(
  186. parent,
  187. {
  188. type: 'missing_instrumentation',
  189. start_timestamp: previousSpan.value.timestamp,
  190. timestamp: node.value.start_timestamp,
  191. },
  192. {
  193. event_id: undefined,
  194. project_slug: undefined,
  195. },
  196. previousSpan,
  197. node
  198. );
  199. parent.spanChildren.push(missingInstrumentationSpan);
  200. }
  201. // cls is not included as it is a cumulative layout shift and not a single point in time
  202. const RENDERABLE_MEASUREMENTS = ['fcp', 'fp', 'lcp', 'ttfb'];
  203. export class TraceTree {
  204. type: 'loading' | 'empty' | 'trace' = 'trace';
  205. root: TraceTreeNode<null> = TraceTreeNode.Root();
  206. indicators: TraceTree.Indicator[] = [];
  207. private _spanPromises: Map<string, Promise<Event>> = new Map();
  208. private _list: TraceTreeNode<TraceTree.NodeValue>[] = [];
  209. static Empty() {
  210. const tree = new TraceTree().build();
  211. tree.type = 'empty';
  212. return tree;
  213. }
  214. static Loading(metadata: TraceTree.Metadata): TraceTree {
  215. const tree = makeExampleTrace(metadata);
  216. tree.type = 'loading';
  217. return tree;
  218. }
  219. static FromTrace(trace: TraceTree.Trace, event?: EventTransaction): TraceTree {
  220. const tree = new TraceTree();
  221. let traceStart = Number.POSITIVE_INFINITY;
  222. let traceEnd = Number.NEGATIVE_INFINITY;
  223. function visit(
  224. parent: TraceTreeNode<TraceTree.NodeValue | null>,
  225. value: TraceTree.Transaction | TraceTree.TraceError
  226. ) {
  227. const node = new TraceTreeNode(parent, value, {
  228. project_slug: value && 'project_slug' in value ? value.project_slug : undefined,
  229. event_id: value && 'event_id' in value ? value.event_id : undefined,
  230. });
  231. node.canFetch = true;
  232. if (parent) {
  233. parent.children.push(node as TraceTreeNode<TraceTree.NodeValue>);
  234. }
  235. if ('start_timestamp' in value && value.start_timestamp < traceStart) {
  236. traceStart = value.start_timestamp;
  237. }
  238. if ('timestamp' in value && typeof value.timestamp === 'number') {
  239. // Errors don't have 'start_timestamp', so we adjust traceStart
  240. // with an errors 'timestamp'
  241. if (isTraceError(value)) {
  242. traceStart = Math.min(value.timestamp, traceStart);
  243. }
  244. traceEnd = Math.max(value.timestamp, traceEnd);
  245. }
  246. if (value && 'children' in value) {
  247. for (const child of value.children) {
  248. visit(node, child);
  249. }
  250. }
  251. return node;
  252. }
  253. const traceNode = new TraceTreeNode(tree.root, trace, {
  254. event_id: undefined,
  255. project_slug: undefined,
  256. });
  257. // Trace is always expanded by default
  258. tree.root.children.push(traceNode);
  259. for (const transaction of trace.transactions) {
  260. visit(traceNode, transaction);
  261. }
  262. for (const trace_error of trace.orphan_errors) {
  263. visit(traceNode, trace_error);
  264. }
  265. if (event?.measurements) {
  266. const indicators = tree
  267. .collectMeasurements(traceStart, event.measurements)
  268. .sort((a, b) => a.start - b.start);
  269. for (const indicator of indicators) {
  270. if (indicator.start > traceEnd) {
  271. traceEnd = indicator.start;
  272. }
  273. indicator.start *= traceNode.multiplier;
  274. }
  275. tree.indicators = indicators;
  276. }
  277. traceNode.space = [
  278. traceStart * traceNode.multiplier,
  279. (traceEnd - traceStart) * traceNode.multiplier,
  280. ];
  281. tree.root.space = [
  282. traceStart * traceNode.multiplier,
  283. (traceEnd - traceStart) * traceNode.multiplier,
  284. ];
  285. return tree.build();
  286. }
  287. static GetTraceType(root: TraceTreeNode<null>): TraceType {
  288. const trace = root.children[0];
  289. if (!trace || !isTraceNode(trace)) {
  290. throw new TypeError('Not trace node');
  291. }
  292. const {transactions, orphan_errors} = trace.value;
  293. const {roots, orphans} = (transactions ?? []).reduce(
  294. (counts, transaction) => {
  295. if (isRootTransaction(transaction)) {
  296. counts.roots++;
  297. } else {
  298. counts.orphans++;
  299. }
  300. return counts;
  301. },
  302. {roots: 0, orphans: 0}
  303. );
  304. if (roots === 0) {
  305. if (orphans > 0) {
  306. return TraceType.NO_ROOT;
  307. }
  308. if (orphan_errors && orphan_errors.length > 0) {
  309. return TraceType.ONLY_ERRORS;
  310. }
  311. return TraceType.EMPTY_TRACE;
  312. }
  313. if (roots === 1) {
  314. if (orphans > 0) {
  315. return TraceType.BROKEN_SUBTRACES;
  316. }
  317. return TraceType.ONE_ROOT;
  318. }
  319. if (roots > 1) {
  320. return TraceType.MULTIPLE_ROOTS;
  321. }
  322. throw new Error('Unknown trace type');
  323. }
  324. static FromSpans(
  325. parent: TraceTreeNode<TraceTree.NodeValue>,
  326. data: Event,
  327. spans: RawSpanType[],
  328. options: {sdk: string | undefined} | undefined
  329. ): TraceTreeNode<TraceTree.NodeValue> {
  330. parent.invalidate(parent);
  331. const platformHasMissingSpans = shouldAddMissingInstrumentationSpan(options?.sdk);
  332. const parentIsSpan = isSpanNode(parent);
  333. const lookuptable: Record<
  334. RawSpanType['span_id'],
  335. TraceTreeNode<TraceTree.Span | TraceTree.Transaction>
  336. > = {};
  337. if (parent.spanChildren.length > 0) {
  338. parent.zoomedIn = true;
  339. return parent;
  340. }
  341. if (parentIsSpan) {
  342. if (parent.value && 'span_id' in parent.value) {
  343. lookuptable[parent.value.span_id] = parent as TraceTreeNode<TraceTree.Span>;
  344. }
  345. }
  346. const transactionsToSpanMap = new Map<string, TraceTreeNode<TraceTree.Transaction>>();
  347. for (const child of parent.children) {
  348. if (
  349. isTransactionNode(child) &&
  350. 'parent_span_id' in child.value &&
  351. typeof child.value.parent_span_id === 'string'
  352. ) {
  353. transactionsToSpanMap.set(child.value.parent_span_id, child);
  354. }
  355. continue;
  356. }
  357. for (const span of spans) {
  358. const childTxn = transactionsToSpanMap.get(span.span_id);
  359. const spanNodeValue: TraceTree.Span = {
  360. ...span,
  361. event: data as EventTransaction,
  362. relatedErrors: childTxn
  363. ? getSpanErrorsOrIssuesFromTransaction(span, childTxn.value)
  364. : [],
  365. childTxn: childTxn?.value,
  366. };
  367. const node: TraceTreeNode<TraceTree.Span> = new TraceTreeNode(null, spanNodeValue, {
  368. event_id: undefined,
  369. project_slug: undefined,
  370. });
  371. // This is the case where the current span is the parent of a txn at the
  372. // trace level. When zooming into the parent of the txn, we want to place a copy
  373. // of the txn as a child of the parenting span.
  374. if (childTxn) {
  375. const clonedChildTxn =
  376. childTxn.cloneDeep() as unknown as TraceTreeNode<TraceTree.Span>;
  377. node.spanChildren.push(clonedChildTxn);
  378. clonedChildTxn.parent = node;
  379. }
  380. lookuptable[span.span_id] = node;
  381. if (span.parent_span_id) {
  382. const spanParentNode = lookuptable[span.parent_span_id];
  383. if (spanParentNode) {
  384. node.parent = spanParentNode;
  385. if (platformHasMissingSpans) {
  386. maybeInsertMissingInstrumentationSpan(spanParentNode, node);
  387. }
  388. spanParentNode.spanChildren.push(node);
  389. continue;
  390. }
  391. }
  392. if (platformHasMissingSpans) {
  393. maybeInsertMissingInstrumentationSpan(parent, node);
  394. }
  395. parent.spanChildren.push(node);
  396. node.parent = parent;
  397. }
  398. parent.zoomedIn = true;
  399. TraceTree.AutogroupSiblingSpanNodes(parent);
  400. TraceTree.AutogroupDirectChildrenSpanNodes(parent);
  401. return parent;
  402. }
  403. static AutogroupDirectChildrenSpanNodes(
  404. root: TraceTreeNode<TraceTree.NodeValue>
  405. ): void {
  406. const queue = [root];
  407. while (queue.length > 0) {
  408. const node = queue.pop()!;
  409. if (node.children.length > 1 || !isSpanNode(node)) {
  410. for (const child of node.children) {
  411. queue.push(child);
  412. }
  413. continue;
  414. }
  415. const head = node;
  416. let tail = node;
  417. let groupMatchCount = 0;
  418. const erroredChildren: TraceTreeNode<TraceTree.NodeValue>[] = [];
  419. while (
  420. tail &&
  421. tail.children.length === 1 &&
  422. isSpanNode(tail.children[0]) &&
  423. tail.children[0].value.op === head.value.op
  424. ) {
  425. if (tail.value?.relatedErrors.length > 0) {
  426. erroredChildren.push(tail);
  427. }
  428. groupMatchCount++;
  429. tail = tail.children[0];
  430. }
  431. // Checking the tail node for errors as it is not included in the grouping
  432. // while loop, but is hidden when the autogrouped node is collapsed
  433. if (tail.value?.relatedErrors.length > 0) {
  434. erroredChildren.push(tail);
  435. }
  436. if (groupMatchCount < 1) {
  437. for (const child of head.children) {
  438. queue.push(child);
  439. }
  440. continue;
  441. }
  442. const autoGroupedNode = new ParentAutogroupNode(
  443. node.parent,
  444. {
  445. ...head.value,
  446. autogrouped_by: {
  447. op: head.value && 'op' in head.value ? head.value.op ?? '' : '',
  448. },
  449. },
  450. {
  451. event_id: undefined,
  452. project_slug: undefined,
  453. },
  454. head,
  455. tail
  456. );
  457. if (!node.parent) {
  458. throw new Error('Parent node is missing, this should be unreachable code');
  459. }
  460. autoGroupedNode.groupCount = groupMatchCount + 1;
  461. autoGroupedNode.errored_children = erroredChildren;
  462. autoGroupedNode.space = [
  463. Math.min(head.value.start_timestamp, tail.value.timestamp) *
  464. autoGroupedNode.multiplier,
  465. Math.max(
  466. tail.value.timestamp - head.value.start_timestamp,
  467. head.value.timestamp - tail.value.timestamp
  468. ) * autoGroupedNode.multiplier,
  469. ];
  470. for (const c of tail.children) {
  471. c.parent = autoGroupedNode;
  472. queue.push(c);
  473. }
  474. const index = node.parent.children.indexOf(node);
  475. node.parent.children[index] = autoGroupedNode;
  476. }
  477. }
  478. static AutogroupSiblingSpanNodes(root: TraceTreeNode<TraceTree.NodeValue>): void {
  479. const queue = [root];
  480. while (queue.length > 0) {
  481. const node = queue.pop()!;
  482. if (node.children.length < 5) {
  483. for (const child of node.children) {
  484. queue.push(child);
  485. }
  486. continue;
  487. }
  488. let index = 0;
  489. let matchCount = 0;
  490. while (index < node.children.length) {
  491. const current = node.children[index] as TraceTreeNode<TraceTree.Span>;
  492. const next = node.children[index + 1] as TraceTreeNode<TraceTree.Span>;
  493. if (
  494. next &&
  495. next.children.length === 0 &&
  496. current.children.length === 0 &&
  497. next.value.op === current.value.op &&
  498. next.value.description === current.value.description
  499. ) {
  500. matchCount++;
  501. // If the next node is the last node in the list, we keep iterating
  502. if (index + 1 < node.children.length) {
  503. index++;
  504. continue;
  505. }
  506. }
  507. if (matchCount >= 4) {
  508. const autoGroupedNode = new SiblingAutogroupNode(
  509. node,
  510. {
  511. ...current.value,
  512. autogrouped_by: {
  513. op: current.value.op ?? '',
  514. description: current.value.description ?? '',
  515. },
  516. },
  517. {
  518. event_id: undefined,
  519. project_slug: undefined,
  520. }
  521. );
  522. autoGroupedNode.groupCount = matchCount + 1;
  523. const start = index - matchCount;
  524. let start_timestamp = Number.MAX_SAFE_INTEGER;
  525. let timestamp = Number.MIN_SAFE_INTEGER;
  526. for (let j = start; j < start + matchCount + 1; j++) {
  527. const child = node.children[j];
  528. if (
  529. child.value &&
  530. 'timestamp' in child.value &&
  531. typeof child.value.timestamp === 'number' &&
  532. child.value.timestamp > timestamp
  533. ) {
  534. timestamp = child.value.timestamp;
  535. }
  536. if (
  537. child.value &&
  538. 'start_timestamp' in child.value &&
  539. typeof child.value.start_timestamp === 'number' &&
  540. child.value.start_timestamp > start_timestamp
  541. ) {
  542. start_timestamp = child.value.start_timestamp;
  543. }
  544. if (!isSpanNode(child)) {
  545. throw new TypeError(
  546. 'Expected child of autogrouped node to be a span node.'
  547. );
  548. }
  549. if (child.value?.relatedErrors.length > 0) {
  550. autoGroupedNode.errored_children.push(child);
  551. }
  552. autoGroupedNode.children.push(node.children[j]);
  553. autoGroupedNode.children[autoGroupedNode.children.length - 1].parent =
  554. autoGroupedNode;
  555. }
  556. autoGroupedNode.space = [
  557. start_timestamp * autoGroupedNode.multiplier,
  558. (timestamp - start_timestamp) * autoGroupedNode.multiplier,
  559. ];
  560. node.children.splice(start, matchCount + 1, autoGroupedNode);
  561. index = start + 1;
  562. matchCount = 0;
  563. } else {
  564. index++;
  565. matchCount = 0;
  566. }
  567. }
  568. }
  569. }
  570. collectMeasurements(
  571. start_timestamp: number,
  572. measurements: Record<string, Measurement>
  573. ): TraceTree.Indicator[] {
  574. const indicators: TraceTree.Indicator[] = [];
  575. for (const measurement of RENDERABLE_MEASUREMENTS) {
  576. const value = measurements[measurement];
  577. if (!value) {
  578. continue;
  579. }
  580. const timestamp = measurementToTimestamp(
  581. start_timestamp,
  582. value.value,
  583. value.unit ?? 'milliseconds'
  584. );
  585. indicators.push({
  586. start: timestamp,
  587. duration: 0,
  588. measurement: value,
  589. type: measurement as TraceTree.Indicator['type'],
  590. label: measurement.toUpperCase(),
  591. });
  592. }
  593. return indicators;
  594. }
  595. // Returns boolean to indicate if node was updated
  596. expand(node: TraceTreeNode<TraceTree.NodeValue>, expanded: boolean): boolean {
  597. if (expanded === node.expanded) {
  598. return false;
  599. }
  600. // Expanding is not allowed for zoomed in nodes
  601. if (node.zoomedIn) {
  602. return false;
  603. }
  604. if (node instanceof ParentAutogroupNode) {
  605. // In parent autogrouping, we perform a node swap and either point the
  606. // head or tails of the autogrouped sequence to the autogrouped node
  607. if (node.expanded) {
  608. const index = this._list.indexOf(node);
  609. const autogroupedChildren = node.getVisibleChildren();
  610. this._list.splice(index + 1, autogroupedChildren.length);
  611. const newChildren = node.tail.getVisibleChildren();
  612. for (const c of node.tail.children) {
  613. c.parent = node;
  614. }
  615. this._list.splice(index + 1, 0, ...newChildren);
  616. } else {
  617. node.head.parent = node;
  618. const index = this._list.indexOf(node);
  619. const childrenCount = node.getVisibleChildrenCount();
  620. this._list.splice(index + 1, childrenCount);
  621. node.getVisibleChildrenCount();
  622. const newChildren = [node.head].concat(
  623. node.head.getVisibleChildren() as TraceTreeNode<TraceTree.Span>[]
  624. );
  625. for (const c of node.children) {
  626. c.parent = node.tail;
  627. }
  628. this._list.splice(index + 1, 0, ...newChildren);
  629. }
  630. node.invalidate(node);
  631. node.expanded = expanded;
  632. return true;
  633. }
  634. if (node.expanded) {
  635. const index = this._list.indexOf(node);
  636. this._list.splice(index + 1, node.getVisibleChildrenCount());
  637. // Flip expanded after collecting visible children
  638. node.expanded = expanded;
  639. } else {
  640. const index = this._list.indexOf(node);
  641. // Flip expanded so that we can collect visible children
  642. node.expanded = expanded;
  643. this._list.splice(index + 1, 0, ...node.getVisibleChildren());
  644. }
  645. node.expanded = expanded;
  646. return true;
  647. }
  648. zoomIn(
  649. node: TraceTreeNode<TraceTree.NodeValue>,
  650. zoomedIn: boolean,
  651. options: {
  652. api: Client;
  653. organization: Organization;
  654. }
  655. ): Promise<Event | null> {
  656. if (zoomedIn === node.zoomedIn) {
  657. return Promise.resolve(null);
  658. }
  659. if (!zoomedIn) {
  660. const index = this._list.indexOf(node);
  661. const childrenCount = node.getVisibleChildrenCount();
  662. this._list.splice(index + 1, childrenCount);
  663. node.zoomedIn = zoomedIn;
  664. node.invalidate(node);
  665. if (node.expanded) {
  666. this._list.splice(index + 1, 0, ...node.getVisibleChildren());
  667. }
  668. return Promise.resolve(null);
  669. }
  670. const key = cacheKey(
  671. options.organization,
  672. node.metadata.project_slug!,
  673. node.metadata.event_id!
  674. );
  675. const promise =
  676. this._spanPromises.get(key) ??
  677. fetchTransactionSpans(
  678. options.api,
  679. options.organization,
  680. node.metadata.project_slug!,
  681. node.metadata.event_id!
  682. );
  683. node.fetchStatus = 'loading';
  684. promise
  685. .then(data => {
  686. node.fetchStatus = 'resolved';
  687. const spans = data.entries.find(s => s.type === 'spans');
  688. if (!spans) {
  689. return data;
  690. }
  691. // Remove existing entries from the list
  692. const index = this._list.indexOf(node);
  693. if (node.expanded) {
  694. const childrenCount = node.getVisibleChildrenCount();
  695. this._list.splice(index + 1, childrenCount);
  696. }
  697. // Api response is not sorted
  698. if (spans.data) {
  699. spans.data.sort((a, b) => a.start_timestamp - b.start_timestamp);
  700. }
  701. TraceTree.FromSpans(node, data, spans.data, {sdk: data.sdk?.name});
  702. const spanChildren = node.getVisibleChildren();
  703. this._list.splice(index + 1, 0, ...spanChildren);
  704. return data;
  705. })
  706. .catch(_e => {
  707. node.fetchStatus = 'error';
  708. });
  709. this._spanPromises.set(key, promise);
  710. return promise;
  711. }
  712. toList(): TraceTreeNode<TraceTree.NodeValue>[] {
  713. const list: TraceTreeNode<TraceTree.NodeValue>[] = [];
  714. function visit(node: TraceTreeNode<TraceTree.NodeValue>) {
  715. list.push(node);
  716. if (!node.expanded) {
  717. return;
  718. }
  719. for (const child of node.children) {
  720. visit(child);
  721. }
  722. }
  723. for (const child of this.root.children) {
  724. visit(child);
  725. }
  726. return list;
  727. }
  728. get list(): ReadonlyArray<TraceTreeNode<TraceTree.NodeValue>> {
  729. return this._list;
  730. }
  731. /**
  732. * Prints the tree in a human readable format, useful for debugging and testing
  733. */
  734. print() {
  735. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  736. const print = this.list
  737. .map(t => printNode(t, 0))
  738. .filter(Boolean)
  739. .join('\n');
  740. // eslint-disable-next-line no-console
  741. console.log(print);
  742. }
  743. build() {
  744. this._list = this.toList();
  745. return this;
  746. }
  747. }
  748. export class TraceTreeNode<T extends TraceTree.NodeValue> {
  749. canFetch: boolean = false;
  750. fetchStatus: 'resolved' | 'error' | 'idle' | 'loading' = 'idle';
  751. parent: TraceTreeNode<TraceTree.NodeValue> | null = null;
  752. value: T;
  753. expanded: boolean = false;
  754. zoomedIn: boolean = false;
  755. metadata: TraceTree.Metadata = {
  756. project_slug: undefined,
  757. event_id: undefined,
  758. };
  759. space: [number, number] | null = null;
  760. multiplier: number;
  761. private unit: 'milliseconds' = 'milliseconds';
  762. private _depth: number | undefined;
  763. private _children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  764. private _spanChildren: TraceTreeNode<
  765. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  766. >[] = [];
  767. private _connectors: number[] | undefined = undefined;
  768. constructor(
  769. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  770. value: T,
  771. metadata: TraceTree.Metadata
  772. ) {
  773. this.parent = parent ?? null;
  774. this.value = value;
  775. this.metadata = metadata;
  776. this.multiplier = this.unit === 'milliseconds' ? 1e3 : 1;
  777. if (value && 'timestamp' in value && 'start_timestamp' in value) {
  778. this.space = [
  779. value.start_timestamp * this.multiplier,
  780. (value.timestamp - value.start_timestamp) * this.multiplier,
  781. ];
  782. }
  783. if (isTransactionNode(this) || isTraceNode(this) || isSpanNode(this)) {
  784. this.expanded = true;
  785. }
  786. }
  787. cloneDeep(): TraceTreeNode<T> | ParentAutogroupNode | SiblingAutogroupNode {
  788. let node: TraceTreeNode<T> | ParentAutogroupNode | SiblingAutogroupNode;
  789. if (isParentAutogroupedNode(this)) {
  790. node = new ParentAutogroupNode(
  791. this.parent,
  792. this.value,
  793. this.metadata,
  794. this.head,
  795. this.tail
  796. );
  797. node.groupCount = this.groupCount;
  798. } else {
  799. node = new TraceTreeNode(this.parent, this.value, this.metadata);
  800. }
  801. if (!node) {
  802. throw new Error('CloneDeep is not implemented');
  803. }
  804. node.expanded = this.expanded;
  805. node.zoomedIn = this.zoomedIn;
  806. node.canFetch = this.canFetch;
  807. node.space = this.space;
  808. node.metadata = this.metadata;
  809. if (isParentAutogroupedNode(node)) {
  810. node.head = node.head.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  811. node.tail = node.tail.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  812. for (const child of node.head.children) {
  813. child.parent = node;
  814. }
  815. for (const child of node.tail.children) {
  816. child.parent = node;
  817. }
  818. node.head.parent = node;
  819. node.tail.parent = node;
  820. } else {
  821. for (const child of this.children) {
  822. const childClone = child.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  823. node.children.push(childClone);
  824. childClone.parent = node;
  825. }
  826. }
  827. return node;
  828. }
  829. get isOrphaned() {
  830. return this.parent?.value && 'orphan_errors' in this.parent.value;
  831. }
  832. get isLastChild() {
  833. if (!this.parent || this.parent.children.length === 0) {
  834. return true;
  835. }
  836. return this.parent.children[this.parent.children.length - 1] === this;
  837. }
  838. /**
  839. * Return a lazily calculated depth of the node in the tree.
  840. * Root node has a value of -1 as it is abstract.
  841. */
  842. get depth(): number {
  843. if (typeof this._depth === 'number') {
  844. return this._depth;
  845. }
  846. let depth = -2;
  847. let node: TraceTreeNode<any> | null = this;
  848. while (node) {
  849. if (typeof node.parent?.depth === 'number') {
  850. this._depth = node.parent.depth + 1;
  851. return this._depth;
  852. }
  853. depth++;
  854. node = node.parent;
  855. }
  856. this._depth = depth;
  857. return this._depth;
  858. }
  859. /**
  860. * Returns the depth levels at which the row should draw vertical connectors
  861. * negative values mean connector points to an orphaned node
  862. */
  863. get connectors(): number[] {
  864. if (this._connectors !== undefined) {
  865. return this._connectors!;
  866. }
  867. this._connectors = [];
  868. if (!this.parent) {
  869. return this._connectors;
  870. }
  871. if (this.parent?.connectors !== undefined) {
  872. this._connectors = [...this.parent.connectors];
  873. if (this.isLastChild || this.value === null) {
  874. return this._connectors;
  875. }
  876. this.connectors.push(this.isOrphaned ? -this.depth : this.depth);
  877. return this._connectors;
  878. }
  879. let node: TraceTreeNode<T> | TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
  880. while (node) {
  881. if (node.value === null) {
  882. break;
  883. }
  884. if (node.isLastChild) {
  885. node = node.parent;
  886. continue;
  887. }
  888. this._connectors.push(node.isOrphaned ? -node.depth : node.depth);
  889. node = node.parent;
  890. }
  891. return this._connectors;
  892. }
  893. /**
  894. * Returns the children that the node currently points to.
  895. * The logic here is a consequence of the tree design, where we want to be able to store
  896. * both transaction and span nodes in the same tree. This results in an annoying API where
  897. * we either store span children separately or transaction children separately. A better design
  898. * would have been to create an invisible meta node that always points to the correct children.
  899. */
  900. get children(): TraceTreeNode<TraceTree.NodeValue>[] {
  901. if (isAutogroupedNode(this)) {
  902. return this._children;
  903. }
  904. if (isSpanNode(this)) {
  905. return this.canFetch && !this.zoomedIn ? [] : this.spanChildren;
  906. }
  907. if (isTransactionNode(this)) {
  908. return this.zoomedIn ? this._spanChildren : this._children;
  909. }
  910. return this._children;
  911. }
  912. set children(children: TraceTreeNode<TraceTree.NodeValue>[]) {
  913. this._children = children;
  914. }
  915. get spanChildren(): TraceTreeNode<
  916. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  917. >[] {
  918. return this._spanChildren;
  919. }
  920. /**
  921. * Invalidate the visual data used to render the tree, forcing it
  922. * to be recalculated on the next render. This is useful when for example
  923. * the tree is expanded or collapsed, or when the tree is mutated and
  924. * the visual data is no longer valid as the indentation changes
  925. */
  926. invalidate(root?: TraceTreeNode<TraceTree.NodeValue>) {
  927. this._connectors = undefined;
  928. this._depth = undefined;
  929. if (root) {
  930. const queue = [...this.children];
  931. if (isParentAutogroupedNode(this)) {
  932. queue.push(this.head);
  933. }
  934. while (queue.length > 0) {
  935. const next = queue.pop()!;
  936. next.invalidate();
  937. if (isParentAutogroupedNode(next)) {
  938. queue.push(next.head);
  939. }
  940. for (let i = 0; i < next.children.length; i++) {
  941. queue.push(next.children[i]);
  942. }
  943. }
  944. }
  945. }
  946. getVisibleChildrenCount(): number {
  947. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  948. let count = 0;
  949. if (
  950. this.expanded ||
  951. isParentAutogroupedNode(this) ||
  952. isMissingInstrumentationNode(this)
  953. ) {
  954. for (let i = this.children.length - 1; i >= 0; i--) {
  955. stack.push(this.children[i]);
  956. }
  957. }
  958. while (stack.length > 0) {
  959. const node = stack.pop()!;
  960. count++;
  961. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  962. // to ensure they are processed in the original left-to-right order.
  963. if (node.expanded || isParentAutogroupedNode(node)) {
  964. for (let i = node.children.length - 1; i >= 0; i--) {
  965. stack.push(node.children[i]);
  966. }
  967. }
  968. }
  969. return count;
  970. }
  971. getVisibleChildren(): TraceTreeNode<TraceTree.NodeValue>[] {
  972. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  973. const children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  974. if (
  975. this.expanded ||
  976. isParentAutogroupedNode(this) ||
  977. isMissingInstrumentationNode(this)
  978. ) {
  979. for (let i = this.children.length - 1; i >= 0; i--) {
  980. stack.push(this.children[i]);
  981. }
  982. }
  983. while (stack.length > 0) {
  984. const node = stack.pop()!;
  985. children.push(node);
  986. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  987. // to ensure they are processed in the original left-to-right order.
  988. if (node.expanded || isParentAutogroupedNode(node)) {
  989. for (let i = node.children.length - 1; i >= 0; i--) {
  990. stack.push(node.children[i]);
  991. }
  992. }
  993. }
  994. return children;
  995. }
  996. // Returns the min path required to reach the node from the root.
  997. // @TODO: skip nodes that do not require fetching
  998. get path(): TraceTree.NodePath[] {
  999. const nodes: TraceTreeNode<TraceTree.NodeValue>[] = [this];
  1000. let current: TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
  1001. if (isSpanNode(this) || isAutogroupedNode(this)) {
  1002. while (
  1003. current &&
  1004. (isSpanNode(current) || (isAutogroupedNode(current) && !current.expanded))
  1005. ) {
  1006. current = current.parent;
  1007. }
  1008. }
  1009. while (current) {
  1010. if (isTransactionNode(current)) {
  1011. nodes.push(current);
  1012. }
  1013. if (isSpanNode(current)) {
  1014. nodes.push(current);
  1015. while (current.parent) {
  1016. if (isTransactionNode(current.parent)) {
  1017. break;
  1018. }
  1019. if (isAutogroupedNode(current.parent) && current.parent.expanded) {
  1020. break;
  1021. }
  1022. current = current.parent;
  1023. }
  1024. }
  1025. if (isAutogroupedNode(current)) {
  1026. nodes.push(current);
  1027. }
  1028. current = current.parent;
  1029. }
  1030. return nodes.map(nodeToId);
  1031. }
  1032. print() {
  1033. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  1034. const offset = this.depth === -1 ? 1 : 0;
  1035. const nodes = [this, ...this.getVisibleChildren()];
  1036. const print = nodes
  1037. .map(t => printNode(t, offset))
  1038. .filter(Boolean)
  1039. .join('\n');
  1040. // eslint-disable-next-line no-console
  1041. console.log(print);
  1042. }
  1043. static Find(
  1044. root: TraceTreeNode<TraceTree.NodeValue>,
  1045. predicate: (node: TraceTreeNode<TraceTree.NodeValue>) => boolean
  1046. ): TraceTreeNode<TraceTree.NodeValue> | null {
  1047. const queue = [root];
  1048. while (queue.length > 0) {
  1049. const next = queue.pop()!;
  1050. if (predicate(next)) return next;
  1051. for (const child of next.children) {
  1052. queue.push(child);
  1053. }
  1054. }
  1055. return null;
  1056. }
  1057. static Root() {
  1058. return new TraceTreeNode(null, null, {
  1059. event_id: undefined,
  1060. project_slug: undefined,
  1061. });
  1062. }
  1063. }
  1064. export class MissingInstrumentationNode extends TraceTreeNode<TraceTree.MissingInstrumentationSpan> {
  1065. next: TraceTreeNode<TraceTree.Span>;
  1066. previous: TraceTreeNode<TraceTree.Span>;
  1067. constructor(
  1068. parent: TraceTreeNode<TraceTree.NodeValue>,
  1069. node: TraceTree.MissingInstrumentationSpan,
  1070. metadata: TraceTree.Metadata,
  1071. previous: TraceTreeNode<TraceTree.Span>,
  1072. next: TraceTreeNode<TraceTree.Span>
  1073. ) {
  1074. super(parent, node, metadata);
  1075. this.next = next;
  1076. this.previous = previous;
  1077. }
  1078. }
  1079. export class ParentAutogroupNode extends TraceTreeNode<TraceTree.ChildrenAutogroup> {
  1080. head: TraceTreeNode<TraceTree.Span>;
  1081. tail: TraceTreeNode<TraceTree.Span>;
  1082. errored_children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  1083. groupCount: number = 0;
  1084. private _autogroupedSegments: [number, number][] | undefined;
  1085. constructor(
  1086. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  1087. node: TraceTree.ChildrenAutogroup,
  1088. metadata: TraceTree.Metadata,
  1089. head: TraceTreeNode<TraceTree.Span>,
  1090. tail: TraceTreeNode<TraceTree.Span>
  1091. ) {
  1092. super(parent, node, metadata);
  1093. this.expanded = false;
  1094. this.head = head;
  1095. this.tail = tail;
  1096. }
  1097. get children() {
  1098. if (this.expanded) {
  1099. return [this.head];
  1100. }
  1101. return this.tail.children;
  1102. }
  1103. get has_error(): boolean {
  1104. return this.errored_children.length > 0;
  1105. }
  1106. get autogroupedSegments(): [number, number][] {
  1107. if (this._autogroupedSegments) {
  1108. return this._autogroupedSegments;
  1109. }
  1110. const children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  1111. let start: TraceTreeNode<TraceTree.NodeValue> | undefined = this.head;
  1112. while (start && start !== this.tail) {
  1113. children.push(start);
  1114. start = start.children[0];
  1115. }
  1116. children.push(this.tail);
  1117. this._autogroupedSegments = computeAutogroupedBarSegments(children);
  1118. return this._autogroupedSegments;
  1119. }
  1120. }
  1121. export class SiblingAutogroupNode extends TraceTreeNode<TraceTree.SiblingAutogroup> {
  1122. groupCount: number = 0;
  1123. errored_children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  1124. private _autogroupedSegments: [number, number][] | undefined;
  1125. constructor(
  1126. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  1127. node: TraceTree.SiblingAutogroup,
  1128. metadata: TraceTree.Metadata
  1129. ) {
  1130. super(parent, node, metadata);
  1131. this.expanded = false;
  1132. }
  1133. get has_error(): boolean {
  1134. return this.errored_children.length > 0;
  1135. }
  1136. get autogroupedSegments(): [number, number][] {
  1137. if (this._autogroupedSegments) {
  1138. return this._autogroupedSegments;
  1139. }
  1140. this._autogroupedSegments = computeAutogroupedBarSegments(this.children);
  1141. return this._autogroupedSegments;
  1142. }
  1143. }
  1144. function partialTransaction(
  1145. partial: Partial<TraceTree.Transaction>
  1146. ): TraceTree.Transaction {
  1147. return {
  1148. start_timestamp: 0,
  1149. timestamp: 0,
  1150. errors: [],
  1151. performance_issues: [],
  1152. parent_span_id: '',
  1153. span_id: '',
  1154. parent_event_id: '',
  1155. project_id: 0,
  1156. 'transaction.duration': 0,
  1157. 'transaction.op': 'db',
  1158. 'transaction.status': 'ok',
  1159. generation: 0,
  1160. project_slug: '',
  1161. event_id: `event_id`,
  1162. transaction: `transaction`,
  1163. children: [],
  1164. ...partial,
  1165. };
  1166. }
  1167. export function makeExampleTrace(metadata: TraceTree.Metadata): TraceTree {
  1168. const trace: TraceTree.Trace = {
  1169. transactions: [],
  1170. orphan_errors: [],
  1171. };
  1172. function randomBetween(min: number, max: number) {
  1173. return Math.floor(Math.random() * (max - min + 1) + min);
  1174. }
  1175. let start = new Date().getTime();
  1176. const root = partialTransaction({
  1177. ...metadata,
  1178. generation: 0,
  1179. start_timestamp: start,
  1180. transaction: 'root transaction',
  1181. timestamp: start + randomBetween(100, 200),
  1182. });
  1183. trace.transactions.push(root);
  1184. for (let i = 0; i < 25; i++) {
  1185. const end = start + randomBetween(100, 200);
  1186. const nest = i > 0 && Math.random() > 0.33;
  1187. if (nest) {
  1188. const parent = root.children[root.children.length - 1];
  1189. parent.children.push(
  1190. partialTransaction({
  1191. ...metadata,
  1192. generation: 0,
  1193. start_timestamp: start,
  1194. transaction: `parent transaction ${i}`,
  1195. timestamp: end,
  1196. })
  1197. );
  1198. parent.timestamp = end;
  1199. } else {
  1200. root.children.push(
  1201. partialTransaction({
  1202. ...metadata,
  1203. generation: 0,
  1204. start_timestamp: start,
  1205. transaction: 'loading...',
  1206. ['transaction.op']: 'loading',
  1207. timestamp: end,
  1208. })
  1209. );
  1210. }
  1211. start = end;
  1212. }
  1213. const tree = TraceTree.FromTrace(trace);
  1214. return tree;
  1215. }
  1216. function nodeToId(n: TraceTreeNode<TraceTree.NodeValue>): TraceTree.NodePath {
  1217. if (isTransactionNode(n)) {
  1218. return `txn:${n.value.event_id}`;
  1219. }
  1220. if (isSpanNode(n)) {
  1221. return `span:${n.value.span_id}`;
  1222. }
  1223. if (isAutogroupedNode(n)) {
  1224. if (isParentAutogroupedNode(n)) {
  1225. return `ag:${n.head.value.span_id}`;
  1226. }
  1227. if (isSiblingAutogroupedNode(n)) {
  1228. const child = n.children[0];
  1229. if (isSpanNode(child)) {
  1230. return `ag:${child.value.span_id}`;
  1231. }
  1232. }
  1233. }
  1234. if (isTraceNode(n)) {
  1235. return `trace:root`;
  1236. }
  1237. if (isTraceErrorNode(n)) {
  1238. return `error:${n.value.event_id}`;
  1239. }
  1240. if (isRootNode(n)) {
  1241. throw new Error('A path to root node does not exist as the node is virtual');
  1242. }
  1243. if (isMissingInstrumentationNode(n)) {
  1244. if (n.previous) {
  1245. return `ms:${n.previous.value.span_id}`;
  1246. }
  1247. if (n.next) {
  1248. return `ms:${n.next.value.span_id}`;
  1249. }
  1250. throw new Error('Missing instrumentation node must have a previous or next node');
  1251. }
  1252. throw new Error('Not implemented');
  1253. }
  1254. export function computeAutogroupedBarSegments(
  1255. nodes: TraceTreeNode<TraceTree.NodeValue>[]
  1256. ): [number, number][] {
  1257. if (nodes.length === 0) {
  1258. return [];
  1259. }
  1260. if (nodes.length === 1) {
  1261. const space = nodes[0].space;
  1262. if (!space) {
  1263. throw new Error(
  1264. 'Autogrouped node child has no defined space. This should not happen.'
  1265. );
  1266. }
  1267. return [space];
  1268. }
  1269. const first = nodes[0];
  1270. const multiplier = first.multiplier;
  1271. if (!isSpanNode(first)) {
  1272. throw new Error('Autogrouped node must have span children');
  1273. }
  1274. const segments: [number, number][] = [];
  1275. let start = first.value.start_timestamp;
  1276. let end = first.value.timestamp;
  1277. let i = 1;
  1278. while (i < nodes.length) {
  1279. const next = nodes[i];
  1280. if (!isSpanNode(next)) {
  1281. throw new Error('Autogrouped node must have span children');
  1282. }
  1283. if (next.value.start_timestamp > end) {
  1284. segments.push([start * multiplier, (end - start) * multiplier]);
  1285. start = next.value.start_timestamp;
  1286. end = next.value.timestamp;
  1287. i++;
  1288. } else {
  1289. end = next.value.timestamp;
  1290. i++;
  1291. }
  1292. }
  1293. segments.push([start * multiplier, (end - start) * multiplier]);
  1294. return segments;
  1295. }
  1296. // Returns a list of errors or performance issues related to the txn
  1297. // with ids matching the span id
  1298. function getSpanErrorsOrIssuesFromTransaction(
  1299. span: RawSpanType,
  1300. txn: TraceTree.Transaction
  1301. ): TraceErrorOrIssue[] {
  1302. if (!txn.performance_issues.length && !txn.errors.length) {
  1303. return [];
  1304. }
  1305. const errorsOrIssues: TraceErrorOrIssue[] = [];
  1306. for (const perfIssue of txn.performance_issues) {
  1307. for (const s of perfIssue.span) {
  1308. if (s === span.span_id) {
  1309. errorsOrIssues.push(perfIssue);
  1310. }
  1311. }
  1312. for (const suspect of perfIssue.suspect_spans) {
  1313. if (suspect === span.span_id) {
  1314. errorsOrIssues.push(perfIssue);
  1315. }
  1316. }
  1317. }
  1318. for (const error of txn.errors) {
  1319. if (error.span === span.span_id) {
  1320. errorsOrIssues.push(error);
  1321. }
  1322. }
  1323. return errorsOrIssues;
  1324. }
  1325. function printNode(t: TraceTreeNode<TraceTree.NodeValue>, offset: number): string {
  1326. // +1 because we may be printing from the root which is -1 indexed
  1327. const padding = ' '.repeat(t.depth + offset);
  1328. if (isAutogroupedNode(t)) {
  1329. if (isParentAutogroupedNode(t)) {
  1330. return padding + `parent autogroup (${t.groupCount})`;
  1331. }
  1332. if (isSiblingAutogroupedNode(t)) {
  1333. return padding + `sibling autogroup (${t.groupCount})`;
  1334. }
  1335. return padding + 'autogroup';
  1336. }
  1337. if (isSpanNode(t)) {
  1338. return padding + (t.value.op || t.value.span_id || 'unknown span');
  1339. }
  1340. if (isTransactionNode(t)) {
  1341. return padding + (t.value.transaction || 'unknown transaction');
  1342. }
  1343. if (isMissingInstrumentationNode(t)) {
  1344. return padding + 'missing_instrumentation';
  1345. }
  1346. if (isRootNode(t)) {
  1347. return padding + 'Root';
  1348. }
  1349. if (isTraceNode(t)) {
  1350. return padding + 'Trace';
  1351. }
  1352. if (isTraceErrorNode(t)) {
  1353. return padding + t.value.event_id || 'unknown trace error';
  1354. }
  1355. return 'unknown node';
  1356. }