traceTree.tsx 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620
  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 extends RawSpanType {
  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 (
  784. isTraceErrorNode(this) &&
  785. 'timestamp' in this.value &&
  786. typeof this.value.timestamp === 'number'
  787. ) {
  788. this.space = [this.value.timestamp * this.multiplier, 0];
  789. }
  790. if (isTransactionNode(this) || isTraceNode(this) || isSpanNode(this)) {
  791. this.expanded = true;
  792. }
  793. }
  794. cloneDeep(): TraceTreeNode<T> | ParentAutogroupNode | SiblingAutogroupNode {
  795. let node: TraceTreeNode<T> | ParentAutogroupNode | SiblingAutogroupNode;
  796. if (isParentAutogroupedNode(this)) {
  797. node = new ParentAutogroupNode(
  798. this.parent,
  799. this.value,
  800. this.metadata,
  801. this.head,
  802. this.tail
  803. );
  804. node.groupCount = this.groupCount;
  805. } else {
  806. node = new TraceTreeNode(this.parent, this.value, this.metadata);
  807. }
  808. if (!node) {
  809. throw new Error('CloneDeep is not implemented');
  810. }
  811. node.expanded = this.expanded;
  812. node.zoomedIn = this.zoomedIn;
  813. node.canFetch = this.canFetch;
  814. node.space = this.space;
  815. node.metadata = this.metadata;
  816. if (isParentAutogroupedNode(node)) {
  817. node.head = node.head.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  818. node.tail = node.tail.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  819. for (const child of node.head.children) {
  820. child.parent = node;
  821. }
  822. for (const child of node.tail.children) {
  823. child.parent = node;
  824. }
  825. node.head.parent = node;
  826. node.tail.parent = node;
  827. } else {
  828. for (const child of this.children) {
  829. const childClone = child.cloneDeep() as TraceTreeNode<TraceTree.Span>;
  830. node.children.push(childClone);
  831. childClone.parent = node;
  832. }
  833. }
  834. return node;
  835. }
  836. get isOrphaned() {
  837. return this.parent?.value && 'orphan_errors' in this.parent.value;
  838. }
  839. get isLastChild() {
  840. if (!this.parent || this.parent.children.length === 0) {
  841. return true;
  842. }
  843. return this.parent.children[this.parent.children.length - 1] === this;
  844. }
  845. /**
  846. * Return a lazily calculated depth of the node in the tree.
  847. * Root node has a value of -1 as it is abstract.
  848. */
  849. get depth(): number {
  850. if (typeof this._depth === 'number') {
  851. return this._depth;
  852. }
  853. let depth = -2;
  854. let node: TraceTreeNode<any> | null = this;
  855. while (node) {
  856. if (typeof node.parent?.depth === 'number') {
  857. this._depth = node.parent.depth + 1;
  858. return this._depth;
  859. }
  860. depth++;
  861. node = node.parent;
  862. }
  863. this._depth = depth;
  864. return this._depth;
  865. }
  866. /**
  867. * Returns the depth levels at which the row should draw vertical connectors
  868. * negative values mean connector points to an orphaned node
  869. */
  870. get connectors(): number[] {
  871. if (this._connectors !== undefined) {
  872. return this._connectors!;
  873. }
  874. this._connectors = [];
  875. if (!this.parent) {
  876. return this._connectors;
  877. }
  878. if (this.parent?.connectors !== undefined) {
  879. this._connectors = [...this.parent.connectors];
  880. if (this.isLastChild || this.value === null) {
  881. return this._connectors;
  882. }
  883. this.connectors.push(this.isOrphaned ? -this.depth : this.depth);
  884. return this._connectors;
  885. }
  886. let node: TraceTreeNode<T> | TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
  887. while (node) {
  888. if (node.value === null) {
  889. break;
  890. }
  891. if (node.isLastChild) {
  892. node = node.parent;
  893. continue;
  894. }
  895. this._connectors.push(node.isOrphaned ? -node.depth : node.depth);
  896. node = node.parent;
  897. }
  898. return this._connectors;
  899. }
  900. /**
  901. * Returns the children that the node currently points to.
  902. * The logic here is a consequence of the tree design, where we want to be able to store
  903. * both transaction and span nodes in the same tree. This results in an annoying API where
  904. * we either store span children separately or transaction children separately. A better design
  905. * would have been to create an invisible meta node that always points to the correct children.
  906. */
  907. get children(): TraceTreeNode<TraceTree.NodeValue>[] {
  908. if (isAutogroupedNode(this)) {
  909. return this._children;
  910. }
  911. if (isSpanNode(this)) {
  912. return this.canFetch && !this.zoomedIn ? [] : this.spanChildren;
  913. }
  914. if (isTransactionNode(this)) {
  915. return this.zoomedIn ? this._spanChildren : this._children;
  916. }
  917. return this._children;
  918. }
  919. set children(children: TraceTreeNode<TraceTree.NodeValue>[]) {
  920. this._children = children;
  921. }
  922. get spanChildren(): TraceTreeNode<
  923. TraceTree.Span | TraceTree.MissingInstrumentationSpan
  924. >[] {
  925. return this._spanChildren;
  926. }
  927. /**
  928. * Invalidate the visual data used to render the tree, forcing it
  929. * to be recalculated on the next render. This is useful when for example
  930. * the tree is expanded or collapsed, or when the tree is mutated and
  931. * the visual data is no longer valid as the indentation changes
  932. */
  933. invalidate(root?: TraceTreeNode<TraceTree.NodeValue>) {
  934. this._connectors = undefined;
  935. this._depth = undefined;
  936. if (root) {
  937. const queue = [...this.children];
  938. if (isParentAutogroupedNode(this)) {
  939. queue.push(this.head);
  940. }
  941. while (queue.length > 0) {
  942. const next = queue.pop()!;
  943. next.invalidate();
  944. if (isParentAutogroupedNode(next)) {
  945. queue.push(next.head);
  946. }
  947. for (let i = 0; i < next.children.length; i++) {
  948. queue.push(next.children[i]);
  949. }
  950. }
  951. }
  952. }
  953. getVisibleChildrenCount(): number {
  954. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  955. let count = 0;
  956. if (
  957. this.expanded ||
  958. isParentAutogroupedNode(this) ||
  959. isMissingInstrumentationNode(this)
  960. ) {
  961. for (let i = this.children.length - 1; i >= 0; i--) {
  962. stack.push(this.children[i]);
  963. }
  964. }
  965. while (stack.length > 0) {
  966. const node = stack.pop()!;
  967. count++;
  968. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  969. // to ensure they are processed in the original left-to-right order.
  970. if (node.expanded || isParentAutogroupedNode(node)) {
  971. for (let i = node.children.length - 1; i >= 0; i--) {
  972. stack.push(node.children[i]);
  973. }
  974. }
  975. }
  976. return count;
  977. }
  978. getVisibleChildren(): TraceTreeNode<TraceTree.NodeValue>[] {
  979. const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
  980. const children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  981. if (
  982. this.expanded ||
  983. isParentAutogroupedNode(this) ||
  984. isMissingInstrumentationNode(this)
  985. ) {
  986. for (let i = this.children.length - 1; i >= 0; i--) {
  987. stack.push(this.children[i]);
  988. }
  989. }
  990. while (stack.length > 0) {
  991. const node = stack.pop()!;
  992. children.push(node);
  993. // Since we're using a stack and it's LIFO, reverse the children before pushing them
  994. // to ensure they are processed in the original left-to-right order.
  995. if (node.expanded || isParentAutogroupedNode(node)) {
  996. for (let i = node.children.length - 1; i >= 0; i--) {
  997. stack.push(node.children[i]);
  998. }
  999. }
  1000. }
  1001. return children;
  1002. }
  1003. // Returns the min path required to reach the node from the root.
  1004. // @TODO: skip nodes that do not require fetching
  1005. get path(): TraceTree.NodePath[] {
  1006. const nodes: TraceTreeNode<TraceTree.NodeValue>[] = [this];
  1007. let current: TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
  1008. if (isSpanNode(this) || isAutogroupedNode(this)) {
  1009. while (
  1010. current &&
  1011. (isSpanNode(current) || (isAutogroupedNode(current) && !current.expanded))
  1012. ) {
  1013. current = current.parent;
  1014. }
  1015. }
  1016. while (current) {
  1017. if (isTransactionNode(current)) {
  1018. nodes.push(current);
  1019. }
  1020. if (isSpanNode(current)) {
  1021. nodes.push(current);
  1022. while (current.parent) {
  1023. if (isTransactionNode(current.parent)) {
  1024. break;
  1025. }
  1026. if (isAutogroupedNode(current.parent) && current.parent.expanded) {
  1027. break;
  1028. }
  1029. current = current.parent;
  1030. }
  1031. }
  1032. if (isAutogroupedNode(current)) {
  1033. nodes.push(current);
  1034. }
  1035. current = current.parent;
  1036. }
  1037. return nodes.map(nodeToId);
  1038. }
  1039. print() {
  1040. // root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
  1041. const offset = this.depth === -1 ? 1 : 0;
  1042. const nodes = [this, ...this.getVisibleChildren()];
  1043. const print = nodes
  1044. .map(t => printNode(t, offset))
  1045. .filter(Boolean)
  1046. .join('\n');
  1047. // eslint-disable-next-line no-console
  1048. console.log(print);
  1049. }
  1050. static Find(
  1051. root: TraceTreeNode<TraceTree.NodeValue>,
  1052. predicate: (node: TraceTreeNode<TraceTree.NodeValue>) => boolean
  1053. ): TraceTreeNode<TraceTree.NodeValue> | null {
  1054. const queue = [root];
  1055. while (queue.length > 0) {
  1056. const next = queue.pop()!;
  1057. if (predicate(next)) return next;
  1058. for (const child of next.children) {
  1059. queue.push(child);
  1060. }
  1061. }
  1062. return null;
  1063. }
  1064. static Root() {
  1065. return new TraceTreeNode(null, null, {
  1066. event_id: undefined,
  1067. project_slug: undefined,
  1068. });
  1069. }
  1070. }
  1071. export class MissingInstrumentationNode extends TraceTreeNode<TraceTree.MissingInstrumentationSpan> {
  1072. next: TraceTreeNode<TraceTree.Span>;
  1073. previous: TraceTreeNode<TraceTree.Span>;
  1074. constructor(
  1075. parent: TraceTreeNode<TraceTree.NodeValue>,
  1076. node: TraceTree.MissingInstrumentationSpan,
  1077. metadata: TraceTree.Metadata,
  1078. previous: TraceTreeNode<TraceTree.Span>,
  1079. next: TraceTreeNode<TraceTree.Span>
  1080. ) {
  1081. super(parent, node, metadata);
  1082. this.next = next;
  1083. this.previous = previous;
  1084. }
  1085. }
  1086. export class ParentAutogroupNode extends TraceTreeNode<TraceTree.ChildrenAutogroup> {
  1087. head: TraceTreeNode<TraceTree.Span>;
  1088. tail: TraceTreeNode<TraceTree.Span>;
  1089. errored_children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  1090. groupCount: number = 0;
  1091. private _autogroupedSegments: [number, number][] | undefined;
  1092. constructor(
  1093. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  1094. node: TraceTree.ChildrenAutogroup,
  1095. metadata: TraceTree.Metadata,
  1096. head: TraceTreeNode<TraceTree.Span>,
  1097. tail: TraceTreeNode<TraceTree.Span>
  1098. ) {
  1099. super(parent, node, metadata);
  1100. this.expanded = false;
  1101. this.head = head;
  1102. this.tail = tail;
  1103. }
  1104. get children() {
  1105. if (this.expanded) {
  1106. return [this.head];
  1107. }
  1108. return this.tail.children;
  1109. }
  1110. get has_error(): boolean {
  1111. return this.errored_children.length > 0;
  1112. }
  1113. get autogroupedSegments(): [number, number][] {
  1114. if (this._autogroupedSegments) {
  1115. return this._autogroupedSegments;
  1116. }
  1117. const children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  1118. let start: TraceTreeNode<TraceTree.NodeValue> | undefined = this.head;
  1119. while (start && start !== this.tail) {
  1120. children.push(start);
  1121. start = start.children[0];
  1122. }
  1123. children.push(this.tail);
  1124. this._autogroupedSegments = computeAutogroupedBarSegments(children);
  1125. return this._autogroupedSegments;
  1126. }
  1127. }
  1128. export class SiblingAutogroupNode extends TraceTreeNode<TraceTree.SiblingAutogroup> {
  1129. groupCount: number = 0;
  1130. errored_children: TraceTreeNode<TraceTree.NodeValue>[] = [];
  1131. private _autogroupedSegments: [number, number][] | undefined;
  1132. constructor(
  1133. parent: TraceTreeNode<TraceTree.NodeValue> | null,
  1134. node: TraceTree.SiblingAutogroup,
  1135. metadata: TraceTree.Metadata
  1136. ) {
  1137. super(parent, node, metadata);
  1138. this.expanded = false;
  1139. }
  1140. get has_error(): boolean {
  1141. return this.errored_children.length > 0;
  1142. }
  1143. get autogroupedSegments(): [number, number][] {
  1144. if (this._autogroupedSegments) {
  1145. return this._autogroupedSegments;
  1146. }
  1147. this._autogroupedSegments = computeAutogroupedBarSegments(this.children);
  1148. return this._autogroupedSegments;
  1149. }
  1150. }
  1151. function partialTransaction(
  1152. partial: Partial<TraceTree.Transaction>
  1153. ): TraceTree.Transaction {
  1154. return {
  1155. start_timestamp: 0,
  1156. timestamp: 0,
  1157. errors: [],
  1158. performance_issues: [],
  1159. parent_span_id: '',
  1160. span_id: '',
  1161. parent_event_id: '',
  1162. project_id: 0,
  1163. 'transaction.duration': 0,
  1164. 'transaction.op': 'db',
  1165. 'transaction.status': 'ok',
  1166. generation: 0,
  1167. project_slug: '',
  1168. event_id: `event_id`,
  1169. transaction: `transaction`,
  1170. children: [],
  1171. ...partial,
  1172. };
  1173. }
  1174. export function makeExampleTrace(metadata: TraceTree.Metadata): TraceTree {
  1175. const trace: TraceTree.Trace = {
  1176. transactions: [],
  1177. orphan_errors: [],
  1178. };
  1179. function randomBetween(min: number, max: number) {
  1180. return Math.floor(Math.random() * (max - min + 1) + min);
  1181. }
  1182. let start = new Date().getTime();
  1183. const root = partialTransaction({
  1184. ...metadata,
  1185. generation: 0,
  1186. start_timestamp: start,
  1187. transaction: 'root transaction',
  1188. timestamp: start + randomBetween(100, 200),
  1189. });
  1190. trace.transactions.push(root);
  1191. for (let i = 0; i < 50; i++) {
  1192. const end = start + randomBetween(100, 200);
  1193. const nest = i > 0 && Math.random() > 0.33;
  1194. if (nest) {
  1195. const parent = root.children[root.children.length - 1];
  1196. parent.children.push(
  1197. partialTransaction({
  1198. ...metadata,
  1199. generation: 0,
  1200. start_timestamp: start,
  1201. transaction: `parent transaction ${i}`,
  1202. timestamp: end,
  1203. })
  1204. );
  1205. parent.timestamp = end;
  1206. } else {
  1207. root.children.push(
  1208. partialTransaction({
  1209. ...metadata,
  1210. generation: 0,
  1211. start_timestamp: start,
  1212. transaction: 'loading...',
  1213. ['transaction.op']: 'loading',
  1214. timestamp: end,
  1215. })
  1216. );
  1217. }
  1218. start = end;
  1219. }
  1220. const tree = TraceTree.FromTrace(trace);
  1221. return tree;
  1222. }
  1223. function nodeToId(n: TraceTreeNode<TraceTree.NodeValue>): TraceTree.NodePath {
  1224. if (isTransactionNode(n)) {
  1225. return `txn:${n.value.event_id}`;
  1226. }
  1227. if (isSpanNode(n)) {
  1228. return `span:${n.value.span_id}`;
  1229. }
  1230. if (isAutogroupedNode(n)) {
  1231. if (isParentAutogroupedNode(n)) {
  1232. return `ag:${n.head.value.span_id}`;
  1233. }
  1234. if (isSiblingAutogroupedNode(n)) {
  1235. const child = n.children[0];
  1236. if (isSpanNode(child)) {
  1237. return `ag:${child.value.span_id}`;
  1238. }
  1239. }
  1240. }
  1241. if (isTraceNode(n)) {
  1242. return `trace:root`;
  1243. }
  1244. if (isTraceErrorNode(n)) {
  1245. return `error:${n.value.event_id}`;
  1246. }
  1247. if (isRootNode(n)) {
  1248. throw new Error('A path to root node does not exist as the node is virtual');
  1249. }
  1250. if (isMissingInstrumentationNode(n)) {
  1251. if (n.previous) {
  1252. return `ms:${n.previous.value.span_id}`;
  1253. }
  1254. if (n.next) {
  1255. return `ms:${n.next.value.span_id}`;
  1256. }
  1257. throw new Error('Missing instrumentation node must have a previous or next node');
  1258. }
  1259. throw new Error('Not implemented');
  1260. }
  1261. export function computeAutogroupedBarSegments(
  1262. nodes: TraceTreeNode<TraceTree.NodeValue>[]
  1263. ): [number, number][] {
  1264. if (nodes.length === 0) {
  1265. return [];
  1266. }
  1267. if (nodes.length === 1) {
  1268. const space = nodes[0].space;
  1269. if (!space) {
  1270. throw new Error(
  1271. 'Autogrouped node child has no defined space. This should not happen.'
  1272. );
  1273. }
  1274. return [space];
  1275. }
  1276. const first = nodes[0];
  1277. const multiplier = first.multiplier;
  1278. if (!isSpanNode(first)) {
  1279. throw new Error('Autogrouped node must have span children');
  1280. }
  1281. const segments: [number, number][] = [];
  1282. let start = first.value.start_timestamp;
  1283. let end = first.value.timestamp;
  1284. let i = 1;
  1285. while (i < nodes.length) {
  1286. const next = nodes[i];
  1287. if (!isSpanNode(next)) {
  1288. throw new Error('Autogrouped node must have span children');
  1289. }
  1290. if (next.value.start_timestamp > end) {
  1291. segments.push([start * multiplier, (end - start) * multiplier]);
  1292. start = next.value.start_timestamp;
  1293. end = next.value.timestamp;
  1294. i++;
  1295. } else {
  1296. end = next.value.timestamp;
  1297. i++;
  1298. }
  1299. }
  1300. segments.push([start * multiplier, (end - start) * multiplier]);
  1301. return segments;
  1302. }
  1303. // Returns a list of errors or performance issues related to the txn
  1304. // with ids matching the span id
  1305. function getSpanErrorsOrIssuesFromTransaction(
  1306. span: RawSpanType,
  1307. txn: TraceTree.Transaction
  1308. ): TraceErrorOrIssue[] {
  1309. if (!txn.performance_issues.length && !txn.errors.length) {
  1310. return [];
  1311. }
  1312. const errorsOrIssues: TraceErrorOrIssue[] = [];
  1313. for (const perfIssue of txn.performance_issues) {
  1314. for (const s of perfIssue.span) {
  1315. if (s === span.span_id) {
  1316. errorsOrIssues.push(perfIssue);
  1317. }
  1318. }
  1319. for (const suspect of perfIssue.suspect_spans) {
  1320. if (suspect === span.span_id) {
  1321. errorsOrIssues.push(perfIssue);
  1322. }
  1323. }
  1324. }
  1325. for (const error of txn.errors) {
  1326. if (error.span === span.span_id) {
  1327. errorsOrIssues.push(error);
  1328. }
  1329. }
  1330. return errorsOrIssues;
  1331. }
  1332. function printNode(t: TraceTreeNode<TraceTree.NodeValue>, offset: number): string {
  1333. // +1 because we may be printing from the root which is -1 indexed
  1334. const padding = ' '.repeat(t.depth + offset);
  1335. if (isAutogroupedNode(t)) {
  1336. if (isParentAutogroupedNode(t)) {
  1337. return padding + `parent autogroup (${t.groupCount})`;
  1338. }
  1339. if (isSiblingAutogroupedNode(t)) {
  1340. return padding + `sibling autogroup (${t.groupCount})`;
  1341. }
  1342. return padding + 'autogroup';
  1343. }
  1344. if (isSpanNode(t)) {
  1345. return padding + (t.value.op || t.value.span_id || 'unknown span');
  1346. }
  1347. if (isTransactionNode(t)) {
  1348. return padding + (t.value.transaction || 'unknown transaction');
  1349. }
  1350. if (isMissingInstrumentationNode(t)) {
  1351. return padding + 'missing_instrumentation';
  1352. }
  1353. if (isRootNode(t)) {
  1354. return padding + 'Root';
  1355. }
  1356. if (isTraceNode(t)) {
  1357. return padding + 'Trace';
  1358. }
  1359. if (isTraceErrorNode(t)) {
  1360. return padding + t.value.event_id || 'unknown trace error';
  1361. }
  1362. return 'unknown node';
  1363. }