waterfallModel.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import isEqual from 'lodash/isEqual';
  2. import pick from 'lodash/pick';
  3. import {action, computed, makeObservable, observable} from 'mobx';
  4. import {Client} from 'sentry/api';
  5. import {EventTransaction} from 'sentry/types/event';
  6. import {createFuzzySearch, Fuse} from 'sentry/utils/fuzzySearch';
  7. import {ActiveOperationFilter, noFilter, toggleAllFilters, toggleFilter} from './filter';
  8. import SpanTreeModel from './spanTreeModel';
  9. import {
  10. FilterSpans,
  11. FocusedSpanIDMap,
  12. IndexedFusedSpan,
  13. ParsedTraceType,
  14. RawSpanType,
  15. TraceBound,
  16. } from './types';
  17. import {boundsGenerator, generateRootSpan, getSpanID, parseTrace} from './utils';
  18. class WaterfallModel {
  19. api: Client = new Client();
  20. // readonly state
  21. event: Readonly<EventTransaction>;
  22. rootSpan: SpanTreeModel;
  23. parsedTrace: ParsedTraceType;
  24. fuse: Fuse<IndexedFusedSpan> | undefined = undefined;
  25. focusedSpanIds?: FocusedSpanIDMap;
  26. // readable/writable state
  27. operationNameFilters: ActiveOperationFilter = noFilter;
  28. filterSpans: FilterSpans | undefined = undefined;
  29. searchQuery: string | undefined = undefined;
  30. hiddenSpanSubTrees: Set<string>;
  31. traceBounds: Array<TraceBound>;
  32. constructor(event: Readonly<EventTransaction>, focusedSpanIds?: FocusedSpanIDMap) {
  33. this.event = event;
  34. this.parsedTrace = parseTrace(event);
  35. const rootSpan = generateRootSpan(this.parsedTrace);
  36. this.rootSpan = new SpanTreeModel(
  37. rootSpan,
  38. this.parsedTrace.childSpans,
  39. this.api,
  40. true
  41. );
  42. // Track the trace bounds of the current transaction and the trace bounds of
  43. // any embedded transactions
  44. this.traceBounds = [this.rootSpan.generateTraceBounds()];
  45. this.indexSearch(this.parsedTrace, rootSpan);
  46. // Set of span IDs whose sub-trees should be hidden. This is used for the
  47. // span tree toggling product feature.
  48. this.hiddenSpanSubTrees = new Set();
  49. // When viewing the span waterfall from a Performance Issue, a set of span IDs may be provided
  50. this.focusedSpanIds = focusedSpanIds;
  51. makeObservable(this, {
  52. parsedTrace: observable,
  53. rootSpan: observable,
  54. // operation names filtering
  55. operationNameFilters: observable,
  56. toggleOperationNameFilter: action,
  57. toggleAllOperationNameFilters: action,
  58. operationNameCounts: computed.struct,
  59. // span search
  60. filterSpans: observable,
  61. searchQuery: observable,
  62. querySpanSearch: action,
  63. // span sub-tree toggling
  64. hiddenSpanSubTrees: observable,
  65. toggleSpanSubTree: action,
  66. // trace bounds
  67. traceBounds: observable,
  68. addTraceBounds: action,
  69. removeTraceBounds: action,
  70. });
  71. }
  72. isEvent(otherEvent: Readonly<EventTransaction>) {
  73. return isEqual(this.event, otherEvent);
  74. }
  75. toggleOperationNameFilter = (operationName: string) => {
  76. this.operationNameFilters = toggleFilter(this.operationNameFilters, operationName);
  77. };
  78. toggleAllOperationNameFilters = () => {
  79. const operationNames = Array.from(this.operationNameCounts.keys());
  80. this.operationNameFilters = toggleAllFilters(
  81. this.operationNameFilters,
  82. operationNames
  83. );
  84. };
  85. get operationNameCounts(): Map<string, number> {
  86. return this.rootSpan.operationNameCounts;
  87. }
  88. async indexSearch(parsedTrace: ParsedTraceType, rootSpan: RawSpanType) {
  89. this.filterSpans = undefined;
  90. this.searchQuery = undefined;
  91. const {spans} = parsedTrace;
  92. const transformed: IndexedFusedSpan[] = [rootSpan, ...spans].map(
  93. (span): IndexedFusedSpan => {
  94. const indexed: string[] = [];
  95. // basic properties
  96. const pickedSpan = pick(span, [
  97. // TODO: do we want this?
  98. // 'trace_id',
  99. 'span_id',
  100. 'start_timestamp',
  101. 'timestamp',
  102. 'op',
  103. 'description',
  104. ]);
  105. const basicValues: string[] = Object.values(pickedSpan)
  106. .filter(value => !!value)
  107. .map(value => String(value));
  108. indexed.push(...basicValues);
  109. // tags
  110. let tagKeys: string[] = [];
  111. let tagValues: string[] = [];
  112. const tags: {[tag_name: string]: string} | undefined = span?.tags;
  113. if (tags) {
  114. tagKeys = Object.keys(tags);
  115. tagValues = Object.values(tags);
  116. }
  117. const data: {[data_name: string]: any} | undefined = span?.data ?? {};
  118. let dataKeys: string[] = [];
  119. let dataValues: string[] = [];
  120. if (data) {
  121. dataKeys = Object.keys(data);
  122. dataValues = Object.values(data).map(
  123. value => JSON.stringify(value, null, 4) || ''
  124. );
  125. }
  126. return {
  127. span,
  128. indexed,
  129. tagKeys,
  130. tagValues,
  131. dataKeys,
  132. dataValues,
  133. };
  134. }
  135. );
  136. this.fuse = await createFuzzySearch(transformed, {
  137. keys: ['indexed', 'tagKeys', 'tagValues', 'dataKeys', 'dataValues'],
  138. includeMatches: false,
  139. threshold: 0.6,
  140. location: 0,
  141. distance: 100,
  142. maxPatternLength: 32,
  143. });
  144. }
  145. querySpanSearch(searchQuery: string | undefined) {
  146. if (!searchQuery) {
  147. // reset
  148. if (this.filterSpans !== undefined) {
  149. this.filterSpans = undefined;
  150. this.searchQuery = undefined;
  151. }
  152. return;
  153. }
  154. if (!this.fuse) {
  155. return;
  156. }
  157. const results = this.fuse.search(searchQuery);
  158. const spanIDs: Set<string> = results.reduce((setOfSpanIDs: Set<string>, result) => {
  159. const spanID = getSpanID(result.item.span);
  160. if (spanID) {
  161. setOfSpanIDs.add(spanID);
  162. }
  163. return setOfSpanIDs;
  164. }, new Set<string>());
  165. this.searchQuery = searchQuery;
  166. this.filterSpans = {spanIDs, results};
  167. }
  168. toggleSpanSubTree = (spanID: string) => {
  169. if (this.hiddenSpanSubTrees.has(spanID)) {
  170. this.hiddenSpanSubTrees.delete(spanID);
  171. return;
  172. }
  173. this.hiddenSpanSubTrees.add(spanID);
  174. };
  175. addTraceBounds = (traceBound: TraceBound) => {
  176. this.traceBounds.push(traceBound);
  177. this.parsedTrace = {
  178. ...this.parsedTrace,
  179. ...this.getTraceBounds(),
  180. };
  181. };
  182. removeTraceBounds = (spanId: string) => {
  183. this.traceBounds = this.traceBounds.filter(bound => bound.spanId !== spanId);
  184. // traceBounds must always be non-empty
  185. if (this.traceBounds.length === 0) {
  186. this.traceBounds = [this.rootSpan.generateTraceBounds()];
  187. }
  188. this.parsedTrace = {
  189. ...this.parsedTrace,
  190. ...this.getTraceBounds(),
  191. };
  192. };
  193. getTraceBounds = () => {
  194. // traceBounds must always be non-empty
  195. if (this.traceBounds.length === 0) {
  196. this.traceBounds = [this.rootSpan.generateTraceBounds()];
  197. }
  198. return this.traceBounds.reduce(
  199. (acc, bounds) => {
  200. return {
  201. traceStartTimestamp: Math.min(
  202. acc.traceStartTimestamp,
  203. bounds.traceStartTimestamp
  204. ),
  205. traceEndTimestamp: Math.max(acc.traceEndTimestamp, bounds.traceEndTimestamp),
  206. };
  207. },
  208. {
  209. traceStartTimestamp: this.traceBounds[0].traceStartTimestamp,
  210. traceEndTimestamp: this.traceBounds[0].traceEndTimestamp,
  211. }
  212. );
  213. };
  214. generateBounds = ({
  215. viewStart,
  216. viewEnd,
  217. }: {
  218. // in [0, 1]
  219. viewEnd: number;
  220. viewStart: number; // in [0, 1]
  221. }) => {
  222. return boundsGenerator({
  223. ...this.getTraceBounds(),
  224. viewStart,
  225. viewEnd,
  226. });
  227. };
  228. getWaterfall = ({
  229. viewStart,
  230. viewEnd,
  231. }: {
  232. // in [0, 1]
  233. viewEnd: number;
  234. viewStart: number; // in [0, 1]
  235. }) => {
  236. const generateBounds = this.generateBounds({
  237. viewStart,
  238. viewEnd,
  239. });
  240. return this.rootSpan.getSpansList({
  241. operationNameFilters: this.operationNameFilters,
  242. generateBounds,
  243. treeDepth: 0,
  244. directParent: null,
  245. isLastSibling: true,
  246. continuingTreeDepths: [],
  247. hiddenSpanSubTrees: this.hiddenSpanSubTrees,
  248. spanAncestors: new Set(),
  249. filterSpans: this.filterSpans,
  250. focusedSpanIds: this.focusedSpanIds,
  251. previousSiblingEndTimestamp: undefined,
  252. event: this.event,
  253. isOnlySibling: true,
  254. spanNestedGrouping: undefined,
  255. toggleNestedSpanGroup: undefined,
  256. isNestedSpanGroupExpanded: false,
  257. addTraceBounds: this.addTraceBounds,
  258. removeTraceBounds: this.removeTraceBounds,
  259. });
  260. };
  261. }
  262. export default WaterfallModel;