traceTree.tsx 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067
  1. import * as Sentry from '@sentry/react';
  2. import type {Location} from 'history';
  3. import * as qs from 'query-string';
  4. import type {Client} from 'sentry/api';
  5. import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
  6. import type {Event, EventTransaction, Measurement} from 'sentry/types/event';
  7. import type {Organization} from 'sentry/types/organization';
  8. import type {
  9. TraceError as TraceErrorType,
  10. TraceFullDetailed,
  11. TracePerformanceIssue as TracePerformanceIssueType,
  12. TraceSplitResults,
  13. } from 'sentry/utils/performance/quickTrace/types';
  14. import {collectTraceMeasurements} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree.measurements';
  15. import type {TracePreferencesState} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
  16. import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces';
  17. import type {ReplayRecord} from 'sentry/views/replays/types';
  18. import {isRootTransaction} from '../../traceDetails/utils';
  19. import {getTraceQueryParams} from '../traceApi/useTrace';
  20. import type {TraceMetaQueryResults} from '../traceApi/useTraceMeta';
  21. import {
  22. getPageloadTransactionChildCount,
  23. isAutogroupedNode,
  24. isBrowserRequestSpan,
  25. isJavascriptSDKTransaction,
  26. isMissingInstrumentationNode,
  27. isPageloadTransactionNode,
  28. isParentAutogroupedNode,
  29. isRootNode,
  30. isServerRequestHandlerTransactionNode,
  31. isSiblingAutogroupedNode,
  32. isSpanNode,
  33. isTraceErrorNode,
  34. isTraceNode,
  35. isTransactionNode,
  36. shouldAddMissingInstrumentationSpan,
  37. } from '../traceGuards';
  38. import {makeExampleTrace} from './makeExampleTrace';
  39. import {MissingInstrumentationNode} from './missingInstrumentationNode';
  40. import {ParentAutogroupNode} from './parentAutogroupNode';
  41. import {SiblingAutogroupNode} from './siblingAutogroupNode';
  42. import {TraceTreeEventDispatcher} from './traceTreeEventDispatcher';
  43. import {TraceTreeNode} from './traceTreeNode';
  44. /**
  45. *
  46. * This file implements the tree data structure that is used to represent a trace. We do
  47. * this both for performance reasons as well as flexibility. The requirement for a tree
  48. * is to support incremental patching and updates. This is important because we want to
  49. * be able to fetch more data as the user interacts with the tree, and we want to be able
  50. * efficiently update the tree as we receive more data.
  51. *
  52. * The trace is represented as a tree with different node value types (transaction or span)
  53. * Each tree node contains a reference to its parent and a list of references to its children,
  54. * as well as a reference to the value that the node holds. Each node also contains
  55. * some meta data and state about the node, such as if it is expanded or zoomed in. The benefit
  56. * of abstracting parts of the UI state is that the tree will persist user actions such as expanding
  57. * or collapsing nodes which would have otherwise been lost when individual nodes are remounted in the tree.
  58. *
  59. * Each tree holds a list reference, which is a live reference to a flattened representation
  60. * 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
  61. * reasons as we want to support mutations on traces with ~100k+ nodes), callers need to manage reactivity themselves.
  62. *
  63. * An alternative, but not recommended approach is to call build() on the tree after each mutation,
  64. * which will iterate over all of the children and build a fresh list reference.
  65. *
  66. * In most cases, the initial tree is a list of transactions containing other transactions. Each transaction can
  67. * then be expanded into a list of spans which can also in some cases be expanded.
  68. *
  69. * - trace - trace
  70. * |- parent transaction --> when expanding |- parent transaction
  71. * |- child transaction |- span
  72. * |- span this used to be a transaction,
  73. * |- child transaction span <- but is now be a list of spans
  74. * |- span belonging to the transaction
  75. * this results in child txns to be lost,
  76. * which is a confusing user experience
  77. *
  78. * The tree supports autogrouping of spans vertically or as siblings. When that happens, a autogrouped node of either a vertical or
  79. * sibling type is inserted as an intermediary node. In the vertical case, the autogrouped node
  80. * holds the reference to the head and tail of the autogrouped sequence. In the sibling case, the autogrouped node
  81. * holds a reference to the children that are part of the autogrouped sequence. When expanding and collapsing these nodes,
  82. * the tree perform a reference swap to either point to the head (when expanded) or tail (when collapsed) of the autogrouped sequence.
  83. *
  84. * In vertical grouping case, the following happens:
  85. *
  86. * - root - root
  87. * - trace - trace
  88. * |- transaction |- transaction
  89. * |- span 1 <-| these become autogrouped |- autogrouped (head=span1, tail=span3, children points to children of tail)
  90. * |- span 2 |- as they are inserted into |- other span (parent points to autogrouped node)
  91. * |- span 3 <-| the tree.
  92. * |- other span
  93. *
  94. * When the autogrouped node is expanded the UI needs to show the entire collapsed chain, so we swap the tail children to point
  95. * back to the tail, and have autogrouped node point to its head as the children.
  96. *
  97. * - root - root
  98. * - trace - trace
  99. * |- transaction |- transaction
  100. * |- autogrouped (head=span1, tail=span3) <- when expanding |- autogrouped (head=span1, tail=span3, children points to head)
  101. * | other span (paren points to autogrouped) |- span 1 (head)
  102. * |- span 2
  103. * |- span 3 (tail)
  104. * |- other span (children of tail, parent points to tail)
  105. *
  106. * Notes and improvements:
  107. * - the notion of expanded and zoomed is confusing, they stand for the same idea from a UI pov
  108. * - ???
  109. */
  110. export declare namespace TraceTree {
  111. interface TraceTreeEvents {
  112. ['trace timeline change']: (view: [number, number]) => void;
  113. }
  114. // Raw node values
  115. interface Span extends RawSpanType {
  116. measurements?: Record<string, Measurement>;
  117. }
  118. interface Transaction extends TraceFullDetailed {
  119. profiler_id: string;
  120. sdk_name: string;
  121. }
  122. type Trace = TraceSplitResults<Transaction>;
  123. type TraceError = TraceErrorType;
  124. type TracePerformanceIssue = TracePerformanceIssueType;
  125. type Profile = {profile_id: string} | {profiler_id: string};
  126. type Project = {
  127. id: number;
  128. slug: string;
  129. };
  130. type Root = null;
  131. // All possible node value types
  132. type NodeValue =
  133. | Trace
  134. | Transaction
  135. | TraceError
  136. | Span
  137. | MissingInstrumentationSpan
  138. | SiblingAutogroup
  139. | ChildrenAutogroup
  140. | Root;
  141. // Node types
  142. interface MissingInstrumentationSpan {
  143. start_timestamp: number;
  144. timestamp: number;
  145. type: 'missing_instrumentation';
  146. }
  147. interface SiblingAutogroup extends Span {
  148. autogrouped_by: {
  149. description: string;
  150. op: string;
  151. };
  152. }
  153. interface ChildrenAutogroup extends Span {
  154. autogrouped_by: {
  155. op: string;
  156. };
  157. }
  158. // All possible node types
  159. type Node =
  160. | TraceTreeNode<NodeValue>
  161. | ParentAutogroupNode
  162. | SiblingAutogroupNode
  163. | MissingInstrumentationNode;
  164. type NodePath =
  165. `${'txn' | 'span' | 'ag' | 'trace' | 'ms' | 'error' | 'empty'}-${string}`;
  166. type Metadata = {
  167. event_id: string | undefined;
  168. project_slug: string | undefined;
  169. spans?: number;
  170. };
  171. type Indicator = {
  172. duration: number;
  173. label: string;
  174. measurement: Measurement;
  175. poor: boolean;
  176. start: number;
  177. type: 'cls' | 'fcp' | 'fp' | 'lcp' | 'ttfb';
  178. };
  179. type CollectedVital = {key: string; measurement: Measurement};
  180. }
  181. export enum TraceShape {
  182. ONE_ROOT = 'one_root',
  183. NO_ROOT = 'no_root',
  184. BROWSER_MULTIPLE_ROOTS = 'browser_multiple_roots',
  185. MULTIPLE_ROOTS = 'multiple_roots',
  186. BROKEN_SUBTRACES = 'broken_subtraces',
  187. ONLY_ERRORS = 'only_errors',
  188. EMPTY_TRACE = 'empty_trace',
  189. }
  190. function fetchTransactionSpans(
  191. api: Client,
  192. organization: Organization,
  193. project_slug: string,
  194. event_id: string
  195. ): Promise<EventTransaction> {
  196. return api.requestPromise(
  197. `/organizations/${organization.slug}/events/${project_slug}:${event_id}/?averageColumn=span.self_time&averageColumn=span.duration`
  198. );
  199. }
  200. function fetchTrace(
  201. api: Client,
  202. params: {
  203. orgSlug: string;
  204. query: string;
  205. traceId: string;
  206. }
  207. ): Promise<TraceSplitResults<TraceTree.Transaction>> {
  208. return api.requestPromise(
  209. `/organizations/${params.orgSlug}/events-trace/${params.traceId}/?${params.query}`
  210. );
  211. }
  212. export class TraceTree extends TraceTreeEventDispatcher {
  213. eventsCount = 0;
  214. projects = new Set<TraceTree.Project>();
  215. type: 'loading' | 'empty' | 'error' | 'trace' = 'trace';
  216. root: TraceTreeNode<null> = TraceTreeNode.Root();
  217. vital_types: Set<'web' | 'mobile'> = new Set();
  218. vitals = new Map<TraceTreeNode<TraceTree.NodeValue>, TraceTree.CollectedVital[]>();
  219. profiled_events = new Set<TraceTreeNode<TraceTree.NodeValue>>();
  220. indicators: TraceTree.Indicator[] = [];
  221. list: TraceTreeNode<TraceTree.NodeValue>[] = [];
  222. events: Map<string, EventTransaction> = new Map();
  223. private _spanPromises: Map<string, Promise<EventTransaction>> = new Map();
  224. static MISSING_INSTRUMENTATION_THRESHOLD_MS = 100;
  225. static Empty() {
  226. const tree = new TraceTree().build();
  227. tree.type = 'empty';
  228. return tree;
  229. }
  230. static Loading(metadata: TraceTree.Metadata): TraceTree {
  231. const t = makeExampleTrace(metadata);
  232. t.type = 'loading';
  233. t.build();
  234. return t;
  235. }
  236. static Error(metadata: TraceTree.Metadata): TraceTree {
  237. const t = makeExampleTrace(metadata);
  238. t.type = 'error';
  239. t.build();
  240. return t;
  241. }
  242. static FromTrace(
  243. trace: TraceTree.Trace,
  244. options: {
  245. meta: TraceMetaQueryResults['data'] | null;
  246. replay: ReplayRecord | null;
  247. }
  248. ): TraceTree {
  249. const tree = new TraceTree();
  250. const traceNode = new TraceTreeNode<TraceTree.Trace>(tree.root, trace, {
  251. event_id: undefined,
  252. project_slug: undefined,
  253. });
  254. tree.root.children.push(traceNode);
  255. function visit(
  256. parent: TraceTreeNode<TraceTree.NodeValue | null>,
  257. value: TraceTree.Transaction | TraceTree.TraceError
  258. ) {
  259. tree.eventsCount++;
  260. tree.projects.add({
  261. id: value.project_id,
  262. slug: value.project_slug,
  263. });
  264. const node = new TraceTreeNode(parent, value, {
  265. spans: options.meta?.transactiontoSpanChildrenCount[value.event_id] ?? 0,
  266. project_slug: value && 'project_slug' in value ? value.project_slug : undefined,
  267. event_id: value && 'event_id' in value ? value.event_id : undefined,
  268. });
  269. if (isTransactionNode(node)) {
  270. const spanChildrenCount =
  271. options.meta?.transactiontoSpanChildrenCount[node.value.event_id];
  272. // We check for >1 events, as the first one is the transaction node itself
  273. node.canFetch = spanChildrenCount === undefined ? true : spanChildrenCount > 1;
  274. }
  275. if (!node.metadata.project_slug && !node.metadata.event_id) {
  276. const parentNodeMetadata = TraceTree.ParentTransaction(node)?.metadata;
  277. if (parentNodeMetadata) {
  278. node.metadata = {...parentNodeMetadata};
  279. }
  280. }
  281. parent.children.push(node);
  282. if (node.value && 'children' in node.value) {
  283. for (const child of node.value.children) {
  284. visit(node, child);
  285. }
  286. }
  287. }
  288. traceQueueIterator(trace, traceNode, visit);
  289. // At this point, the tree is built, we need iterate over it again to collect all of the
  290. // measurements, web vitals, errors and perf issues as well as calculate the min and max space
  291. // the trace should take up.
  292. const traceSpaceBounds = [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY];
  293. TraceTree.ForEachChild(traceNode, c => {
  294. traceSpaceBounds[0] = Math.min(traceSpaceBounds[0], c.space[0]);
  295. traceSpaceBounds[1] = Math.max(traceSpaceBounds[1], c.space[0] + c.space[1]);
  296. if (isTransactionNode(c)) {
  297. for (const error of c.value.errors) {
  298. traceNode.errors.add(error);
  299. }
  300. for (const performanceIssue of c.value.performance_issues) {
  301. traceNode.performance_issues.add(performanceIssue);
  302. }
  303. }
  304. if (isTraceErrorNode(c)) {
  305. traceNode.errors.add(c.value);
  306. }
  307. if (c.profiles.length > 0) {
  308. tree.profiled_events.add(c);
  309. }
  310. if (c.value && 'measurements' in c.value) {
  311. tree.indicators = tree.indicators.concat(
  312. collectTraceMeasurements(
  313. c,
  314. c.space[0],
  315. c.value.measurements,
  316. tree.vitals,
  317. tree.vital_types
  318. )
  319. );
  320. }
  321. if (
  322. c.parent &&
  323. isPageloadTransactionNode(c) &&
  324. isServerRequestHandlerTransactionNode(c.parent) &&
  325. getPageloadTransactionChildCount(c.parent) === 1
  326. ) {
  327. // // The swap can occur at a later point when new transactions are fetched,
  328. // // which means we need to invalidate the tree and re-render the UI.
  329. const parent = c.parent.parent;
  330. TraceTree.Swap({parent: c.parent, child: c, reason: 'pageload server handler'});
  331. TraceTree.invalidate(parent!, true);
  332. }
  333. });
  334. // The sum of all durations of traces that exist under a replay is not always
  335. // equal to the duration of the replay. We need to adjust the traceview bounds
  336. // to ensure that we can see the max of the replay duration and the sum(trace durations). This way, we
  337. // can ensure that the replay timestamp indicators are always visible in the traceview along with all spans from the traces.
  338. if (options.replay) {
  339. const replayStart = options.replay.started_at.getTime();
  340. const replayEnd = options.replay.finished_at.getTime();
  341. traceSpaceBounds[0] = Math.min(traceSpaceBounds[0], replayStart);
  342. traceSpaceBounds[1] = Math.max(traceSpaceBounds[1], replayEnd);
  343. }
  344. for (const indicator of tree.indicators) {
  345. // If any indicator starts after the trace ends, set end to the indicator start
  346. if (indicator.start > traceSpaceBounds[1]) {
  347. traceSpaceBounds[1] = indicator.start;
  348. }
  349. // If an indicator starts before the trace start, set start to the indicator start
  350. if (indicator.start < traceSpaceBounds[0]) {
  351. traceSpaceBounds[0] = indicator.start;
  352. }
  353. }
  354. // Space needs a start and end, if we don't have one we can't construct a timeline.
  355. if (!Number.isFinite(traceSpaceBounds[0])) {
  356. traceSpaceBounds[0] = 0;
  357. }
  358. if (!Number.isFinite(traceSpaceBounds[1])) {
  359. traceSpaceBounds[1] = 0;
  360. }
  361. const space = [traceSpaceBounds[0], traceSpaceBounds[1] - traceSpaceBounds[0]];
  362. tree.root.space = [space[0], space[1]];
  363. traceNode.space = [space[0], space[1]];
  364. tree.indicators.sort((a, b) => a.start - b.start);
  365. return tree;
  366. }
  367. static FromSpans(
  368. node: TraceTreeNode<TraceTree.NodeValue>,
  369. spans: TraceTree.Span[],
  370. event: EventTransaction | null
  371. ): [TraceTreeNode<TraceTree.NodeValue>, [number, number]] {
  372. // collect transactions
  373. const transactions = TraceTree.FindAll(node, n =>
  374. isTransactionNode(n)
  375. ) as TraceTreeNode<TraceTree.Transaction>[];
  376. // Create span nodes
  377. const spanNodes: TraceTreeNode<TraceTree.Span>[] = [];
  378. const spanIdToNode = new Map<string, TraceTreeNode<TraceTree.NodeValue>>();
  379. // Transactions have a span_id that needs to be used as the edge to child child span
  380. if (node.value && 'span_id' in node.value) {
  381. spanIdToNode.set(node.value.span_id, node);
  382. }
  383. for (const span of spans) {
  384. const spanNode: TraceTreeNode<TraceTree.Span> = new TraceTreeNode(null, span, {
  385. event_id: node.metadata.event_id,
  386. project_slug: node.metadata.project_slug,
  387. });
  388. spanNode.event = event;
  389. if (spanIdToNode.has(span.span_id)) {
  390. Sentry.withScope(scope => {
  391. scope.setFingerprint(['trace-span-id-hash-collision']);
  392. scope.captureMessage('Span ID hash collision detected');
  393. });
  394. }
  395. spanIdToNode.set(span.span_id, spanNode);
  396. spanNodes.push(spanNode);
  397. }
  398. // Clear children of root node as we are recreating the sub tree
  399. node.children = [];
  400. // Construct the span tree
  401. for (const span of spanNodes) {
  402. // If the span has no parent span id, nest it under the root
  403. const parent = span.value.parent_span_id
  404. ? spanIdToNode.get(span.value.parent_span_id) ?? node
  405. : node;
  406. span.parent = parent;
  407. parent.children.push(span);
  408. }
  409. // Reparent transactions under children spans
  410. for (const transaction of transactions) {
  411. const parent = spanIdToNode.get(transaction.value.parent_span_id!);
  412. // If the parent span does not exist in the span tree, the transaction will remain under the current node
  413. if (!parent) {
  414. if (transaction.parent?.children.indexOf(transaction) === -1) {
  415. transaction.parent.children.push(transaction);
  416. }
  417. continue;
  418. }
  419. if (transaction === node) {
  420. Sentry.withScope(scope => {
  421. scope.setFingerprint(['trace-tree-span-parent-cycle']);
  422. scope.captureMessage(
  423. 'Span is a parent of its own transaction, this should not be possible'
  424. );
  425. });
  426. continue;
  427. }
  428. parent.children.push(transaction);
  429. transaction.parent = parent;
  430. }
  431. const subTreeSpaceBounds: [number, number] = [node.space[0], node.space[1]];
  432. TraceTree.ForEachChild(node, c => {
  433. c.invalidate();
  434. // When reparenting transactions under spans, the children are not guaranteed to be in order
  435. // so we need to sort them chronologically after the reparenting is complete
  436. // Track the min and max space of the sub tree as spans have ms precision
  437. subTreeSpaceBounds[0] = Math.min(subTreeSpaceBounds[0], c.space[0]);
  438. subTreeSpaceBounds[1] = Math.max(subTreeSpaceBounds[1], c.space[1]);
  439. if (isSpanNode(c)) {
  440. for (const performanceIssue of getRelatedPerformanceIssuesFromTransaction(
  441. c.value,
  442. node
  443. )) {
  444. c.performance_issues.add(performanceIssue);
  445. }
  446. for (const error of getRelatedSpanErrorsFromTransaction(c.value, node)) {
  447. c.errors.add(error);
  448. }
  449. if (isBrowserRequestSpan(c.value)) {
  450. const serverRequestHandler = c.parent?.children.find(n =>
  451. isServerRequestHandlerTransactionNode(n)
  452. );
  453. if (serverRequestHandler) {
  454. serverRequestHandler.parent!.children =
  455. serverRequestHandler.parent!.children.filter(
  456. n => n !== serverRequestHandler
  457. );
  458. c.children.push(serverRequestHandler);
  459. serverRequestHandler.parent = c;
  460. }
  461. }
  462. }
  463. c.children.sort(traceChronologicalSort);
  464. });
  465. if (!Number.isFinite(subTreeSpaceBounds[0])) {
  466. subTreeSpaceBounds[0] = 0;
  467. }
  468. if (!Number.isFinite(subTreeSpaceBounds[1])) {
  469. subTreeSpaceBounds[1] = 0;
  470. }
  471. return [node, subTreeSpaceBounds];
  472. }
  473. appendTree(tree: TraceTree) {
  474. const baseTraceNode = this.root.children[0];
  475. const additionalTraceNode = tree.root.children[0];
  476. if (!baseTraceNode || !additionalTraceNode) {
  477. throw new Error('No trace node found in tree');
  478. }
  479. for (const child of additionalTraceNode.children) {
  480. child.parent = baseTraceNode;
  481. baseTraceNode.children.push(child);
  482. }
  483. for (const error of additionalTraceNode.errors) {
  484. baseTraceNode.errors.add(error);
  485. }
  486. for (const performanceIssue of additionalTraceNode.performance_issues) {
  487. baseTraceNode.performance_issues.add(performanceIssue);
  488. }
  489. for (const profile of additionalTraceNode.profiles) {
  490. baseTraceNode.profiles.push(profile);
  491. }
  492. for (const [node, vitals] of tree.vitals) {
  493. this.vitals.set(node, vitals);
  494. }
  495. for (const [node, _] of tree.vitals) {
  496. if (
  497. baseTraceNode.space?.[0] &&
  498. node.value &&
  499. 'start_timestamp' in node.value &&
  500. 'measurements' in node.value
  501. ) {
  502. tree.indicators = tree.indicators.concat(
  503. collectTraceMeasurements(
  504. node,
  505. baseTraceNode.space[0],
  506. node.value.measurements,
  507. this.vitals,
  508. this.vital_types
  509. )
  510. );
  511. }
  512. }
  513. // We need to invalidate the data in the last node of the tree
  514. // so that the connectors are updated and pointing to the sibling nodes
  515. const last = this.root.children[this.root.children.length - 1];
  516. TraceTree.invalidate(last, true);
  517. const previousEnd = this.root.space[0] + this.root.space[1];
  518. const newEnd = tree.root.space[0] + tree.root.space[1];
  519. this.root.space[0] = Math.min(tree.root.space[0], this.root.space[0]);
  520. this.root.space[1] = Math.max(
  521. previousEnd - this.root.space[0],
  522. newEnd - this.root.space[0]
  523. );
  524. for (const child of tree.root.children) {
  525. this.list = this.list.concat(TraceTree.VisibleChildren(child));
  526. }
  527. }
  528. /**
  529. * Invalidate the visual data used to render the tree, forcing it
  530. * to be recalculated on the next render. This is useful when for example
  531. * the tree is expanded or collapsed, or when the tree is mutated and
  532. * the visual data is no longer valid as the indentation changes
  533. */
  534. static invalidate(node: TraceTreeNode<TraceTree.NodeValue>, recurse: boolean) {
  535. node.invalidate();
  536. if (recurse) {
  537. const queue = [...node.children];
  538. if (isParentAutogroupedNode(node)) {
  539. queue.push(node.head);
  540. }
  541. while (queue.length > 0) {
  542. const next = queue.pop()!;
  543. next.invalidate();
  544. if (isParentAutogroupedNode(next)) {
  545. queue.push(next.head);
  546. }
  547. for (let i = 0; i < next.children.length; i++) {
  548. queue.push(next.children[i]);
  549. }
  550. }
  551. }
  552. }
  553. static DetectMissingInstrumentation(root: TraceTreeNode<TraceTree.NodeValue>): number {
  554. let previous: TraceTreeNode<TraceTree.NodeValue> | null = null;
  555. let missingInstrumentationCount = 0;
  556. TraceTree.ForEachChild(root, child => {
  557. if (
  558. previous &&
  559. child &&
  560. isSpanNode(previous) &&
  561. isSpanNode(child) &&
  562. shouldAddMissingInstrumentationSpan(child.event?.sdk?.name ?? '') &&
  563. shouldAddMissingInstrumentationSpan(previous.event?.sdk?.name ?? '') &&
  564. child.space[0] - previous.space[0] - previous.space[1] >=
  565. TraceTree.MISSING_INSTRUMENTATION_THRESHOLD_MS
  566. ) {
  567. const node = new MissingInstrumentationNode(
  568. child.parent!,
  569. {
  570. type: 'missing_instrumentation',
  571. start_timestamp: previous.value.timestamp,
  572. timestamp: child.value.start_timestamp,
  573. },
  574. {
  575. event_id: undefined,
  576. project_slug: undefined,
  577. },
  578. previous,
  579. child
  580. );
  581. missingInstrumentationCount++;
  582. if (child.parent === previous) {
  583. // The tree is dfs iterated, so it can only ever be the first child
  584. previous.children.splice(0, 0, node);
  585. node.parent = previous;
  586. } else {
  587. const childIndex = child.parent?.children.indexOf(child) ?? -1;
  588. if (childIndex === -1) {
  589. Sentry.captureException('Detecting missing instrumentation failed');
  590. return;
  591. }
  592. child.parent?.children.splice(childIndex, 0, node);
  593. }
  594. previous = node;
  595. return;
  596. }
  597. previous = child;
  598. });
  599. return missingInstrumentationCount;
  600. }
  601. // We can just filter out the missing instrumentation
  602. // nodes as they never have any children that require remapping
  603. static RemoveMissingInstrumentationNodes(
  604. root: TraceTreeNode<TraceTree.NodeValue>
  605. ): number {
  606. let removeCount = 0;
  607. TraceTree.Filter(root, node => {
  608. if (isMissingInstrumentationNode(node)) {
  609. removeCount++;
  610. return false;
  611. }
  612. return true;
  613. });
  614. return removeCount;
  615. }
  616. static AutogroupDirectChildrenSpanNodes(
  617. root: TraceTreeNode<TraceTree.NodeValue>
  618. ): number {
  619. const queue = [root];
  620. let autogroupCount = 0;
  621. while (queue.length > 0) {
  622. const node = queue.pop()!;
  623. if (!isSpanNode(node) || node.children.length > 1) {
  624. for (const child of node.children) {
  625. queue.push(child);
  626. }
  627. continue;
  628. }
  629. const head = node;
  630. let tail = node;
  631. let groupMatchCount = 0;
  632. let errors: TraceErrorType[] = [];
  633. let performance_issues: TraceTree.TracePerformanceIssue[] = [];
  634. let start = head.space[0];
  635. let end = head.space[0] + head.space[1];
  636. while (
  637. tail &&
  638. tail.children.length === 1 &&
  639. isSpanNode(tail.children[0]) &&
  640. tail.children[0].value.op === head.value.op
  641. ) {
  642. start = Math.min(start, tail.space[0]);
  643. end = Math.max(end, tail.space[0] + tail.space[1]);
  644. errors = errors.concat(Array.from(tail.errors));
  645. performance_issues = performance_issues.concat(
  646. Array.from(tail.performance_issues)
  647. );
  648. groupMatchCount++;
  649. tail = tail.children[0];
  650. }
  651. if (groupMatchCount < 1) {
  652. for (const child of head.children) {
  653. queue.push(child);
  654. }
  655. continue;
  656. }
  657. const autoGroupedNode = new ParentAutogroupNode(
  658. node.parent,
  659. {
  660. ...head.value,
  661. autogrouped_by: {
  662. op: head.value && 'op' in head.value ? head.value.op ?? '' : '',
  663. },
  664. },
  665. {
  666. event_id: undefined,
  667. project_slug: undefined,
  668. },
  669. head,
  670. tail
  671. );
  672. autogroupCount++;
  673. if (!node.parent) {
  674. throw new Error('Parent node is missing, this should be unreachable code');
  675. }
  676. const children = isParentAutogroupedNode(node.parent)
  677. ? node.parent.tail.children
  678. : node.parent.children;
  679. const index = children.indexOf(node);
  680. if (index === -1) {
  681. throw new Error('Node is not a child of its parent');
  682. }
  683. children[index] = autoGroupedNode;
  684. autoGroupedNode.head.parent = autoGroupedNode;
  685. autoGroupedNode.groupCount = groupMatchCount + 1;
  686. // Checking the tail node for errors as it is not included in the grouping
  687. // while loop, but is hidden when the autogrouped node is collapsed
  688. errors = errors.concat(Array.from(tail.errors));
  689. performance_issues = performance_issues.concat(Array.from(tail.performance_issues));
  690. start = Math.min(start, tail.space[0]);
  691. end = Math.max(end, tail.space[0] + tail.space[1]);
  692. autoGroupedNode.space = [start, end - start];
  693. autoGroupedNode.errors = new Set(errors);
  694. autoGroupedNode.performance_issues = new Set(performance_issues);
  695. for (const c of tail.children) {
  696. c.parent = autoGroupedNode;
  697. queue.push(c);
  698. }
  699. }
  700. return autogroupCount;
  701. }
  702. static RemoveDirectChildrenAutogroupNodes(
  703. root: TraceTreeNode<TraceTree.NodeValue>
  704. ): number {
  705. let removeCount = 0;
  706. TraceTree.ForEachChild(root, node => {
  707. if (isParentAutogroupedNode(node)) {
  708. const index = node.parent?.children.indexOf(node) ?? -1;
  709. if (!node.parent || index === -1) {
  710. Sentry.captureException('Removing direct children autogroup nodes failed');
  711. return;
  712. }
  713. removeCount++;
  714. node.parent.children[index] = node.head;
  715. // Head of parent now points to the parent of autogrouped node
  716. node.head.parent = node.parent;
  717. // All children now point to the tail of the autogrouped node
  718. for (const child of node.tail.children) {
  719. child.parent = node.tail;
  720. }
  721. }
  722. });
  723. return removeCount;
  724. }
  725. static AutogroupSiblingSpanNodes(root: TraceTreeNode<TraceTree.NodeValue>): number {
  726. const queue = [root];
  727. let autogroupCount = 0;
  728. while (queue.length > 0) {
  729. const node = queue.pop()!;
  730. if (isParentAutogroupedNode(node)) {
  731. queue.push(node.head);
  732. } else {
  733. for (const child of node.children) {
  734. queue.push(child);
  735. }
  736. }
  737. if (isAutogroupedNode(node) || isMissingInstrumentationNode(node)) {
  738. continue;
  739. }
  740. if (node.children.length < 5) {
  741. continue;
  742. }
  743. let index = 0;
  744. let matchCount = 0;
  745. while (index < node.children.length) {
  746. // Skip until we find a span candidate
  747. if (!isSpanNode(node.children[index])) {
  748. index++;
  749. matchCount = 0;
  750. continue;
  751. }
  752. const current = node.children[index] as TraceTreeNode<TraceTree.Span>;
  753. const next = node.children[index + 1] as TraceTreeNode<TraceTree.Span>;
  754. if (
  755. next &&
  756. isSpanNode(next) &&
  757. next.children.length === 0 &&
  758. current.children.length === 0 &&
  759. next.value.op === current.value.op &&
  760. next.value.description === current.value.description
  761. ) {
  762. matchCount++;
  763. // If the next node is the last node in the list, we keep iterating
  764. if (index + 1 < node.children.length) {
  765. index++;
  766. continue;
  767. }
  768. }
  769. if (matchCount >= 4) {
  770. const autoGroupedNode = new SiblingAutogroupNode(
  771. node,
  772. {
  773. ...current.value,
  774. autogrouped_by: {
  775. op: current.value.op ?? '',
  776. description: current.value.description ?? '',
  777. },
  778. },
  779. {
  780. event_id: undefined,
  781. project_slug: undefined,
  782. }
  783. );
  784. autogroupCount++;
  785. autoGroupedNode.groupCount = matchCount + 1;
  786. const start = index - matchCount;
  787. let start_timestamp = Number.POSITIVE_INFINITY;
  788. let timestamp = Number.NEGATIVE_INFINITY;
  789. for (let j = start; j < start + matchCount + 1; j++) {
  790. const child = node.children[j];
  791. start_timestamp = Math.min(start_timestamp, node.children[j].space[0]);
  792. timestamp = Math.max(
  793. timestamp,
  794. node.children[j].space[0] + node.children[j].space[1]
  795. );
  796. if (node.children[j].hasErrors) {
  797. for (const error of child.errors) {
  798. autoGroupedNode.errors.add(error);
  799. }
  800. for (const performanceIssue of child.performance_issues) {
  801. autoGroupedNode.performance_issues.add(performanceIssue);
  802. }
  803. }
  804. autoGroupedNode.children.push(node.children[j]);
  805. node.children[j].parent = autoGroupedNode;
  806. }
  807. autoGroupedNode.space = [start_timestamp, timestamp - start_timestamp];
  808. node.children.splice(start, matchCount + 1, autoGroupedNode);
  809. index = start + 1;
  810. matchCount = 0;
  811. } else {
  812. index++;
  813. matchCount = 0;
  814. }
  815. }
  816. }
  817. return autogroupCount;
  818. }
  819. static RemoveSiblingAutogroupNodes(root: TraceTreeNode<TraceTree.NodeValue>): number {
  820. let removeCount = 0;
  821. TraceTree.ForEachChild(root, node => {
  822. if (isSiblingAutogroupedNode(node)) {
  823. removeCount++;
  824. const index = node.parent?.children.indexOf(node) ?? -1;
  825. if (!node.parent || index === -1) {
  826. Sentry.captureException('Removing sibling autogroup nodes failed');
  827. return;
  828. }
  829. node.parent.children.splice(index, 1, ...node.children);
  830. for (const child of node.children) {
  831. child.parent = node.parent;
  832. }
  833. }
  834. });
  835. return removeCount;
  836. }
  837. static DirectVisibleChildren(
  838. node: TraceTreeNode<TraceTree.NodeValue>
  839. ): TraceTreeNode<TraceTree.NodeValue>[] {
  840. if (isParentAutogroupedNode(node)) {
  841. if (node.expanded) {
  842. return [node.head];
  843. }
  844. return node.tail.children;
  845. }
  846. return node.children;
  847. }
  848. static VisibleChildren(
  849. root: TraceTreeNode<TraceTree.NodeValue>
  850. ): TraceTreeNode<TraceTree.NodeValue>[] {
  851. const queue: TraceTreeNode<TraceTree.NodeValue>[] = [];
  852. const visibleChildren: TraceTreeNode<TraceTree.NodeValue>[] = [];
  853. if (root.expanded || isParentAutogroupedNode(root)) {
  854. const children = TraceTree.DirectVisibleChildren(root);
  855. for (let i = children.length - 1; i >= 0; i--) {
  856. queue.push(children[i]);
  857. }
  858. }
  859. while (queue.length > 0) {
  860. const node = queue.pop()!;
  861. visibleChildren.push(node);
  862. // iterate in reverseto ensure nodes are processed in order
  863. if (node.expanded || isParentAutogroupedNode(node)) {
  864. const children = TraceTree.DirectVisibleChildren(node);
  865. for (let i = children.length - 1; i >= 0; i--) {
  866. queue.push(children[i]);
  867. }
  868. }
  869. }
  870. return visibleChildren;
  871. }
  872. static PathToNode(node: TraceTreeNode<TraceTree.NodeValue>): TraceTree.NodePath[] {
  873. // If the node is a transaction node, then it will not require any
  874. // fetching and we can link to it directly
  875. if (isTransactionNode(node)) {
  876. return [nodeToId(node)];
  877. }
  878. // Otherwise, we need to traverse up the tree until we find a transaction node.
  879. const nodes: TraceTreeNode<TraceTree.NodeValue>[] = [node];
  880. let current: TraceTreeNode<TraceTree.NodeValue> | null = node.parent;
  881. while (current && !isTransactionNode(current)) {
  882. current = current.parent;
  883. }
  884. if (current && isTransactionNode(current)) {
  885. nodes.push(current);
  886. }
  887. return nodes.map(nodeToId);
  888. }
  889. static ForEachChild(
  890. root: TraceTreeNode<TraceTree.NodeValue>,
  891. cb: (node: TraceTreeNode<TraceTree.NodeValue>) => void
  892. ): void {
  893. const queue: TraceTreeNode<TraceTree.NodeValue>[] = [];
  894. if (isParentAutogroupedNode(root)) {
  895. queue.push(root.head);
  896. } else {
  897. for (let i = root.children.length - 1; i >= 0; i--) {
  898. queue.push(root.children[i]);
  899. }
  900. }
  901. while (queue.length > 0) {
  902. const next = queue.pop()!;
  903. cb(next);
  904. // Parent autogroup nodes have a head and tail pointer instead of children
  905. if (isParentAutogroupedNode(next)) {
  906. queue.push(next.head);
  907. } else {
  908. for (let i = next.children.length - 1; i >= 0; i--) {
  909. queue.push(next.children[i]);
  910. }
  911. }
  912. }
  913. }
  914. // Removes node and all its children from the tree
  915. static Filter(
  916. node: TraceTreeNode<TraceTree.NodeValue>,
  917. predicate: (node: TraceTreeNode) => boolean
  918. ): TraceTreeNode<TraceTree.NodeValue> {
  919. const queue = [node];
  920. while (queue.length) {
  921. const next = queue.pop()!;
  922. next.children = next.children.filter(c => {
  923. if (predicate(c)) {
  924. queue.push(c);
  925. return true;
  926. }
  927. return false;
  928. });
  929. }
  930. return node;
  931. }
  932. static Find(
  933. root: TraceTreeNode<TraceTree.NodeValue>,
  934. predicate: (node: TraceTreeNode<TraceTree.NodeValue>) => boolean
  935. ): TraceTreeNode<TraceTree.NodeValue> | null {
  936. const queue = [root];
  937. while (queue.length > 0) {
  938. const next = queue.pop()!;
  939. if (predicate(next)) {
  940. return next;
  941. }
  942. if (isParentAutogroupedNode(next)) {
  943. queue.push(next.head);
  944. } else {
  945. for (const child of next.children) {
  946. queue.push(child);
  947. }
  948. }
  949. }
  950. return null;
  951. }
  952. static FindAll(
  953. root: TraceTreeNode<TraceTree.NodeValue>,
  954. predicate: (node: TraceTreeNode<TraceTree.NodeValue>) => boolean
  955. ): TraceTreeNode<TraceTree.NodeValue>[] {
  956. const queue = [root];
  957. const results: TraceTreeNode<TraceTree.NodeValue>[] = [];
  958. while (queue.length > 0) {
  959. const next = queue.pop()!;
  960. if (predicate(next)) {
  961. results.push(next);
  962. }
  963. if (isParentAutogroupedNode(next)) {
  964. queue.push(next.head);
  965. } else {
  966. for (const child of next.children) {
  967. queue.push(child);
  968. }
  969. }
  970. }
  971. return results;
  972. }
  973. static FindByPath(
  974. tree: TraceTree,
  975. path: TraceTree.NodePath
  976. ): TraceTreeNode<TraceTree.NodeValue> | null {
  977. const [type, id, rest] = path.split('-');
  978. if (!type || !id || rest) {
  979. Sentry.withScope(scope => {
  980. scope.setFingerprint(['trace-view-path-error']);
  981. scope.captureMessage('Invalid path to trace tree node ');
  982. });
  983. return null;
  984. }
  985. if (type === 'trace' && id === 'root') {
  986. return tree.root.children[0];
  987. }
  988. return TraceTree.Find(tree.root, node => {
  989. if (type === 'txn' && isTransactionNode(node)) {
  990. // A transaction itself is a span and we are starting to treat it as such.
  991. // Hence we check for both event_id and span_id.
  992. return node.value.event_id === id || node.value.span_id === id;
  993. }
  994. if (type === 'span' && isSpanNode(node)) {
  995. return node.value.span_id === id;
  996. }
  997. if (type === 'ag' && isAutogroupedNode(node)) {
  998. if (isParentAutogroupedNode(node)) {
  999. return (
  1000. node.value.span_id === id ||
  1001. node.head.value.span_id === id ||
  1002. node.tail.value.span_id === id
  1003. );
  1004. }
  1005. if (isSiblingAutogroupedNode(node)) {
  1006. const child = node.children[0];
  1007. if (isSpanNode(child)) {
  1008. return child.value.span_id === id;
  1009. }
  1010. }
  1011. }
  1012. if (type === 'ms' && isMissingInstrumentationNode(node)) {
  1013. return node.previous.value.span_id === id || node.next.value.span_id === id;
  1014. }
  1015. if (type === 'error' && isTraceErrorNode(node)) {
  1016. return node.value.event_id === id;
  1017. }
  1018. return false;
  1019. });
  1020. }
  1021. static FindByID(
  1022. root: TraceTreeNode<TraceTree.NodeValue>,
  1023. eventId: string
  1024. ): TraceTreeNode<TraceTree.NodeValue> | null {
  1025. return TraceTree.Find(root, n => {
  1026. if (isTransactionNode(n)) {
  1027. // A transaction itself is a span and we are starting to treat it as such.
  1028. // Hence we check for both event_id and span_id.
  1029. return n.value.event_id === eventId || n.value.span_id === eventId;
  1030. }
  1031. if (isSpanNode(n)) {
  1032. return n.value.span_id === eventId;
  1033. }
  1034. if (isTraceErrorNode(n)) {
  1035. return n.value.event_id === eventId;
  1036. }
  1037. if (isTraceNode(n)) {
  1038. return false;
  1039. }
  1040. if (isMissingInstrumentationNode(n)) {
  1041. return n.previous.value.span_id === eventId || n.next.value.span_id === eventId;
  1042. }
  1043. if (isParentAutogroupedNode(n)) {
  1044. return (
  1045. n.value.span_id === eventId ||
  1046. n.head.value.span_id === eventId ||
  1047. n.tail.value.span_id === eventId
  1048. );
  1049. }
  1050. if (isSiblingAutogroupedNode(n)) {
  1051. const child = n.children[0];
  1052. if (isSpanNode(child)) {
  1053. return child.value.span_id === eventId;
  1054. }
  1055. }
  1056. if (eventId === 'root' && isTraceNode(n)) {
  1057. return true;
  1058. }
  1059. // If we dont have an exact match, then look for an event_id in the errors or performance issues
  1060. for (const e of n.errors) {
  1061. if (e.event_id === eventId) {
  1062. return true;
  1063. }
  1064. }
  1065. for (const p of n.performance_issues) {
  1066. if (p.event_id === eventId) {
  1067. return true;
  1068. }
  1069. }
  1070. return false;
  1071. });
  1072. }
  1073. static ParentTransaction(
  1074. node: TraceTreeNode<TraceTree.NodeValue>
  1075. ): TraceTreeNode<TraceTree.Transaction> | null {
  1076. let next: TraceTreeNode<TraceTree.NodeValue> | null = node.parent;
  1077. while (next) {
  1078. if (isTransactionNode(next)) {
  1079. return next;
  1080. }
  1081. next = next.parent;
  1082. }
  1083. return null;
  1084. }
  1085. expand(node: TraceTreeNode<TraceTree.NodeValue>, expanded: boolean): boolean {
  1086. // Trace root nodes are not expandable or collapsable
  1087. if (isTraceNode(node)) {
  1088. return false;
  1089. }
  1090. // Expanding is not allowed for zoomed in nodes
  1091. if (expanded === node.expanded || node.zoomedIn) {
  1092. return false;
  1093. }
  1094. if (isParentAutogroupedNode(node)) {
  1095. if (!expanded) {
  1096. const index = this.list.indexOf(node);
  1097. this.list.splice(index + 1, TraceTree.VisibleChildren(node).length);
  1098. // When we collapse the autogroup, we need to point the tail children
  1099. // back to the tail autogroup node.
  1100. for (const c of node.tail.children) {
  1101. c.parent = node;
  1102. }
  1103. this.list.splice(index + 1, 0, ...TraceTree.VisibleChildren(node.tail));
  1104. } else {
  1105. const index = this.list.indexOf(node);
  1106. this.list.splice(index + 1, TraceTree.VisibleChildren(node).length);
  1107. // When the node is collapsed, children point to the autogrouped node.
  1108. // We need to point them back to the tail node which is now visible
  1109. for (const c of node.tail.children) {
  1110. c.parent = node.tail;
  1111. }
  1112. this.list.splice(
  1113. index + 1,
  1114. 0,
  1115. node.head,
  1116. ...TraceTree.VisibleChildren(node.head)
  1117. );
  1118. }
  1119. TraceTree.invalidate(node, true);
  1120. node.expanded = expanded;
  1121. return true;
  1122. }
  1123. if (!expanded) {
  1124. const index = this.list.indexOf(node);
  1125. this.list.splice(index + 1, TraceTree.VisibleChildren(node).length);
  1126. node.expanded = expanded;
  1127. // When transaction nodes are collapsed, they still render child transactions
  1128. if (isTransactionNode(node)) {
  1129. this.list.splice(index + 1, 0, ...TraceTree.VisibleChildren(node));
  1130. }
  1131. } else {
  1132. node.expanded = expanded;
  1133. // Flip expanded so that we can collect visible children
  1134. const index = this.list.indexOf(node);
  1135. this.list.splice(index + 1, 0, ...TraceTree.VisibleChildren(node));
  1136. }
  1137. TraceTree.invalidate(node, true);
  1138. return true;
  1139. }
  1140. zoom(
  1141. node: TraceTreeNode<TraceTree.NodeValue>,
  1142. zoomedIn: boolean,
  1143. options: {
  1144. api: Client;
  1145. organization: Organization;
  1146. preferences: Pick<TracePreferencesState, 'autogroup' | 'missing_instrumentation'>;
  1147. }
  1148. ): Promise<Event | null> {
  1149. if (isTraceNode(node)) {
  1150. return Promise.resolve(null);
  1151. }
  1152. if (zoomedIn === node.zoomedIn || !node.canFetch) {
  1153. return Promise.resolve(null);
  1154. }
  1155. if (!zoomedIn) {
  1156. const index = this.list.indexOf(node);
  1157. // Remove currently visible children
  1158. this.list.splice(index + 1, TraceTree.VisibleChildren(node).length);
  1159. // Flip visibility
  1160. node.zoomedIn = zoomedIn;
  1161. // When transactions are zoomed out, they still render child transactions
  1162. if (isTransactionNode(node)) {
  1163. // Find all transactions that are children of the current transaction
  1164. // remove all non transaction events from current node and its children
  1165. // point transactions back to their parents
  1166. const transactions = TraceTree.FindAll(
  1167. node,
  1168. c => isTransactionNode(c) && c !== node
  1169. );
  1170. for (const t of transactions) {
  1171. // point transactions back to their parents
  1172. const parent = TraceTree.ParentTransaction(t);
  1173. // If they already have the correct parent, then we can skip this
  1174. if (t.parent === parent) {
  1175. continue;
  1176. }
  1177. if (!parent) {
  1178. Sentry.withScope(scope => {
  1179. scope.setFingerprint(['trace-view-transaction-parent']);
  1180. scope.captureMessage('Failed to find parent transaction when zooming out');
  1181. });
  1182. continue;
  1183. }
  1184. t.parent = parent;
  1185. parent.children.push(t);
  1186. }
  1187. node.children = node.children.filter(c => isTransactionNode(c));
  1188. node.children.sort(traceChronologicalSort);
  1189. this.list.splice(index + 1, 0, ...TraceTree.VisibleChildren(node));
  1190. }
  1191. TraceTree.invalidate(node, true);
  1192. return Promise.resolve(null);
  1193. }
  1194. const key =
  1195. options.organization.slug +
  1196. ':' +
  1197. node.metadata.project_slug! +
  1198. ':' +
  1199. node.metadata.event_id!;
  1200. const promise =
  1201. this._spanPromises.get(key) ??
  1202. fetchTransactionSpans(
  1203. options.api,
  1204. options.organization,
  1205. node.metadata.project_slug!,
  1206. node.metadata.event_id!
  1207. );
  1208. node.fetchStatus = 'loading';
  1209. promise
  1210. .then((data: EventTransaction) => {
  1211. // The user may have collapsed the node before the promise resolved. When that
  1212. // happens, dont update the tree with the resolved data. Alternatively, we could implement
  1213. // a cancellable promise and avoid this cumbersome heuristic.
  1214. // Remove existing entries from the list
  1215. const index = this.list.indexOf(node);
  1216. node.fetchStatus = 'resolved';
  1217. if (node.expanded && index !== -1) {
  1218. const childrenCount = TraceTree.VisibleChildren(node).length;
  1219. if (childrenCount > 0) {
  1220. this.list.splice(index + 1, childrenCount);
  1221. }
  1222. }
  1223. // API response is not sorted
  1224. const spans = data.entries.find(s => s.type === 'spans') ?? {data: []};
  1225. spans.data.sort((a, b) => a.start_timestamp - b.start_timestamp);
  1226. const [root, spanTreeSpaceBounds] = TraceTree.FromSpans(node, spans.data, data);
  1227. root.zoomedIn = true;
  1228. // Spans contain millisecond precision, which means that it is possible for the
  1229. // children spans of a transaction to extend beyond the start and end of the transaction
  1230. // through ns precision. To account for this, we need to adjust the space of the transaction node and the space
  1231. // of our trace so that all of the span children are visible and can be rendered inside the view
  1232. const previousStart = this.root.space[0];
  1233. const previousDuration = this.root.space[1];
  1234. const newStart = spanTreeSpaceBounds[0];
  1235. const newEnd = spanTreeSpaceBounds[0] + spanTreeSpaceBounds[1];
  1236. // Extend the start of the trace to include the new min start
  1237. if (newStart <= this.root.space[0]) {
  1238. this.root.space[0] = newStart;
  1239. }
  1240. // Extend the end of the trace to include the new max end
  1241. if (newEnd > this.root.space[0] + this.root.space[1]) {
  1242. this.root.space[1] = newEnd - this.root.space[0];
  1243. }
  1244. if (
  1245. previousStart !== this.root.space[0] ||
  1246. previousDuration !== this.root.space[1]
  1247. ) {
  1248. this.dispatch('trace timeline change', this.root.space);
  1249. }
  1250. if (options.preferences.missing_instrumentation) {
  1251. TraceTree.DetectMissingInstrumentation(root);
  1252. }
  1253. if (options.preferences.autogroup.sibling) {
  1254. TraceTree.AutogroupSiblingSpanNodes(root);
  1255. }
  1256. if (options.preferences.autogroup.parent) {
  1257. TraceTree.AutogroupDirectChildrenSpanNodes(root);
  1258. }
  1259. if (index !== -1) {
  1260. this.list.splice(index + 1, 0, ...TraceTree.VisibleChildren(node));
  1261. }
  1262. return data;
  1263. })
  1264. .catch(_e => {
  1265. node.fetchStatus = 'error';
  1266. });
  1267. this._spanPromises.set(key, promise);
  1268. return promise;
  1269. }
  1270. static EnforceVisibility(
  1271. tree: TraceTree,
  1272. node: TraceTreeNode<TraceTree.NodeValue>
  1273. ): number {
  1274. let index = tree.list.indexOf(node);
  1275. if (node && index === -1) {
  1276. let parent_node = node.parent;
  1277. while (parent_node) {
  1278. // Transactions break autogrouping chains, so we can stop here
  1279. tree.expand(parent_node, true);
  1280. // This is very wasteful as it performs O(n^2) search each time we expand a node...
  1281. // In most cases though, we should be operating on a tree with sub 10k elements and hopefully
  1282. // a low autogrouped node count.
  1283. index = node ? tree.list.findIndex(n => n === node) : -1;
  1284. if (index !== -1) {
  1285. break;
  1286. }
  1287. parent_node = parent_node.parent;
  1288. }
  1289. }
  1290. return index;
  1291. }
  1292. static ExpandToEventID(
  1293. tree: TraceTree,
  1294. eventId: string,
  1295. options: {
  1296. api: Client;
  1297. organization: Organization;
  1298. preferences: Pick<TracePreferencesState, 'autogroup' | 'missing_instrumentation'>;
  1299. }
  1300. ): Promise<void> {
  1301. const node = TraceTree.FindByID(tree.root, eventId);
  1302. if (!node) {
  1303. return Promise.resolve();
  1304. }
  1305. return TraceTree.ExpandToPath(tree, TraceTree.PathToNode(node), options);
  1306. }
  1307. static ExpandToPath(
  1308. tree: TraceTree,
  1309. scrollQueue: TraceTree.NodePath[],
  1310. options: {
  1311. api: Client;
  1312. organization: Organization;
  1313. preferences: Pick<TracePreferencesState, 'autogroup' | 'missing_instrumentation'>;
  1314. }
  1315. ): Promise<void> {
  1316. const transactionIds = new Set(
  1317. scrollQueue.filter(s => s.startsWith('txn-')).map(s => s.replace('txn-', ''))
  1318. );
  1319. // If we are just linking to a transaction, then we dont need to fetch its spans
  1320. if (transactionIds.size === 1 && scrollQueue.length === 1) {
  1321. return Promise.resolve();
  1322. }
  1323. const transactionNodes = TraceTree.FindAll(
  1324. tree.root,
  1325. node =>
  1326. isTransactionNode(node) &&
  1327. (transactionIds.has(node.value.span_id) ||
  1328. transactionIds.has(node.value.event_id))
  1329. );
  1330. const promises = transactionNodes.map(node => tree.zoom(node, true, options));
  1331. return Promise.all(promises)
  1332. .then(_resp => {
  1333. // Ignore response
  1334. })
  1335. .catch(e => {
  1336. Sentry.withScope(scope => {
  1337. scope.setFingerprint(['trace-view-expand-to-path-error']);
  1338. scope.captureMessage('Failed to expand to path');
  1339. scope.captureException(e);
  1340. });
  1341. });
  1342. }
  1343. // Only supports parent/child swaps (the only ones we need)
  1344. static Swap({
  1345. parent,
  1346. child,
  1347. reason,
  1348. }: {
  1349. child: TraceTreeNode<TraceTree.NodeValue>;
  1350. parent: TraceTreeNode<TraceTree.NodeValue>;
  1351. reason: TraceTreeNode['reparent_reason'];
  1352. }) {
  1353. const commonRoot = parent.parent!;
  1354. const parentIndex = commonRoot.children.indexOf(parent);
  1355. if (!commonRoot || parentIndex === -1) {
  1356. throw new Error('Cannot find common parent');
  1357. }
  1358. TraceTree.Filter(commonRoot, c => c !== child);
  1359. parent.parent = null;
  1360. child.parent = null;
  1361. // Insert child into parent
  1362. commonRoot.children[parentIndex] = child;
  1363. child.children.push(parent);
  1364. child.parent = commonRoot;
  1365. parent.parent = child;
  1366. child.reparent_reason = reason;
  1367. parent.reparent_reason = reason;
  1368. }
  1369. static IsLastChild(n: TraceTreeNode<TraceTree.NodeValue>): boolean {
  1370. if (!n.parent) {
  1371. return false;
  1372. }
  1373. if (isParentAutogroupedNode(n.parent)) {
  1374. if (n.parent.expanded) {
  1375. // The autogrouped
  1376. return true;
  1377. }
  1378. return n.parent.tail.children[n.parent.tail.children.length - 1] === n;
  1379. }
  1380. return n.parent.children[n.parent.children.length - 1] === n;
  1381. }
  1382. static HasVisibleChildren(node: TraceTreeNode<TraceTree.NodeValue>): boolean {
  1383. if (isParentAutogroupedNode(node)) {
  1384. if (node.expanded) {
  1385. return node.head.children.length > 0;
  1386. }
  1387. return node.tail.children.length > 0;
  1388. }
  1389. if (node.expanded) {
  1390. return node.children.length > 0;
  1391. }
  1392. return false;
  1393. }
  1394. /**
  1395. * Return a lazily calculated depth of the node in the tree.
  1396. * Root node has a value of -1 as it is abstract.
  1397. */
  1398. static Depth(node: TraceTreeNode<any>): number {
  1399. if (node.depth !== undefined) {
  1400. return node.depth;
  1401. }
  1402. let depth = -2;
  1403. let start: TraceTreeNode<any> | null = node;
  1404. while (start) {
  1405. depth++;
  1406. start = start.parent;
  1407. }
  1408. node.depth = depth;
  1409. return depth;
  1410. }
  1411. static ConnectorsTo(node: TraceTreeNode<TraceTree.NodeValue>): number[] {
  1412. if (node.connectors !== undefined) {
  1413. return node.connectors;
  1414. }
  1415. const connectors: number[] = [];
  1416. let start: TraceTreeNode<TraceTree.NodeValue> | null = node.parent;
  1417. if (start && isTraceNode(start) && !TraceTree.IsLastChild(node)) {
  1418. node.connectors = [-TraceTree.Depth(node)];
  1419. return node.connectors;
  1420. }
  1421. if (!TraceTree.IsLastChild(node)) {
  1422. connectors.push(TraceTree.Depth(node));
  1423. }
  1424. while (start) {
  1425. if (!start.value || !start.parent) {
  1426. break;
  1427. }
  1428. if (TraceTree.IsLastChild(start)) {
  1429. start = start.parent;
  1430. continue;
  1431. }
  1432. connectors.push(
  1433. isTraceNode(start.parent) ? -TraceTree.Depth(start) : TraceTree.Depth(start)
  1434. );
  1435. start = start.parent;
  1436. }
  1437. node.connectors = connectors;
  1438. return connectors;
  1439. }
  1440. toList(): TraceTreeNode<TraceTree.NodeValue>[] {
  1441. this.list = TraceTree.VisibleChildren(this.root);
  1442. return this.list;
  1443. }
  1444. rebuild() {
  1445. TraceTree.invalidate(this.root, true);
  1446. this.list = this.toList();
  1447. return this;
  1448. }
  1449. build() {
  1450. this.list = this.toList();
  1451. return this;
  1452. }
  1453. get shape(): TraceShape {
  1454. const trace = this.root.children[0];
  1455. if (!trace) {
  1456. return TraceShape.EMPTY_TRACE;
  1457. }
  1458. if (!isTraceNode(trace)) {
  1459. throw new TypeError('Not trace node');
  1460. }
  1461. const traceStats = trace.value.transactions?.reduce<{
  1462. javascriptRootTransactions: TraceTree.Transaction[];
  1463. orphans: number;
  1464. roots: number;
  1465. }>(
  1466. (stats, transaction) => {
  1467. if (isRootTransaction(transaction)) {
  1468. stats.roots++;
  1469. if (isJavascriptSDKTransaction(transaction)) {
  1470. stats.javascriptRootTransactions.push(transaction);
  1471. }
  1472. } else {
  1473. stats.orphans++;
  1474. }
  1475. return stats;
  1476. },
  1477. {roots: 0, orphans: 0, javascriptRootTransactions: []}
  1478. ) ?? {roots: 0, orphans: 0, javascriptRootTransactions: []};
  1479. if (traceStats.roots === 0) {
  1480. if (traceStats.orphans > 0) {
  1481. return TraceShape.NO_ROOT;
  1482. }
  1483. if ((trace.value.orphan_errors?.length ?? 0) > 0) {
  1484. return TraceShape.ONLY_ERRORS;
  1485. }
  1486. return TraceShape.EMPTY_TRACE;
  1487. }
  1488. if (traceStats.roots === 1) {
  1489. if (traceStats.orphans > 0) {
  1490. return TraceShape.BROKEN_SUBTRACES;
  1491. }
  1492. return TraceShape.ONE_ROOT;
  1493. }
  1494. if (traceStats.roots > 1) {
  1495. if (traceStats.javascriptRootTransactions.length > 0) {
  1496. return TraceShape.BROWSER_MULTIPLE_ROOTS;
  1497. }
  1498. return TraceShape.MULTIPLE_ROOTS;
  1499. }
  1500. throw new Error('Unknown trace type');
  1501. }
  1502. fetchAdditionalTraces(options: {
  1503. api: Client;
  1504. filters: any;
  1505. meta: TraceMetaQueryResults | null;
  1506. organization: Organization;
  1507. replayTraces: ReplayTrace[];
  1508. rerender: () => void;
  1509. urlParams: Location['query'];
  1510. }): () => void {
  1511. let cancelled = false;
  1512. const {organization, api, urlParams, filters, rerender, replayTraces} = options;
  1513. const clonedTraceIds = [...replayTraces];
  1514. const root = this.root.children[0];
  1515. root.fetchStatus = 'loading';
  1516. rerender();
  1517. (async () => {
  1518. while (clonedTraceIds.length > 0) {
  1519. const batch = clonedTraceIds.splice(0, 3);
  1520. const results = await Promise.allSettled(
  1521. batch.map(batchTraceData => {
  1522. return fetchTrace(api, {
  1523. orgSlug: organization.slug,
  1524. query: qs.stringify(
  1525. getTraceQueryParams(urlParams, filters.selection, {
  1526. timestamp: batchTraceData.timestamp,
  1527. })
  1528. ),
  1529. traceId: batchTraceData.traceSlug,
  1530. });
  1531. })
  1532. );
  1533. if (cancelled) {
  1534. return;
  1535. }
  1536. const updatedData = results.reduce(
  1537. (acc, result) => {
  1538. // Ignoring the error case for now
  1539. if (result.status === 'fulfilled') {
  1540. const {transactions, orphan_errors} = result.value;
  1541. acc.transactions.push(...transactions);
  1542. acc.orphan_errors.push(...orphan_errors);
  1543. }
  1544. return acc;
  1545. },
  1546. {
  1547. transactions: [],
  1548. orphan_errors: [],
  1549. } as TraceSplitResults<TraceTree.Transaction>
  1550. );
  1551. this.appendTree(
  1552. TraceTree.FromTrace(updatedData, {
  1553. meta: options.meta?.data,
  1554. replay: null,
  1555. })
  1556. );
  1557. rerender();
  1558. }
  1559. root.fetchStatus = 'idle';
  1560. rerender();
  1561. })();
  1562. return () => {
  1563. root.fetchStatus = 'idle';
  1564. cancelled = true;
  1565. };
  1566. }
  1567. /**
  1568. * Prints the tree in a human readable format, useful for debugging and testing
  1569. */
  1570. print() {
  1571. // eslint-disable-next-line no-console
  1572. console.log(this.serialize());
  1573. }
  1574. serialize() {
  1575. return (
  1576. '\n' +
  1577. this.list
  1578. .map(t => printTraceTreeNode(t, 0))
  1579. .filter(Boolean)
  1580. .join('\n') +
  1581. '\n'
  1582. );
  1583. }
  1584. }
  1585. // Generates a ID of the tree node based on its type
  1586. function nodeToId(n: TraceTreeNode<TraceTree.NodeValue>): TraceTree.NodePath {
  1587. if (isAutogroupedNode(n)) {
  1588. if (isParentAutogroupedNode(n)) {
  1589. return `ag-${n.head.value.span_id}`;
  1590. }
  1591. if (isSiblingAutogroupedNode(n)) {
  1592. const child = n.children[0];
  1593. if (isSpanNode(child)) {
  1594. return `ag-${child.value.span_id}`;
  1595. }
  1596. }
  1597. }
  1598. if (isTransactionNode(n)) {
  1599. return `txn-${n.value.event_id}`;
  1600. }
  1601. if (isSpanNode(n)) {
  1602. return `span-${n.value.span_id}`;
  1603. }
  1604. if (isTraceNode(n)) {
  1605. return `trace-root`;
  1606. }
  1607. if (isTraceErrorNode(n)) {
  1608. return `error-${n.value.event_id}`;
  1609. }
  1610. if (isRootNode(n)) {
  1611. throw new Error('A path to root node does not exist as the node is virtual');
  1612. }
  1613. if (isMissingInstrumentationNode(n)) {
  1614. return `ms-${n.previous.value.span_id}`;
  1615. }
  1616. throw new Error('Not implemented');
  1617. }
  1618. function printTraceTreeNode(
  1619. t: TraceTreeNode<TraceTree.NodeValue>,
  1620. offset: number
  1621. ): string {
  1622. // +1 because we may be printing from the root which is -1 indexed
  1623. const padding = ' '.repeat(TraceTree.Depth(t) + offset);
  1624. if (isAutogroupedNode(t)) {
  1625. if (isParentAutogroupedNode(t)) {
  1626. return padding + `parent autogroup (${t.head.value.op}: ${t.groupCount})`;
  1627. }
  1628. if (isSiblingAutogroupedNode(t)) {
  1629. return (
  1630. padding +
  1631. `sibling autogroup (${(t.children[0] as TraceTreeNode<TraceTree.Span>)?.value?.op}: ${t.groupCount})`
  1632. );
  1633. }
  1634. return padding + 'autogroup';
  1635. }
  1636. if (isSpanNode(t)) {
  1637. return (
  1638. padding +
  1639. (t.value.op || 'unknown span') +
  1640. ' - ' +
  1641. (t.value.description || 'unknown description')
  1642. );
  1643. }
  1644. if (isTransactionNode(t)) {
  1645. return (
  1646. padding +
  1647. (t.value.transaction || 'unknown transaction') +
  1648. ' - ' +
  1649. (t.value['transaction.op'] ?? 'unknown op')
  1650. );
  1651. }
  1652. if (isMissingInstrumentationNode(t)) {
  1653. return padding + 'missing_instrumentation';
  1654. }
  1655. if (isRootNode(t)) {
  1656. return padding + 'virtual root';
  1657. }
  1658. if (isTraceNode(t)) {
  1659. return padding + 'trace root';
  1660. }
  1661. if (isTraceErrorNode(t)) {
  1662. return padding + (t.value.event_id || t.value.level) || 'unknown trace error';
  1663. }
  1664. return 'unknown node';
  1665. }
  1666. // Double queue iterator to merge transactions and errors into a single list ordered by timestamp
  1667. // without having to reallocate the potentially large list of transactions and errors.
  1668. function traceQueueIterator(
  1669. trace: TraceTree.Trace,
  1670. root: TraceTreeNode<TraceTree.NodeValue>,
  1671. visitor: (
  1672. parent: TraceTreeNode<TraceTree.NodeValue>,
  1673. value: TraceTree.Transaction | TraceTree.TraceError
  1674. ) => void
  1675. ) {
  1676. let tIdx = 0;
  1677. let oIdx = 0;
  1678. const tLen = trace.transactions.length;
  1679. const oLen = trace.orphan_errors.length;
  1680. const transactions = [...trace.transactions].sort(
  1681. (a, b) => a.start_timestamp - b.start_timestamp
  1682. );
  1683. const orphan_errors = [...trace.orphan_errors].sort(
  1684. (a, b) => (a?.timestamp ?? 0) - (b?.timestamp ?? 0)
  1685. );
  1686. // Items in each queue are sorted by timestamp, so we just take
  1687. // from the queue with the earliest timestamp which means the final list will be ordered.
  1688. while (tIdx < tLen || oIdx < oLen) {
  1689. const transaction = transactions[tIdx];
  1690. const orphan = orphan_errors[oIdx];
  1691. if (transaction && orphan) {
  1692. if (
  1693. typeof orphan.timestamp === 'number' &&
  1694. transaction.start_timestamp <= orphan.timestamp
  1695. ) {
  1696. visitor(root, transaction);
  1697. tIdx++;
  1698. } else {
  1699. visitor(root, orphan);
  1700. oIdx++;
  1701. }
  1702. } else if (transaction) {
  1703. visitor(root, transaction);
  1704. tIdx++;
  1705. } else if (orphan) {
  1706. visitor(root, orphan);
  1707. oIdx++;
  1708. }
  1709. }
  1710. }
  1711. function traceChronologicalSort(
  1712. a: TraceTreeNode<TraceTree.NodeValue>,
  1713. b: TraceTreeNode<TraceTree.NodeValue>
  1714. ) {
  1715. return a.space[0] - b.space[0];
  1716. }
  1717. function getRelatedSpanErrorsFromTransaction(
  1718. span: TraceTree.Span,
  1719. node: TraceTreeNode<TraceTree.NodeValue>
  1720. ): TraceTree.TraceError[] {
  1721. if (!isTransactionNode(node) || !node.value?.errors?.length) {
  1722. return [];
  1723. }
  1724. const errors: TraceTree.TraceError[] = [];
  1725. for (const error of node.value.errors) {
  1726. if (error.span === span.span_id) {
  1727. errors.push(error);
  1728. }
  1729. }
  1730. return errors;
  1731. }
  1732. // Returns a list of performance errors related to the txn with ids matching the span id
  1733. function getRelatedPerformanceIssuesFromTransaction(
  1734. span: TraceTree.Span,
  1735. node: TraceTreeNode<TraceTree.NodeValue>
  1736. ): TraceTree.TracePerformanceIssue[] {
  1737. if (!isTransactionNode(node) || !node.value?.performance_issues?.length) {
  1738. return [];
  1739. }
  1740. const performanceIssues: TraceTree.TracePerformanceIssue[] = [];
  1741. for (const perfIssue of node.value.performance_issues) {
  1742. for (const s of perfIssue.span) {
  1743. if (s === span.span_id) {
  1744. performanceIssues.push(perfIssue);
  1745. }
  1746. }
  1747. for (const suspect of perfIssue.suspect_spans) {
  1748. if (suspect === span.span_id) {
  1749. performanceIssues.push(perfIssue);
  1750. }
  1751. }
  1752. }
  1753. return performanceIssues;
  1754. }