Files
2024-02-15 17:01:50 +00:00

361 lines
9.6 KiB
TypeScript

import isEqual from 'lodash/isEqual';
import pick from 'lodash/pick';
import {action, computed, makeObservable, observable} from 'mobx';
import {Client} from 'sentry/api';
import type {AggregateEventTransaction, EventTransaction} from 'sentry/types/event';
import type {Fuse} from 'sentry/utils/fuzzySearch';
import {createFuzzySearch} from 'sentry/utils/fuzzySearch';
import type {TraceInfo} from 'sentry/views/performance/traceDetails/types';
import type {ActiveOperationFilter} from './filter';
import {noFilter, toggleAllFilters, toggleFilter} from './filter';
import SpanTreeModel from './spanTreeModel';
import type {
EnhancedProcessedSpanType,
FilterSpans,
IndexedFusedSpan,
ParsedTraceType,
RawSpanType,
TraceBound,
} from './types';
import {boundsGenerator, generateRootSpan, getSpanID, parseTrace} from './utils';
class WaterfallModel {
api: Client = new Client();
// readonly state
event: Readonly<EventTransaction | AggregateEventTransaction>;
rootSpan: SpanTreeModel;
parsedTrace: ParsedTraceType;
fuse: Fuse<IndexedFusedSpan> | undefined = undefined;
affectedSpanIds: string[] | undefined = undefined;
isEmbeddedSpanTree: boolean;
// readable/writable state
operationNameFilters: ActiveOperationFilter = noFilter;
filterSpans: FilterSpans | undefined = undefined;
searchQuery: string | undefined = undefined;
hiddenSpanSubTrees: Set<string>;
traceBounds: Array<TraceBound>;
focusedSpanIds: Set<string> | undefined = undefined;
traceInfo: TraceInfo | undefined = undefined;
constructor(
event: Readonly<EventTransaction | AggregateEventTransaction>,
affectedSpanIds?: string[],
focusedSpanIds?: string[],
hiddenSpanSubTrees?: Set<string>,
traceInfo?: TraceInfo
) {
this.event = event;
this.traceInfo = traceInfo;
this.parsedTrace = parseTrace(event);
const rootSpan = generateRootSpan(this.parsedTrace);
this.rootSpan = new SpanTreeModel(
rootSpan,
this.parsedTrace.childSpans,
this.api,
true,
traceInfo
);
// 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 = hiddenSpanSubTrees ?? new Set();
// When viewing the span waterfall from a Performance Issue, a set of span IDs may be provided
this.affectedSpanIds = affectedSpanIds;
if (affectedSpanIds || focusedSpanIds) {
affectedSpanIds ??= [];
focusedSpanIds ??= [];
this.focusedSpanIds = new Set([...affectedSpanIds, ...focusedSpanIds]);
}
// If the set of span IDs is provided, this waterfall is for an embedded span tree
this.isEmbeddedSpanTree = !!this.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,
focusedSpanIds: observable,
expandHiddenSpans: 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]
}) => {
const bounds = this.traceInfo
? {
traceEndTimestamp: this.traceInfo.endTimestamp,
traceStartTimestamp: this.traceInfo.startTimestamp,
}
: this.getTraceBounds();
return boundsGenerator({
...bounds,
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,
});
};
expandHiddenSpans = (spans: EnhancedProcessedSpanType[]) => {
spans.forEach(span => {
if (span.type !== 'filtered_out') {
return;
}
this.focusedSpanIds?.add(span.span.span_id);
});
};
}
export default WaterfallModel;