spanTreeModel.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. import {action, computed, makeObservable, observable} from 'mobx';
  2. import {Client} from 'sentry/api';
  3. import {t} from 'sentry/locale';
  4. import {EventTransaction} from 'sentry/types/event';
  5. import {ActiveOperationFilter} from './filter';
  6. import {
  7. DescendantGroup,
  8. EnhancedProcessedSpanType,
  9. EnhancedSpan,
  10. FetchEmbeddedChildrenState,
  11. FilterSpans,
  12. FocusedSpanIDMap,
  13. OrphanTreeDepth,
  14. RawSpanType,
  15. SpanChildrenLookupType,
  16. SpanType,
  17. TraceBound,
  18. TreeDepthType,
  19. } from './types';
  20. import {
  21. generateRootSpan,
  22. getSiblingGroupKey,
  23. getSpanID,
  24. getSpanOperation,
  25. isEventFromBrowserJavaScriptSDK,
  26. isOrphanSpan,
  27. isSpanIdFocused,
  28. parseTrace,
  29. SpanBoundsType,
  30. SpanGeneratedBoundsType,
  31. } from './utils';
  32. export const MIN_SIBLING_GROUP_SIZE = 5;
  33. class SpanTreeModel {
  34. api: Client;
  35. // readonly state
  36. span: Readonly<SpanType>;
  37. children: Array<SpanTreeModel> = [];
  38. isRoot: boolean;
  39. // readable/writable state
  40. fetchEmbeddedChildrenState: FetchEmbeddedChildrenState = 'idle';
  41. showEmbeddedChildren: boolean = false;
  42. embeddedChildren: Array<SpanTreeModel> = [];
  43. isEmbeddedTransactionTimeAdjusted: boolean = false;
  44. // This controls if a chain of nested spans that are the only sibling to be visually grouped together or not.
  45. // On initial render, they're visually grouped together.
  46. isNestedSpanGroupExpanded: boolean = false;
  47. // Entries in this set will follow the format 'op.description'.
  48. // An entry in this set indicates that all siblings with the op and description should be left ungrouped
  49. expandedSiblingGroups: Set<string> = new Set();
  50. constructor(
  51. parentSpan: SpanType,
  52. childSpans: SpanChildrenLookupType,
  53. api: Client,
  54. isRoot: boolean = false
  55. ) {
  56. this.api = api;
  57. this.span = parentSpan;
  58. this.isRoot = isRoot;
  59. const spanID = getSpanID(parentSpan);
  60. const spanChildren: Array<RawSpanType> = childSpans?.[spanID] ?? [];
  61. // Mark descendents as being rendered. This is to address potential recursion issues due to malformed data.
  62. // For example if a span has a span_id that's identical to its parent_span_id.
  63. childSpans = {
  64. ...childSpans,
  65. };
  66. delete childSpans[spanID];
  67. this.children = spanChildren.map(span => {
  68. return new SpanTreeModel(span, childSpans, api);
  69. });
  70. makeObservable(this, {
  71. operationNameCounts: computed.struct,
  72. showEmbeddedChildren: observable,
  73. embeddedChildren: observable,
  74. fetchEmbeddedChildrenState: observable,
  75. toggleEmbeddedChildren: action,
  76. fetchEmbeddedTransactions: action,
  77. isNestedSpanGroupExpanded: observable,
  78. toggleNestedSpanGroup: action,
  79. expandedSiblingGroups: observable,
  80. toggleSiblingSpanGroup: action,
  81. isEmbeddedTransactionTimeAdjusted: observable,
  82. });
  83. }
  84. get operationNameCounts(): Map<string, number> {
  85. const result = new Map<string, number>();
  86. const operationName = this.span.op;
  87. if (typeof operationName === 'string' && operationName.length > 0) {
  88. result.set(operationName, 1);
  89. }
  90. for (const directChild of this.children) {
  91. const operationNameCounts = directChild.operationNameCounts;
  92. for (const [key, count] of operationNameCounts) {
  93. result.set(key, (result.get(key) ?? 0) + count);
  94. }
  95. }
  96. // sort alphabetically using case insensitive comparison
  97. return new Map(
  98. [...result].sort((a, b) =>
  99. String(a[0]).localeCompare(b[0], undefined, {sensitivity: 'base'})
  100. )
  101. );
  102. }
  103. isSpanFilteredOut = (
  104. props: {
  105. filterSpans: FilterSpans | undefined;
  106. operationNameFilters: ActiveOperationFilter;
  107. },
  108. spanModel: SpanTreeModel
  109. ): boolean => {
  110. const {operationNameFilters, filterSpans} = props;
  111. if (operationNameFilters.type === 'active_filter') {
  112. const operationName = getSpanOperation(spanModel.span);
  113. if (
  114. typeof operationName === 'string' &&
  115. !operationNameFilters.operationNames.has(operationName)
  116. ) {
  117. return true;
  118. }
  119. }
  120. if (!filterSpans) {
  121. return false;
  122. }
  123. return !filterSpans.spanIDs.has(getSpanID(spanModel.span));
  124. };
  125. generateSpanGap(
  126. event: Readonly<EventTransaction>,
  127. previousSiblingEndTimestamp: number | undefined,
  128. treeDepth: number,
  129. continuingTreeDepths: Array<TreeDepthType>
  130. ): EnhancedProcessedSpanType | undefined {
  131. // hide gap spans (i.e. "missing instrumentation" spans) for browser js transactions,
  132. // since they're not useful to indicate
  133. const shouldIncludeGap = !isEventFromBrowserJavaScriptSDK(event);
  134. const isValidGap =
  135. shouldIncludeGap &&
  136. typeof previousSiblingEndTimestamp === 'number' &&
  137. previousSiblingEndTimestamp < this.span.start_timestamp &&
  138. // gap is at least 100 ms
  139. this.span.start_timestamp - previousSiblingEndTimestamp >= 0.1;
  140. if (!isValidGap) {
  141. return undefined;
  142. }
  143. const gapSpan: EnhancedProcessedSpanType = {
  144. type: 'gap',
  145. span: {
  146. type: 'gap',
  147. start_timestamp: previousSiblingEndTimestamp || this.span.start_timestamp,
  148. timestamp: this.span.start_timestamp, // this is essentially end_timestamp
  149. description: t('Missing instrumentation'),
  150. isOrphan: isOrphanSpan(this.span),
  151. },
  152. numOfSpanChildren: 0,
  153. treeDepth,
  154. isLastSibling: false,
  155. continuingTreeDepths,
  156. fetchEmbeddedChildrenState: 'idle',
  157. showEmbeddedChildren: false,
  158. toggleEmbeddedChildren: undefined,
  159. isEmbeddedTransactionTimeAdjusted: this.isEmbeddedTransactionTimeAdjusted,
  160. };
  161. return gapSpan;
  162. }
  163. getSpansList = (props: {
  164. addTraceBounds: (bounds: TraceBound) => void;
  165. continuingTreeDepths: Array<TreeDepthType>;
  166. directParent: SpanTreeModel | null;
  167. event: Readonly<EventTransaction>;
  168. filterSpans: FilterSpans | undefined;
  169. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
  170. hiddenSpanSubTrees: Set<String>;
  171. isLastSibling: boolean;
  172. isNestedSpanGroupExpanded: boolean;
  173. isOnlySibling: boolean;
  174. operationNameFilters: ActiveOperationFilter;
  175. previousSiblingEndTimestamp: number | undefined;
  176. removeTraceBounds: (eventSlug: string) => void;
  177. spanAncestors: Set<String>;
  178. spanNestedGrouping: EnhancedSpan[] | undefined;
  179. toggleNestedSpanGroup: (() => void) | undefined;
  180. treeDepth: number;
  181. focusedSpanIds?: FocusedSpanIDMap;
  182. }): EnhancedProcessedSpanType[] => {
  183. const {
  184. operationNameFilters,
  185. generateBounds,
  186. isLastSibling,
  187. hiddenSpanSubTrees,
  188. // The set of ancestor span IDs whose sub-tree that the span belongs to
  189. spanAncestors,
  190. filterSpans,
  191. previousSiblingEndTimestamp,
  192. event,
  193. isOnlySibling,
  194. spanNestedGrouping,
  195. toggleNestedSpanGroup,
  196. isNestedSpanGroupExpanded,
  197. addTraceBounds,
  198. removeTraceBounds,
  199. focusedSpanIds,
  200. directParent,
  201. } = props;
  202. let {treeDepth, continuingTreeDepths} = props;
  203. const parentSpanID = getSpanID(this.span);
  204. const nextSpanAncestors = new Set(spanAncestors);
  205. nextSpanAncestors.add(parentSpanID);
  206. const descendantsSource = this.showEmbeddedChildren
  207. ? [...this.embeddedChildren, ...this.children]
  208. : this.children;
  209. if (focusedSpanIds && this.span.span_id in focusedSpanIds) {
  210. // Since this is a focused span, show this span's direct parent, and also its children
  211. directParent && focusedSpanIds[this.span.span_id].add(directParent.span.span_id);
  212. descendantsSource.forEach(descendant =>
  213. focusedSpanIds[this.span.span_id].add(descendant.span.span_id)
  214. );
  215. }
  216. const isNotLastSpanOfGroup =
  217. isOnlySibling && !this.isRoot && descendantsSource.length === 1;
  218. const shouldGroup = isNotLastSpanOfGroup;
  219. const hideSpanTree = hiddenSpanSubTrees.has(parentSpanID);
  220. const isLastSpanOfGroup =
  221. isOnlySibling && !this.isRoot && (descendantsSource.length !== 1 || hideSpanTree);
  222. const isFirstSpanOfGroup =
  223. shouldGroup &&
  224. (spanNestedGrouping === undefined ||
  225. (Array.isArray(spanNestedGrouping) && spanNestedGrouping.length === 0));
  226. if (
  227. isLastSpanOfGroup &&
  228. Array.isArray(spanNestedGrouping) &&
  229. spanNestedGrouping.length >= 1 &&
  230. !isNestedSpanGroupExpanded
  231. ) {
  232. // We always want to indent the last span of the span group chain
  233. treeDepth = treeDepth + 1;
  234. // For a collapsed span group chain to be useful, we prefer span groupings
  235. // that are two or more spans.
  236. // Since there is no concept of "backtracking" when constructing the span tree,
  237. // we will need to reconstruct the tree depth information. This is only neccessary
  238. // when the span group chain is hidden/collapsed.
  239. if (spanNestedGrouping.length === 1) {
  240. const treeDepthEntry = isOrphanSpan(spanNestedGrouping[0].span)
  241. ? ({type: 'orphan', depth: spanNestedGrouping[0].treeDepth} as OrphanTreeDepth)
  242. : spanNestedGrouping[0].treeDepth;
  243. if (!spanNestedGrouping[0].isLastSibling) {
  244. continuingTreeDepths = [...continuingTreeDepths, treeDepthEntry];
  245. }
  246. }
  247. }
  248. // Criteria for propagating information about the span group to the last span of the span group chain
  249. const spanGroupingCriteria =
  250. isLastSpanOfGroup &&
  251. Array.isArray(spanNestedGrouping) &&
  252. spanNestedGrouping.length > 1;
  253. const wrappedSpan: EnhancedSpan = {
  254. type: this.isRoot ? 'root_span' : 'span',
  255. span: this.span,
  256. numOfSpanChildren: descendantsSource.length,
  257. treeDepth,
  258. isLastSibling,
  259. continuingTreeDepths,
  260. fetchEmbeddedChildrenState: this.fetchEmbeddedChildrenState,
  261. showEmbeddedChildren: this.showEmbeddedChildren,
  262. toggleEmbeddedChildren: this.toggleEmbeddedChildren({
  263. addTraceBounds,
  264. removeTraceBounds,
  265. }),
  266. toggleNestedSpanGroup:
  267. spanGroupingCriteria && toggleNestedSpanGroup && !isNestedSpanGroupExpanded
  268. ? toggleNestedSpanGroup
  269. : isFirstSpanOfGroup && this.isNestedSpanGroupExpanded && !hideSpanTree
  270. ? this.toggleNestedSpanGroup
  271. : undefined,
  272. toggleSiblingSpanGroup: undefined,
  273. isEmbeddedTransactionTimeAdjusted: this.isEmbeddedTransactionTimeAdjusted,
  274. };
  275. if (wrappedSpan.type === 'root_span') {
  276. // @ts-expect-error
  277. delete wrappedSpan.toggleNestedSpanGroup;
  278. }
  279. const treeDepthEntry = isOrphanSpan(this.span)
  280. ? ({type: 'orphan', depth: treeDepth} as OrphanTreeDepth)
  281. : treeDepth;
  282. const shouldHideSpanOfGroup =
  283. shouldGroup &&
  284. !isLastSpanOfGroup &&
  285. ((toggleNestedSpanGroup === undefined && !this.isNestedSpanGroupExpanded) ||
  286. (toggleNestedSpanGroup !== undefined && !isNestedSpanGroupExpanded));
  287. const descendantContinuingTreeDepths =
  288. isLastSibling || shouldHideSpanOfGroup
  289. ? continuingTreeDepths
  290. : [...continuingTreeDepths, treeDepthEntry];
  291. for (const hiddenSpanSubTree of hiddenSpanSubTrees) {
  292. if (spanAncestors.has(hiddenSpanSubTree)) {
  293. // If this span is hidden, then all the descendants are hidden as well
  294. return [];
  295. }
  296. }
  297. const groupedDescendants: DescendantGroup[] = [];
  298. // Used to number sibling groups in case there are multiple groups with the same op and description
  299. const siblingGroupOccurrenceMap = {};
  300. const addGroupToMap = (prevSpanModel: SpanTreeModel, group: SpanTreeModel[]) => {
  301. if (!group.length) {
  302. return;
  303. }
  304. const groupKey = `${prevSpanModel.span.op}.${prevSpanModel.span.description}`;
  305. if (!siblingGroupOccurrenceMap[groupKey]) {
  306. siblingGroupOccurrenceMap[groupKey] = 1;
  307. } else {
  308. siblingGroupOccurrenceMap[groupKey] += 1;
  309. }
  310. groupedDescendants.push({
  311. group,
  312. occurrence: siblingGroupOccurrenceMap[groupKey],
  313. });
  314. };
  315. if (descendantsSource?.length >= MIN_SIBLING_GROUP_SIZE) {
  316. let prevSpanModel = descendantsSource[0];
  317. let currentGroup = [prevSpanModel];
  318. for (let i = 1; i < descendantsSource.length; i++) {
  319. const currSpanModel = descendantsSource[i];
  320. // We want to group siblings only if they share the same op and description, and if they have no children
  321. if (
  322. prevSpanModel.span.op === currSpanModel.span.op &&
  323. prevSpanModel.span.description === currSpanModel.span.description &&
  324. currSpanModel.children.length === 0
  325. ) {
  326. currentGroup.push(currSpanModel);
  327. } else {
  328. addGroupToMap(prevSpanModel, currentGroup);
  329. if (currSpanModel.children.length) {
  330. currentGroup = [currSpanModel];
  331. groupedDescendants.push({group: currentGroup});
  332. currentGroup = [];
  333. } else {
  334. currentGroup = [currSpanModel];
  335. }
  336. }
  337. prevSpanModel = currSpanModel;
  338. }
  339. addGroupToMap(prevSpanModel, currentGroup);
  340. } else if (descendantsSource.length >= 1) {
  341. groupedDescendants.push({group: descendantsSource});
  342. }
  343. const descendants = (hideSpanTree ? [] : groupedDescendants).reduce(
  344. (
  345. acc: {
  346. descendants: EnhancedProcessedSpanType[];
  347. previousSiblingEndTimestamp: number | undefined;
  348. },
  349. {group, occurrence},
  350. groupIndex
  351. ) => {
  352. // Groups less than 5 indicate that the spans should be left ungrouped
  353. if (group.length < MIN_SIBLING_GROUP_SIZE) {
  354. group.forEach((spanModel, index) => {
  355. acc.descendants.push(
  356. ...spanModel.getSpansList({
  357. operationNameFilters,
  358. generateBounds,
  359. treeDepth: shouldHideSpanOfGroup ? treeDepth : treeDepth + 1,
  360. isLastSibling:
  361. groupIndex === groupedDescendants.length - 1 &&
  362. index === group.length - 1,
  363. continuingTreeDepths: descendantContinuingTreeDepths,
  364. hiddenSpanSubTrees,
  365. spanAncestors: new Set(nextSpanAncestors),
  366. filterSpans,
  367. previousSiblingEndTimestamp: acc.previousSiblingEndTimestamp,
  368. event,
  369. isOnlySibling: descendantsSource.length === 1,
  370. spanNestedGrouping: shouldGroup
  371. ? [...(spanNestedGrouping ?? []), wrappedSpan]
  372. : undefined,
  373. toggleNestedSpanGroup: isNotLastSpanOfGroup
  374. ? toggleNestedSpanGroup === undefined
  375. ? this.toggleNestedSpanGroup
  376. : toggleNestedSpanGroup
  377. : undefined,
  378. isNestedSpanGroupExpanded: isNotLastSpanOfGroup
  379. ? toggleNestedSpanGroup === undefined
  380. ? this.isNestedSpanGroupExpanded
  381. : isNestedSpanGroupExpanded
  382. : false,
  383. addTraceBounds,
  384. removeTraceBounds,
  385. focusedSpanIds,
  386. directParent: this,
  387. })
  388. );
  389. acc.previousSiblingEndTimestamp = spanModel.span.timestamp;
  390. });
  391. return acc;
  392. }
  393. // NOTE: I am making the assumption here that grouped sibling spans will not have children.
  394. // By making this assumption, I can immediately wrap the grouped spans here without having
  395. // to recursively traverse them.
  396. // This may not be the case, and needs to be looked into later
  397. const key = getSiblingGroupKey(group[0].span, occurrence);
  398. if (this.expandedSiblingGroups.has(key)) {
  399. // This check is needed here, since it is possible that a user could be filtering for a specific span ID.
  400. // In this case, we must add only the specified span into the accumulator's descendants
  401. group.forEach((spanModel, index) => {
  402. if (
  403. this.isSpanFilteredOut(props, spanModel) ||
  404. (focusedSpanIds && !isSpanIdFocused(spanModel.span.span_id, focusedSpanIds))
  405. ) {
  406. acc.descendants.push({
  407. type: 'filtered_out',
  408. span: spanModel.span,
  409. });
  410. } else {
  411. const enhancedSibling: EnhancedSpan = {
  412. type: 'span',
  413. span: spanModel.span,
  414. numOfSpanChildren: 0,
  415. treeDepth: treeDepth + 1,
  416. isLastSibling:
  417. index === group.length - 1 &&
  418. groupIndex === groupedDescendants.length - 1,
  419. isFirstSiblingOfGroup: index === 0,
  420. groupOccurrence: occurrence,
  421. continuingTreeDepths: descendantContinuingTreeDepths,
  422. fetchEmbeddedChildrenState: spanModel.fetchEmbeddedChildrenState,
  423. showEmbeddedChildren: spanModel.showEmbeddedChildren,
  424. toggleEmbeddedChildren: spanModel.toggleEmbeddedChildren({
  425. addTraceBounds,
  426. removeTraceBounds,
  427. }),
  428. toggleNestedSpanGroup: undefined,
  429. toggleSiblingSpanGroup:
  430. index === 0 ? this.toggleSiblingSpanGroup : undefined,
  431. isEmbeddedTransactionTimeAdjusted:
  432. spanModel.isEmbeddedTransactionTimeAdjusted,
  433. };
  434. acc.previousSiblingEndTimestamp = spanModel.span.timestamp;
  435. acc.descendants.push(enhancedSibling);
  436. }
  437. });
  438. return acc;
  439. }
  440. // Since we are not recursively traversing elements in this group, need to check
  441. // if the spans are filtered or out of bounds here
  442. if (this.isSpanFilteredOut(props, group[0])) {
  443. group.forEach(spanModel => {
  444. acc.descendants.push({
  445. type: 'filtered_out',
  446. span: spanModel.span,
  447. });
  448. });
  449. return acc;
  450. }
  451. // TODO: Check within the group if any of the focusedSpanIDs are present
  452. const bounds = generateBounds({
  453. startTimestamp: group[0].span.start_timestamp,
  454. endTimestamp: group[group.length - 1].span.timestamp,
  455. });
  456. if (!bounds.isSpanVisibleInView) {
  457. group.forEach(spanModel =>
  458. acc.descendants.push({
  459. type: 'out_of_view',
  460. span: spanModel.span,
  461. })
  462. );
  463. return acc;
  464. }
  465. // Since the group is not expanded, return a singular grouped span bar
  466. const wrappedSiblings: EnhancedSpan[] = group.map((spanModel, index) => {
  467. const enhancedSibling: EnhancedSpan = {
  468. type: 'span',
  469. span: spanModel.span,
  470. numOfSpanChildren: 0,
  471. treeDepth: treeDepth + 1,
  472. isLastSibling:
  473. index === group.length - 1 && groupIndex === groupedDescendants.length - 1,
  474. isFirstSiblingOfGroup: index === 0,
  475. groupOccurrence: occurrence,
  476. continuingTreeDepths: descendantContinuingTreeDepths,
  477. fetchEmbeddedChildrenState: spanModel.fetchEmbeddedChildrenState,
  478. showEmbeddedChildren: spanModel.showEmbeddedChildren,
  479. toggleEmbeddedChildren: spanModel.toggleEmbeddedChildren({
  480. addTraceBounds,
  481. removeTraceBounds,
  482. }),
  483. toggleNestedSpanGroup: undefined,
  484. toggleSiblingSpanGroup: index === 0 ? this.toggleSiblingSpanGroup : undefined,
  485. isEmbeddedTransactionTimeAdjusted:
  486. spanModel.isEmbeddedTransactionTimeAdjusted,
  487. };
  488. return enhancedSibling;
  489. });
  490. const groupedSiblingsSpan: EnhancedProcessedSpanType = {
  491. type: 'span_group_siblings',
  492. span: this.span,
  493. treeDepth: treeDepth + 1,
  494. continuingTreeDepths: descendantContinuingTreeDepths,
  495. spanSiblingGrouping: wrappedSiblings,
  496. isLastSibling: groupIndex === groupedDescendants.length - 1,
  497. occurrence: occurrence ?? 0,
  498. toggleSiblingSpanGroup: this.toggleSiblingSpanGroup,
  499. };
  500. acc.previousSiblingEndTimestamp =
  501. wrappedSiblings[wrappedSiblings.length - 1].span.timestamp;
  502. acc.descendants.push(groupedSiblingsSpan);
  503. return acc;
  504. },
  505. {
  506. descendants: [],
  507. previousSiblingEndTimestamp: undefined,
  508. }
  509. ).descendants;
  510. if (
  511. this.isSpanFilteredOut(props, this) ||
  512. (focusedSpanIds && !isSpanIdFocused(this.span.span_id, focusedSpanIds))
  513. ) {
  514. return [
  515. {
  516. type: 'filtered_out',
  517. span: this.span,
  518. },
  519. ...descendants,
  520. ];
  521. }
  522. const bounds = generateBounds({
  523. startTimestamp: this.span.start_timestamp,
  524. endTimestamp: this.span.timestamp,
  525. });
  526. const isCurrentSpanOutOfView = !bounds.isSpanVisibleInView;
  527. if (isCurrentSpanOutOfView) {
  528. return [
  529. {
  530. type: 'out_of_view',
  531. span: this.span,
  532. },
  533. ...descendants,
  534. ];
  535. }
  536. if (shouldHideSpanOfGroup) {
  537. return [...descendants];
  538. }
  539. if (
  540. isLastSpanOfGroup &&
  541. Array.isArray(spanNestedGrouping) &&
  542. spanNestedGrouping.length > 1 &&
  543. !isNestedSpanGroupExpanded &&
  544. wrappedSpan.type === 'span'
  545. ) {
  546. const spanGroupChain: EnhancedProcessedSpanType = {
  547. type: 'span_group_chain',
  548. span: this.span,
  549. treeDepth: treeDepth - 1,
  550. continuingTreeDepths,
  551. spanNestedGrouping,
  552. isNestedSpanGroupExpanded,
  553. toggleNestedSpanGroup: wrappedSpan.toggleNestedSpanGroup,
  554. toggleSiblingSpanGroup: undefined,
  555. };
  556. return [
  557. spanGroupChain,
  558. {...wrappedSpan, toggleNestedSpanGroup: undefined},
  559. ...descendants,
  560. ];
  561. }
  562. if (
  563. isFirstSpanOfGroup &&
  564. this.isNestedSpanGroupExpanded &&
  565. !hideSpanTree &&
  566. descendants.length <= 1 &&
  567. wrappedSpan.type === 'span'
  568. ) {
  569. // If we know the descendants will be one span or less, we remove the "regroup" feature (therefore hide it)
  570. // by setting toggleNestedSpanGroup to be undefined for the first span of the group chain.
  571. wrappedSpan.toggleNestedSpanGroup = undefined;
  572. }
  573. // Do not autogroup groups that will only have two spans
  574. if (
  575. isLastSpanOfGroup &&
  576. Array.isArray(spanNestedGrouping) &&
  577. spanNestedGrouping.length === 1
  578. ) {
  579. if (!isNestedSpanGroupExpanded) {
  580. const parentSpan = spanNestedGrouping[0].span;
  581. const parentSpanBounds = generateBounds({
  582. startTimestamp: parentSpan.start_timestamp,
  583. endTimestamp: parentSpan.timestamp,
  584. });
  585. const isParentSpanOutOfView = !parentSpanBounds.isSpanVisibleInView;
  586. if (!isParentSpanOutOfView) {
  587. return [spanNestedGrouping[0], wrappedSpan, ...descendants];
  588. }
  589. }
  590. return [wrappedSpan, ...descendants];
  591. }
  592. const gapSpan = this.generateSpanGap(
  593. event,
  594. previousSiblingEndTimestamp,
  595. treeDepth,
  596. continuingTreeDepths
  597. );
  598. if (gapSpan) {
  599. return [gapSpan, wrappedSpan, ...descendants];
  600. }
  601. return [wrappedSpan, ...descendants];
  602. };
  603. toggleEmbeddedChildren =
  604. ({
  605. addTraceBounds,
  606. removeTraceBounds,
  607. }: {
  608. addTraceBounds: (bounds: TraceBound) => void;
  609. removeTraceBounds: (eventSlug: string) => void;
  610. }) =>
  611. (props: {eventSlug: string; orgSlug: string}) => {
  612. this.showEmbeddedChildren = !this.showEmbeddedChildren;
  613. this.fetchEmbeddedChildrenState = 'idle';
  614. if (!this.showEmbeddedChildren) {
  615. if (this.embeddedChildren.length > 0) {
  616. this.embeddedChildren.forEach(child => {
  617. removeTraceBounds(child.generateTraceBounds().spanId);
  618. });
  619. }
  620. }
  621. if (this.showEmbeddedChildren) {
  622. if (this.embeddedChildren.length === 0) {
  623. return this.fetchEmbeddedTransactions({...props, addTraceBounds});
  624. }
  625. this.embeddedChildren.forEach(child => {
  626. addTraceBounds(child.generateTraceBounds());
  627. });
  628. }
  629. return Promise.resolve(undefined);
  630. };
  631. fetchEmbeddedTransactions({
  632. orgSlug,
  633. eventSlug,
  634. addTraceBounds,
  635. }: {
  636. addTraceBounds: (bounds: TraceBound) => void;
  637. eventSlug: string;
  638. orgSlug: string;
  639. }) {
  640. const url = `/organizations/${orgSlug}/events/${eventSlug}/`;
  641. this.fetchEmbeddedChildrenState = 'loading_embedded_transactions';
  642. return this.api
  643. .requestPromise(url, {
  644. method: 'GET',
  645. query: {},
  646. })
  647. .then(
  648. action('fetchEmbeddedTransactionsSuccess', (event: EventTransaction) => {
  649. if (!event) {
  650. return;
  651. }
  652. const parsedTrace = parseTrace(event);
  653. // We need to adjust the timestamps for this embedded transaction only if it is not within the bounds of its parent span
  654. if (
  655. parsedTrace.traceStartTimestamp < this.span.start_timestamp ||
  656. parsedTrace.traceEndTimestamp > this.span.timestamp
  657. ) {
  658. const startTimeDelta =
  659. this.span.start_timestamp - parsedTrace.traceStartTimestamp;
  660. parsedTrace.traceStartTimestamp += startTimeDelta;
  661. parsedTrace.traceEndTimestamp += startTimeDelta;
  662. parsedTrace.spans.forEach(span => {
  663. span.start_timestamp += startTimeDelta;
  664. span.timestamp += startTimeDelta;
  665. });
  666. this.isEmbeddedTransactionTimeAdjusted = true;
  667. }
  668. const rootSpan = generateRootSpan(parsedTrace);
  669. const parsedRootSpan = new SpanTreeModel(
  670. rootSpan,
  671. parsedTrace.childSpans,
  672. this.api,
  673. false
  674. );
  675. this.embeddedChildren = [parsedRootSpan];
  676. this.fetchEmbeddedChildrenState = 'idle';
  677. addTraceBounds(parsedRootSpan.generateTraceBounds());
  678. })
  679. )
  680. .catch(
  681. action('fetchEmbeddedTransactionsError', () => {
  682. this.embeddedChildren = [];
  683. this.fetchEmbeddedChildrenState = 'error_fetching_embedded_transactions';
  684. })
  685. );
  686. }
  687. toggleNestedSpanGroup = () => {
  688. this.isNestedSpanGroupExpanded = !this.isNestedSpanGroupExpanded;
  689. };
  690. toggleSiblingSpanGroup = (span: SpanType, occurrence?: number) => {
  691. const key = getSiblingGroupKey(span, occurrence);
  692. if (this.expandedSiblingGroups.has(key)) {
  693. this.expandedSiblingGroups.delete(key);
  694. } else {
  695. this.expandedSiblingGroups.add(key);
  696. }
  697. };
  698. generateTraceBounds = (): TraceBound => {
  699. return {
  700. spanId: this.span.span_id,
  701. traceStartTimestamp: this.span.start_timestamp,
  702. traceEndTimestamp: this.span.timestamp,
  703. };
  704. };
  705. }
  706. export default SpanTreeModel;