123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- import isEqual from 'lodash/isEqual';
- import pick from 'lodash/pick';
- import {action, computed, makeObservable, observable} from 'mobx';
- import {Client} from 'sentry/api';
- import {EventTransaction} from 'sentry/types/event';
- import {createFuzzySearch, Fuse} from 'sentry/utils/fuzzySearch';
- import {ActiveOperationFilter, noFilter, toggleAllFilters, toggleFilter} from './filter';
- import SpanTreeModel from './spanTreeModel';
- import {
- FilterSpans,
- FocusedSpanIDMap,
- IndexedFusedSpan,
- ParsedTraceType,
- RawSpanType,
- TraceBound,
- } from './types';
- import {boundsGenerator, generateRootSpan, getSpanID, parseTrace} from './utils';
- class WaterfallModel {
- api: Client = new Client();
- // readonly state
- event: Readonly<EventTransaction>;
- rootSpan: SpanTreeModel;
- parsedTrace: ParsedTraceType;
- fuse: Fuse<IndexedFusedSpan> | undefined = undefined;
- focusedSpanIds?: FocusedSpanIDMap;
- // readable/writable state
- operationNameFilters: ActiveOperationFilter = noFilter;
- filterSpans: FilterSpans | undefined = undefined;
- searchQuery: string | undefined = undefined;
- hiddenSpanSubTrees: Set<string>;
- traceBounds: Array<TraceBound>;
- constructor(event: Readonly<EventTransaction>, focusedSpanIds?: FocusedSpanIDMap) {
- this.event = event;
- this.parsedTrace = parseTrace(event);
- const rootSpan = generateRootSpan(this.parsedTrace);
- this.rootSpan = new SpanTreeModel(
- rootSpan,
- this.parsedTrace.childSpans,
- this.api,
- true
- );
- // Track the trace bounds of the current transaction and the trace bounds of
- // any embedded transactions
- this.traceBounds = [this.rootSpan.generateTraceBounds()];
- this.indexSearch(this.parsedTrace, rootSpan);
- // Set of span IDs whose sub-trees should be hidden. This is used for the
- // span tree toggling product feature.
- this.hiddenSpanSubTrees = new Set();
- // When viewing the span waterfall from a Performance Issue, a set of span IDs may be provided
- this.focusedSpanIds = focusedSpanIds;
- makeObservable(this, {
- parsedTrace: observable,
- rootSpan: observable,
- // operation names filtering
- operationNameFilters: observable,
- toggleOperationNameFilter: action,
- toggleAllOperationNameFilters: action,
- operationNameCounts: computed.struct,
- // span search
- filterSpans: observable,
- searchQuery: observable,
- querySpanSearch: action,
- // span sub-tree toggling
- hiddenSpanSubTrees: observable,
- toggleSpanSubTree: action,
- // trace bounds
- traceBounds: observable,
- addTraceBounds: action,
- removeTraceBounds: action,
- });
- }
- isEvent(otherEvent: Readonly<EventTransaction>) {
- return isEqual(this.event, otherEvent);
- }
- toggleOperationNameFilter = (operationName: string) => {
- this.operationNameFilters = toggleFilter(this.operationNameFilters, operationName);
- };
- toggleAllOperationNameFilters = () => {
- const operationNames = Array.from(this.operationNameCounts.keys());
- this.operationNameFilters = toggleAllFilters(
- this.operationNameFilters,
- operationNames
- );
- };
- get operationNameCounts(): Map<string, number> {
- return this.rootSpan.operationNameCounts;
- }
- async indexSearch(parsedTrace: ParsedTraceType, rootSpan: RawSpanType) {
- this.filterSpans = undefined;
- this.searchQuery = undefined;
- const {spans} = parsedTrace;
- const transformed: IndexedFusedSpan[] = [rootSpan, ...spans].map(
- (span): IndexedFusedSpan => {
- const indexed: string[] = [];
- // basic properties
- const pickedSpan = pick(span, [
- // TODO: do we want this?
- // 'trace_id',
- 'span_id',
- 'start_timestamp',
- 'timestamp',
- 'op',
- 'description',
- ]);
- const basicValues: string[] = Object.values(pickedSpan)
- .filter(value => !!value)
- .map(value => String(value));
- indexed.push(...basicValues);
- // tags
- let tagKeys: string[] = [];
- let tagValues: string[] = [];
- const tags: {[tag_name: string]: string} | undefined = span?.tags;
- if (tags) {
- tagKeys = Object.keys(tags);
- tagValues = Object.values(tags);
- }
- const data: {[data_name: string]: any} | undefined = span?.data ?? {};
- let dataKeys: string[] = [];
- let dataValues: string[] = [];
- if (data) {
- dataKeys = Object.keys(data);
- dataValues = Object.values(data).map(
- value => JSON.stringify(value, null, 4) || ''
- );
- }
- return {
- span,
- indexed,
- tagKeys,
- tagValues,
- dataKeys,
- dataValues,
- };
- }
- );
- this.fuse = await createFuzzySearch(transformed, {
- keys: ['indexed', 'tagKeys', 'tagValues', 'dataKeys', 'dataValues'],
- includeMatches: false,
- threshold: 0.6,
- location: 0,
- distance: 100,
- maxPatternLength: 32,
- });
- }
- querySpanSearch(searchQuery: string | undefined) {
- if (!searchQuery) {
- // reset
- if (this.filterSpans !== undefined) {
- this.filterSpans = undefined;
- this.searchQuery = undefined;
- }
- return;
- }
- if (!this.fuse) {
- return;
- }
- const results = this.fuse.search(searchQuery);
- const spanIDs: Set<string> = results.reduce((setOfSpanIDs: Set<string>, result) => {
- const spanID = getSpanID(result.item.span);
- if (spanID) {
- setOfSpanIDs.add(spanID);
- }
- return setOfSpanIDs;
- }, new Set<string>());
- this.searchQuery = searchQuery;
- this.filterSpans = {spanIDs, results};
- }
- toggleSpanSubTree = (spanID: string) => {
- if (this.hiddenSpanSubTrees.has(spanID)) {
- this.hiddenSpanSubTrees.delete(spanID);
- return;
- }
- this.hiddenSpanSubTrees.add(spanID);
- };
- addTraceBounds = (traceBound: TraceBound) => {
- this.traceBounds.push(traceBound);
- this.parsedTrace = {
- ...this.parsedTrace,
- ...this.getTraceBounds(),
- };
- };
- removeTraceBounds = (spanId: string) => {
- this.traceBounds = this.traceBounds.filter(bound => bound.spanId !== spanId);
- // traceBounds must always be non-empty
- if (this.traceBounds.length === 0) {
- this.traceBounds = [this.rootSpan.generateTraceBounds()];
- }
- this.parsedTrace = {
- ...this.parsedTrace,
- ...this.getTraceBounds(),
- };
- };
- getTraceBounds = () => {
- // traceBounds must always be non-empty
- if (this.traceBounds.length === 0) {
- this.traceBounds = [this.rootSpan.generateTraceBounds()];
- }
- return this.traceBounds.reduce(
- (acc, bounds) => {
- return {
- traceStartTimestamp: Math.min(
- acc.traceStartTimestamp,
- bounds.traceStartTimestamp
- ),
- traceEndTimestamp: Math.max(acc.traceEndTimestamp, bounds.traceEndTimestamp),
- };
- },
- {
- traceStartTimestamp: this.traceBounds[0].traceStartTimestamp,
- traceEndTimestamp: this.traceBounds[0].traceEndTimestamp,
- }
- );
- };
- generateBounds = ({
- viewStart,
- viewEnd,
- }: {
- // in [0, 1]
- viewEnd: number;
- viewStart: number; // in [0, 1]
- }) => {
- return boundsGenerator({
- ...this.getTraceBounds(),
- viewStart,
- viewEnd,
- });
- };
- getWaterfall = ({
- viewStart,
- viewEnd,
- }: {
- // in [0, 1]
- viewEnd: number;
- viewStart: number; // in [0, 1]
- }) => {
- const generateBounds = this.generateBounds({
- viewStart,
- viewEnd,
- });
- return this.rootSpan.getSpansList({
- operationNameFilters: this.operationNameFilters,
- generateBounds,
- treeDepth: 0,
- directParent: null,
- isLastSibling: true,
- continuingTreeDepths: [],
- hiddenSpanSubTrees: this.hiddenSpanSubTrees,
- spanAncestors: new Set(),
- filterSpans: this.filterSpans,
- focusedSpanIds: this.focusedSpanIds,
- previousSiblingEndTimestamp: undefined,
- event: this.event,
- isOnlySibling: true,
- spanNestedGrouping: undefined,
- toggleNestedSpanGroup: undefined,
- isNestedSpanGroupExpanded: false,
- addTraceBounds: this.addTraceBounds,
- removeTraceBounds: this.removeTraceBounds,
- });
- };
- }
- export default WaterfallModel;
|