mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
179 lines
4.9 KiB
TypeScript
179 lines
4.9 KiB
TypeScript
import {uuid4} from '@sentry/utils';
|
|
|
|
import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
|
|
import {t} from 'sentry/locale';
|
|
import type {EventTransaction} from 'sentry/types';
|
|
import {EventOrGroupType} from 'sentry/types';
|
|
|
|
// Empty transaction to use as a default value with duration of 1 second
|
|
const EmptyEventTransaction: EventTransaction = {
|
|
id: '',
|
|
projectID: '',
|
|
user: {},
|
|
contexts: {},
|
|
entries: [],
|
|
errors: [],
|
|
dateCreated: '',
|
|
startTimestamp: Date.now(),
|
|
endTimestamp: Date.now() + 1000,
|
|
title: '',
|
|
type: EventOrGroupType.TRANSACTION,
|
|
culprit: '',
|
|
dist: null,
|
|
eventID: '',
|
|
fingerprints: [],
|
|
dateReceived: new Date().toISOString(),
|
|
message: '',
|
|
metadata: {},
|
|
size: 0,
|
|
tags: [],
|
|
occurrence: null,
|
|
location: '',
|
|
crashFile: null,
|
|
};
|
|
|
|
function sortByStartTimeAndDuration(a: RawSpanType, b: RawSpanType) {
|
|
return a.start_timestamp - b.start_timestamp;
|
|
}
|
|
|
|
export class SpanTreeNode {
|
|
parent?: SpanTreeNode | null = null;
|
|
span: RawSpanType;
|
|
children: SpanTreeNode[] = [];
|
|
|
|
constructor(span: RawSpanType, parent?: SpanTreeNode | null) {
|
|
this.span = span;
|
|
this.parent = parent;
|
|
}
|
|
|
|
static Root(partial: Partial<RawSpanType> = {}): SpanTreeNode {
|
|
return new SpanTreeNode(
|
|
{
|
|
description: 'root',
|
|
op: 'root',
|
|
start_timestamp: 0,
|
|
exclusive_time: 0,
|
|
timestamp: Number.MAX_SAFE_INTEGER,
|
|
parent_span_id: '',
|
|
data: {},
|
|
span_id: '<root>',
|
|
trace_id: '',
|
|
hash: '',
|
|
...partial,
|
|
},
|
|
null
|
|
);
|
|
}
|
|
|
|
contains(span: RawSpanType) {
|
|
return (
|
|
this.span.start_timestamp <= span.start_timestamp &&
|
|
this.span.timestamp >= span.timestamp
|
|
);
|
|
}
|
|
}
|
|
|
|
class SpanTree {
|
|
root: SpanTreeNode;
|
|
orphanedSpans: RawSpanType[] = [];
|
|
transaction: EventTransaction;
|
|
|
|
constructor(transaction: EventTransaction, spans: RawSpanType[]) {
|
|
this.transaction = transaction;
|
|
|
|
this.root = SpanTreeNode.Root({
|
|
description: transaction.title,
|
|
start_timestamp: transaction.startTimestamp,
|
|
timestamp: transaction.endTimestamp,
|
|
exclusive_time: transaction.contexts?.trace?.exclusive_time ?? undefined,
|
|
span_id: transaction.contexts?.trace?.span_id ?? undefined,
|
|
parent_span_id: undefined,
|
|
op: 'transaction',
|
|
});
|
|
|
|
this.buildCollapsedSpanTree(spans);
|
|
}
|
|
|
|
static Empty = new SpanTree(EmptyEventTransaction, []);
|
|
|
|
isEmpty(): boolean {
|
|
return this === SpanTree.Empty;
|
|
}
|
|
|
|
buildCollapsedSpanTree(spans: RawSpanType[]) {
|
|
const spansSortedByStartTime = [...spans].sort(sortByStartTimeAndDuration);
|
|
const MISSING_INSTRUMENTATION_THRESHOLD_S = 0.1;
|
|
|
|
for (let i = 0; i < spansSortedByStartTime.length; i++) {
|
|
const span = spansSortedByStartTime[i];
|
|
let parent = this.root;
|
|
|
|
while (parent.contains(span)) {
|
|
let nextParent: SpanTreeNode | null = null;
|
|
for (let j = 0; j < parent.children.length; j++) {
|
|
const child = parent.children[j];
|
|
if (child.span.op !== 'missing instrumentation' && child.contains(span)) {
|
|
nextParent = child;
|
|
break;
|
|
}
|
|
}
|
|
if (nextParent === null) {
|
|
break;
|
|
}
|
|
parent = nextParent;
|
|
}
|
|
|
|
if (parent.span.span_id === span.parent_span_id) {
|
|
// If the missing instrumentation threshold is exceeded, add a span to
|
|
// indicate that there is a gap in instrumentation. We can rely on this check
|
|
// because the spans are sorted by start time, so we know that we will not be
|
|
// updating anything before span.start_timestamp.
|
|
if (
|
|
parent.children.length > 0 &&
|
|
span.start_timestamp -
|
|
parent.children[parent.children.length - 1].span.timestamp >
|
|
MISSING_INSTRUMENTATION_THRESHOLD_S
|
|
) {
|
|
parent.children.push(
|
|
new SpanTreeNode(
|
|
{
|
|
description: t('Missing span instrumentation'),
|
|
op: 'missing span instrumentation',
|
|
start_timestamp:
|
|
parent.children[parent.children.length - 1].span.timestamp,
|
|
timestamp: span.start_timestamp,
|
|
span_id: uuid4(),
|
|
data: {},
|
|
trace_id: span.trace_id,
|
|
},
|
|
parent
|
|
)
|
|
);
|
|
}
|
|
|
|
let foundOverlap = false;
|
|
let start = parent.children.length - 1;
|
|
while (start >= 0) {
|
|
const child = parent.children[start];
|
|
if (span.start_timestamp < child.span.timestamp) {
|
|
foundOverlap = true;
|
|
break;
|
|
}
|
|
start--;
|
|
}
|
|
if (foundOverlap) {
|
|
this.orphanedSpans.push(span);
|
|
continue;
|
|
}
|
|
// Insert child span
|
|
parent.children.push(new SpanTreeNode(span, parent));
|
|
continue;
|
|
}
|
|
|
|
this.orphanedSpans.push(span);
|
|
}
|
|
}
|
|
}
|
|
|
|
export {SpanTree};
|