spanTreeModel.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. import {action, computed, makeObservable, observable} from 'mobx';
  2. import {Client} from 'sentry/api';
  3. import {t} from 'sentry/locale';
  4. import {AggregateEventTransaction, EventTransaction} from 'sentry/types/event';
  5. import {ActiveOperationFilter} from './filter';
  6. import {
  7. DescendantGroup,
  8. EnhancedProcessedSpanType,
  9. EnhancedSpan,
  10. FetchEmbeddedChildrenState,
  11. FilterSpans,
  12. OrphanTreeDepth,
  13. RawSpanType,
  14. SpanChildrenLookupType,
  15. SpanType,
  16. TraceBound,
  17. TreeDepthType,
  18. } from './types';
  19. import {
  20. generateRootSpan,
  21. getSiblingGroupKey,
  22. getSpanID,
  23. getSpanOperation,
  24. groupShouldBeHidden,
  25. isEventFromBrowserJavaScriptSDK,
  26. isOrphanSpan,
  27. parseTrace,
  28. SpanBoundsType,
  29. SpanGeneratedBoundsType,
  30. SpanSubTimingMark,
  31. subTimingMarkToTime,
  32. } from './utils';
  33. export const MIN_SIBLING_GROUP_SIZE = 5;
  34. class SpanTreeModel {
  35. api: Client;
  36. // readonly state
  37. span: Readonly<SpanType>;
  38. children: Array<SpanTreeModel> = [];
  39. isRoot: boolean;
  40. // readable/writable state
  41. fetchEmbeddedChildrenState: FetchEmbeddedChildrenState = 'idle';
  42. showEmbeddedChildren: boolean = false;
  43. embeddedChildren: Array<SpanTreeModel> = [];
  44. isEmbeddedTransactionTimeAdjusted: boolean = false;
  45. // This controls if a chain of nested spans that are the only sibling to be visually grouped together or not.
  46. // On initial render, they're visually grouped together.
  47. isNestedSpanGroupExpanded: boolean = false;
  48. // Entries in this set will follow the format 'op.description'.
  49. // An entry in this set indicates that all siblings with the op and description should be left ungrouped
  50. expandedSiblingGroups: Set<string> = new Set();
  51. constructor(
  52. parentSpan: SpanType,
  53. childSpans: SpanChildrenLookupType,
  54. api: Client,
  55. isRoot: boolean = false
  56. ) {
  57. this.api = api;
  58. this.span = parentSpan;
  59. this.isRoot = isRoot;
  60. const spanID = getSpanID(parentSpan);
  61. const spanChildren: Array<RawSpanType> = childSpans?.[spanID] ?? [];
  62. // Mark descendents as being rendered. This is to address potential recursion issues due to malformed data.
  63. // For example if a span has a span_id that's identical to its parent_span_id.
  64. childSpans = {
  65. ...childSpans,
  66. };
  67. delete childSpans[spanID];
  68. this.children = spanChildren.map(span => {
  69. return new SpanTreeModel(span, childSpans, api);
  70. });
  71. makeObservable(this, {
  72. operationNameCounts: computed.struct,
  73. showEmbeddedChildren: observable,
  74. embeddedChildren: observable,
  75. fetchEmbeddedChildrenState: observable,
  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 | AggregateEventTransaction>,
  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 span 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 | AggregateEventTransaction>;
  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?: Set<string>;
  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. } = props;
  201. let {treeDepth, continuingTreeDepths} = props;
  202. const parentSpanID = getSpanID(this.span);
  203. const nextSpanAncestors = new Set(spanAncestors);
  204. nextSpanAncestors.add(parentSpanID);
  205. const descendantsSource = this.showEmbeddedChildren
  206. ? [...this.embeddedChildren, ...this.children]
  207. : this.children;
  208. const isNotLastSpanOfGroup =
  209. isOnlySibling && !this.isRoot && descendantsSource.length === 1;
  210. const shouldGroup = isNotLastSpanOfGroup;
  211. const hideSpanTree = hiddenSpanSubTrees.has(parentSpanID);
  212. const isLastSpanOfGroup =
  213. isOnlySibling && !this.isRoot && (descendantsSource.length !== 1 || hideSpanTree);
  214. const isFirstSpanOfGroup =
  215. shouldGroup &&
  216. (spanNestedGrouping === undefined ||
  217. (Array.isArray(spanNestedGrouping) && spanNestedGrouping.length === 0));
  218. if (
  219. isLastSpanOfGroup &&
  220. Array.isArray(spanNestedGrouping) &&
  221. spanNestedGrouping.length >= 1 &&
  222. !isNestedSpanGroupExpanded
  223. ) {
  224. // We always want to indent the last span of the span group chain
  225. treeDepth = treeDepth + 1;
  226. // For a collapsed span group chain to be useful, we prefer span groupings
  227. // that are two or more spans.
  228. // Since there is no concept of "backtracking" when constructing the span tree,
  229. // we will need to reconstruct the tree depth information. This is only neccessary
  230. // when the span group chain is hidden/collapsed.
  231. if (spanNestedGrouping.length === 1) {
  232. const treeDepthEntry = isOrphanSpan(spanNestedGrouping[0].span)
  233. ? ({type: 'orphan', depth: spanNestedGrouping[0].treeDepth} as OrphanTreeDepth)
  234. : spanNestedGrouping[0].treeDepth;
  235. if (!spanNestedGrouping[0].isLastSibling) {
  236. continuingTreeDepths = [...continuingTreeDepths, treeDepthEntry];
  237. }
  238. }
  239. }
  240. // Criteria for propagating information about the span group to the last span of the span group chain
  241. const spanGroupingCriteria =
  242. isLastSpanOfGroup &&
  243. Array.isArray(spanNestedGrouping) &&
  244. spanNestedGrouping.length > 1;
  245. const wrappedSpan: EnhancedSpan = {
  246. type: this.isRoot ? 'root_span' : 'span',
  247. span: this.span,
  248. numOfSpanChildren: descendantsSource.length,
  249. treeDepth,
  250. isLastSibling,
  251. continuingTreeDepths,
  252. fetchEmbeddedChildrenState: this.fetchEmbeddedChildrenState,
  253. showEmbeddedChildren: this.showEmbeddedChildren,
  254. toggleEmbeddedChildren: this.makeToggleEmbeddedChildren({
  255. addTraceBounds,
  256. removeTraceBounds,
  257. }),
  258. toggleNestedSpanGroup:
  259. spanGroupingCriteria && toggleNestedSpanGroup && !isNestedSpanGroupExpanded
  260. ? toggleNestedSpanGroup
  261. : isFirstSpanOfGroup && this.isNestedSpanGroupExpanded && !hideSpanTree
  262. ? this.toggleNestedSpanGroup
  263. : undefined,
  264. toggleSiblingSpanGroup: undefined,
  265. isEmbeddedTransactionTimeAdjusted: this.isEmbeddedTransactionTimeAdjusted,
  266. };
  267. if (wrappedSpan.type === 'root_span') {
  268. // @ts-expect-error
  269. delete wrappedSpan.toggleNestedSpanGroup;
  270. }
  271. const treeDepthEntry = isOrphanSpan(this.span)
  272. ? ({type: 'orphan', depth: treeDepth} as OrphanTreeDepth)
  273. : treeDepth;
  274. const shouldHideSpanOfGroup =
  275. shouldGroup &&
  276. !isLastSpanOfGroup &&
  277. ((toggleNestedSpanGroup === undefined && !this.isNestedSpanGroupExpanded) ||
  278. (toggleNestedSpanGroup !== undefined && !isNestedSpanGroupExpanded));
  279. const descendantContinuingTreeDepths =
  280. isLastSibling || shouldHideSpanOfGroup
  281. ? continuingTreeDepths
  282. : [...continuingTreeDepths, treeDepthEntry];
  283. for (const hiddenSpanSubTree of hiddenSpanSubTrees) {
  284. if (spanAncestors.has(hiddenSpanSubTree)) {
  285. // If this span is hidden, then all the descendants are hidden as well
  286. return [];
  287. }
  288. }
  289. const groupedDescendants: DescendantGroup[] = [];
  290. // Used to number sibling groups in case there are multiple groups with the same op and description
  291. const siblingGroupOccurrenceMap = {};
  292. const addGroupToMap = (prevSpanModel: SpanTreeModel, group: SpanTreeModel[]) => {
  293. if (!group.length) {
  294. return;
  295. }
  296. const groupKey = `${prevSpanModel.span.op}.${prevSpanModel.span.description}`;
  297. if (!siblingGroupOccurrenceMap[groupKey]) {
  298. siblingGroupOccurrenceMap[groupKey] = 1;
  299. } else {
  300. siblingGroupOccurrenceMap[groupKey] += 1;
  301. }
  302. groupedDescendants.push({
  303. group,
  304. occurrence: siblingGroupOccurrenceMap[groupKey],
  305. });
  306. };
  307. if (descendantsSource?.length >= MIN_SIBLING_GROUP_SIZE) {
  308. let prevSpanModel = descendantsSource[0];
  309. let currentGroup = [prevSpanModel];
  310. for (let i = 1; i < descendantsSource.length; i++) {
  311. const currSpanModel = descendantsSource[i];
  312. // We want to group siblings only if they share the same op and description, and if they have no children
  313. if (
  314. prevSpanModel.span.op === currSpanModel.span.op &&
  315. prevSpanModel.span.description === currSpanModel.span.description &&
  316. currSpanModel.children.length === 0
  317. ) {
  318. currentGroup.push(currSpanModel);
  319. } else {
  320. addGroupToMap(prevSpanModel, currentGroup);
  321. if (currSpanModel.children.length) {
  322. currentGroup = [currSpanModel];
  323. groupedDescendants.push({group: currentGroup});
  324. currentGroup = [];
  325. } else {
  326. currentGroup = [currSpanModel];
  327. }
  328. }
  329. prevSpanModel = currSpanModel;
  330. }
  331. addGroupToMap(prevSpanModel, currentGroup);
  332. } else if (descendantsSource.length >= 1) {
  333. groupedDescendants.push({group: descendantsSource});
  334. }
  335. const descendants = (hideSpanTree ? [] : groupedDescendants).reduce(
  336. (
  337. acc: {
  338. descendants: EnhancedProcessedSpanType[];
  339. previousSiblingEndTimestamp: number | undefined;
  340. },
  341. {group, occurrence},
  342. groupIndex
  343. ) => {
  344. // Groups less than 5 indicate that the spans should be left ungrouped
  345. if (group.length < MIN_SIBLING_GROUP_SIZE) {
  346. group.forEach((spanModel, index) => {
  347. acc.descendants.push(
  348. ...spanModel.getSpansList({
  349. operationNameFilters,
  350. generateBounds,
  351. treeDepth: shouldHideSpanOfGroup ? treeDepth : treeDepth + 1,
  352. isLastSibling:
  353. groupIndex === groupedDescendants.length - 1 &&
  354. index === group.length - 1,
  355. continuingTreeDepths: descendantContinuingTreeDepths,
  356. hiddenSpanSubTrees,
  357. spanAncestors: new Set(nextSpanAncestors),
  358. filterSpans,
  359. previousSiblingEndTimestamp: acc.previousSiblingEndTimestamp,
  360. event,
  361. isOnlySibling: descendantsSource.length === 1,
  362. spanNestedGrouping: shouldGroup
  363. ? [...(spanNestedGrouping ?? []), wrappedSpan]
  364. : undefined,
  365. toggleNestedSpanGroup: isNotLastSpanOfGroup
  366. ? toggleNestedSpanGroup === undefined
  367. ? this.toggleNestedSpanGroup
  368. : toggleNestedSpanGroup
  369. : undefined,
  370. isNestedSpanGroupExpanded: isNotLastSpanOfGroup
  371. ? toggleNestedSpanGroup === undefined
  372. ? this.isNestedSpanGroupExpanded
  373. : isNestedSpanGroupExpanded
  374. : false,
  375. addTraceBounds,
  376. removeTraceBounds,
  377. focusedSpanIds,
  378. directParent: this,
  379. })
  380. );
  381. acc.previousSiblingEndTimestamp = spanModel.span.timestamp;
  382. });
  383. return acc;
  384. }
  385. const key = getSiblingGroupKey(group[0].span, occurrence);
  386. if (this.expandedSiblingGroups.has(key)) {
  387. // This check is needed here, since it is possible that a user could be filtering for a specific span ID.
  388. // In this case, we must add only the specified span into the accumulator's descendants
  389. group.forEach((spanModel, index) => {
  390. if (
  391. this.isSpanFilteredOut(props, spanModel) ||
  392. (focusedSpanIds && !focusedSpanIds.has(spanModel.span.span_id))
  393. ) {
  394. acc.descendants.push({
  395. type: 'filtered_out',
  396. span: spanModel.span,
  397. });
  398. } else {
  399. const enhancedSibling: EnhancedSpan = {
  400. type: 'span',
  401. span: spanModel.span,
  402. numOfSpanChildren: 0,
  403. treeDepth: treeDepth + 1,
  404. isLastSibling:
  405. index === group.length - 1 &&
  406. groupIndex === groupedDescendants.length - 1,
  407. isFirstSiblingOfGroup: index === 0,
  408. groupOccurrence: occurrence,
  409. continuingTreeDepths: descendantContinuingTreeDepths,
  410. fetchEmbeddedChildrenState: spanModel.fetchEmbeddedChildrenState,
  411. showEmbeddedChildren: spanModel.showEmbeddedChildren,
  412. toggleEmbeddedChildren: spanModel.makeToggleEmbeddedChildren({
  413. addTraceBounds,
  414. removeTraceBounds,
  415. }),
  416. toggleNestedSpanGroup: undefined,
  417. toggleSiblingSpanGroup:
  418. index === 0 ? this.toggleSiblingSpanGroup : undefined,
  419. isEmbeddedTransactionTimeAdjusted:
  420. spanModel.isEmbeddedTransactionTimeAdjusted,
  421. };
  422. const bounds = generateBounds({
  423. startTimestamp: spanModel.span.start_timestamp,
  424. endTimestamp: spanModel.span.timestamp,
  425. });
  426. acc.previousSiblingEndTimestamp = spanModel.span.timestamp;
  427. // It's possible that a section in the minimap is selected so some spans in this group may be out of view
  428. bounds.isSpanVisibleInView
  429. ? acc.descendants.push(enhancedSibling)
  430. : acc.descendants.push({
  431. type: 'filtered_out',
  432. span: spanModel.span,
  433. });
  434. }
  435. });
  436. return acc;
  437. }
  438. // Since we are not recursively traversing elements in this group, need to check
  439. // if the spans are filtered or out of bounds here
  440. if (
  441. this.isSpanFilteredOut(props, group[0]) ||
  442. groupShouldBeHidden(group, focusedSpanIds)
  443. ) {
  444. group.forEach(spanModel => {
  445. acc.descendants.push({
  446. type: 'filtered_out',
  447. span: spanModel.span,
  448. });
  449. });
  450. return acc;
  451. }
  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.makeToggleEmbeddedChildren({
  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 && !focusedSpanIds.has(this.span.span_id))
  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. makeToggleEmbeddedChildren = ({
  604. addTraceBounds,
  605. removeTraceBounds,
  606. }: {
  607. addTraceBounds: (bounds: TraceBound) => void;
  608. removeTraceBounds: (eventSlug: string) => void;
  609. }) =>
  610. action('toggleEmbeddedChildren', (orgSlug: string, eventSlugs: string[]) => {
  611. this.showEmbeddedChildren = !this.showEmbeddedChildren;
  612. this.fetchEmbeddedChildrenState = 'idle';
  613. if (!this.showEmbeddedChildren) {
  614. if (this.embeddedChildren.length > 0) {
  615. this.embeddedChildren.forEach(child => {
  616. removeTraceBounds(child.generateTraceBounds().spanId);
  617. });
  618. }
  619. }
  620. if (this.showEmbeddedChildren) {
  621. if (this.embeddedChildren.length === 0) {
  622. return this.fetchEmbeddedTransactions({
  623. orgSlug,
  624. eventSlugs,
  625. addTraceBounds,
  626. });
  627. }
  628. this.embeddedChildren.forEach(child => {
  629. addTraceBounds(child.generateTraceBounds());
  630. });
  631. }
  632. return Promise.resolve(undefined);
  633. });
  634. fetchEmbeddedTransactions({
  635. orgSlug,
  636. eventSlugs,
  637. addTraceBounds,
  638. }: {
  639. addTraceBounds: (bounds: TraceBound) => void;
  640. eventSlugs: string[];
  641. orgSlug: string;
  642. }) {
  643. const urls = eventSlugs.map(
  644. eventSlug => `/organizations/${orgSlug}/events/${eventSlug}/`
  645. );
  646. this.fetchEmbeddedChildrenState = 'loading_embedded_transactions';
  647. const promiseArray = urls.map(url =>
  648. this.api
  649. .requestPromise(url, {
  650. method: 'GET',
  651. query: {},
  652. })
  653. .then(
  654. action('fetchEmbeddedTransactionsSuccess', (event: EventTransaction) => {
  655. if (!event) {
  656. return;
  657. }
  658. const parsedTrace = parseTrace(event);
  659. // We need to adjust the timestamps for this embedded transaction only if it is not within the bounds of its parent span
  660. if (
  661. parsedTrace.traceStartTimestamp < this.span.start_timestamp ||
  662. parsedTrace.traceEndTimestamp > this.span.timestamp
  663. ) {
  664. const responseStart = subTimingMarkToTime(
  665. this.span,
  666. SpanSubTimingMark.HTTP_RESPONSE_START
  667. ); // Response start is a better approximation
  668. const spanTimeOffset = responseStart
  669. ? responseStart - parsedTrace.traceEndTimestamp
  670. : this.span.start_timestamp - parsedTrace.traceStartTimestamp;
  671. parsedTrace.traceStartTimestamp += spanTimeOffset;
  672. parsedTrace.traceEndTimestamp += spanTimeOffset;
  673. parsedTrace.spans.forEach(span => {
  674. span.start_timestamp += spanTimeOffset;
  675. span.timestamp += spanTimeOffset;
  676. });
  677. this.isEmbeddedTransactionTimeAdjusted = true;
  678. }
  679. const rootSpan = generateRootSpan(parsedTrace);
  680. const parsedRootSpan = new SpanTreeModel(
  681. rootSpan,
  682. parsedTrace.childSpans,
  683. this.api,
  684. false
  685. );
  686. this.embeddedChildren.push(parsedRootSpan);
  687. this.fetchEmbeddedChildrenState = 'idle';
  688. addTraceBounds(parsedRootSpan.generateTraceBounds());
  689. })
  690. )
  691. .catch(
  692. action('fetchEmbeddedTransactionsError', () => {
  693. this.embeddedChildren = [];
  694. this.fetchEmbeddedChildrenState = 'error_fetching_embedded_transactions';
  695. })
  696. )
  697. );
  698. return Promise.all(promiseArray);
  699. }
  700. toggleNestedSpanGroup = () => {
  701. this.isNestedSpanGroupExpanded = !this.isNestedSpanGroupExpanded;
  702. };
  703. toggleSiblingSpanGroup = (span: SpanType, occurrence?: number) => {
  704. const key = getSiblingGroupKey(span, occurrence);
  705. if (this.expandedSiblingGroups.has(key)) {
  706. this.expandedSiblingGroups.delete(key);
  707. } else {
  708. this.expandedSiblingGroups.add(key);
  709. }
  710. };
  711. generateTraceBounds = (): TraceBound => {
  712. return {
  713. spanId: this.span.span_id,
  714. traceStartTimestamp: this.span.start_timestamp,
  715. traceEndTimestamp: this.span.timestamp,
  716. };
  717. };
  718. }
  719. export default SpanTreeModel;