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

1093 lines
32 KiB
TypeScript

import type {Client} from 'sentry/api';
import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
import type {Organization} from 'sentry/types';
import type {Event, EventTransaction} from 'sentry/types/event';
import type {
TraceError as TraceErrorType,
TraceFullDetailed,
TraceSplitResults,
} from 'sentry/utils/performance/quickTrace/types';
import {TraceType} from '../traceDetails/newTraceDetailsContent';
import {isRootTransaction} from '../traceDetails/utils';
import {
isAutogroupedNode,
isMissingInstrumentationNode,
isParentAutogroupedNode,
isRootNode,
isSiblingAutogroupedNode,
isSpanNode,
isTraceNode,
isTransactionNode,
} from './guards';
/**
*
* This file implements the tree data structure that is used to represent a trace. We do
* this both for performance reasons as well as flexibility. The requirement for a tree
* is to support incremental patching and updates. This is important because we want to
* be able to fetch more data as the user interacts with the tree, and we want to be able
* efficiently update the tree as we receive more data.
*
* The trace is represented as a tree with different node value types (transaction or span)
* Each tree node contains a reference to its parent and a list of references to its children,
* as well as a reference to the value that the node holds. Each node also contains
* some meta data and state about the node, such as if it is expanded or zoomed in. The benefit
* of abstracting parts of the UI state is that the tree will persist user actions such as expanding
* or collapsing nodes which would have otherwise been lost when individual nodes are remounted in the tree.
*
* Each tree holds a list reference, which is a live reference to a flattened representation
* of the tree (used to render the tree in the UI). Since the list is mutable (and we want to keep it that way for performance
* reasons as we want to support mutations on traces with ~100k+ nodes), callers need to manage reactivity themselves.
*
* An alternative, but not recommended approach is to call build() on the tree after each mutation,
* which will iterate over all of the children and build a fresh list reference.
*
* In most cases, the initial tree is a list of transactions containing other transactions. Each transaction can
* then be expanded into a list of spans which can also in some cases be expanded.
*
* - trace - trace
* |- parent transaction --> when expanding |- parent transaction
* |- child transaction |- span
* |- span this used to be a transaction,
* |- child transaction span <- but is now be a list of spans
* |- span belonging to the transaction
* this results in child txns to be lost,
* which is a confusing user experience
*
* The tree supports autogrouping of spans vertically or as siblings. When that happens, a autogrouped node of either a vertical or
* sibling type is inserted as an intermediary node. In the vertical case, the autogrouped node
* holds the reference to the head and tail of the autogrouped sequence. In the sibling case, the autogrouped node
* holds a reference to the children that are part of the autogrouped sequence. When expanding and collapsing these nodes,
* the tree perform a reference swap to either point to the head (when expanded) or tail (when collapsed) of the autogrouped sequence.
*
* In vertical grouping case, the following happens:
*
* - root - root
* - trace - trace
* |- transaction |- transaction
* |- span 1 <-| these become autogrouped |- autogrouped (head=span1, tail=span3, children points to children of tail)
* |- span 2 |- as they are inserted into |- other span (parent points to autogrouped node)
* |- span 3 <-| the tree.
* |- other span
*
* When the autogrouped node is expanded the UI needs to show the entire collapsed chain, so we swap the tail children to point
* back to the tail, and have autogrouped node point to it's head as the children.
*
* - root - root
* - trace - trace
* |- transaction |- transaction
* |- autogrouped (head=span1, tail=span3) <- when expanding |- autogrouped (head=span1, tail=span3, children points to head)
* | other span (paren points to autogrouped) |- span 1 (head)
* |- span 2
* |- span 3 (tail)
* |- other span (children of tail, parent points to tail)
*
* Notes and improvements:
* - collecting children should be O(n), it is currently O(n^2) as we are missing a proper queue implementation
* - the notion of expanded and zoomed is confusing, they stand for the same idea from a UI pov
* - there is an annoying thing wrt span and transaction nodes where we either store data on _children or _spanChildren
* this is because we want to be able to store both transaction and span nodes in the same tree, but it makes for an
* annoying API. A better design would have been to create an invisible meta node that just points to the correct children
* - connector generation should live in the UI layer, not in the tree. Same with depth calculation. It is more convenient
* to calculate this when rendering the tree, as we can only calculate it only for the visible nodes and avoid an extra tree pass
* - instead of storing span children separately, we should have meta tree nodes that handle pointing to the correct children
*/
export declare namespace TraceTree {
type Transaction = TraceFullDetailed;
type Span = RawSpanType;
type Trace = TraceSplitResults<Transaction>;
type TraceError = TraceErrorType;
interface MissingInstrumentationSpan {
start_timestamp: number;
timestamp: number;
type: 'missing_instrumentation';
}
interface SiblingAutogroup extends RawSpanType {
autogrouped_by: {
description: string;
op: string;
};
}
interface ChildrenAutogroup {
autogrouped_by: {
op: string;
};
}
type NodeValue =
| Trace
| Transaction
| TraceError
| Span
| MissingInstrumentationSpan
| SiblingAutogroup
| ChildrenAutogroup
| null;
type Metadata = {
event_id: string | undefined;
project_slug: string | undefined;
};
}
function fetchTransactionSpans(
api: Client,
organization: Organization,
project_slug: string,
event_id: string
): Promise<EventTransaction> {
return api.requestPromise(
`/organizations/${organization.slug}/events/${project_slug}:${event_id}/`
);
}
function maybeInsertMissingInstrumentationSpan(
parent: TraceTreeNode<TraceTree.NodeValue>,
node: TraceTreeNode<TraceTree.Span>
) {
const lastInsertedSpan = parent.spanChildren[parent.spanChildren.length - 1];
if (!lastInsertedSpan) {
return;
}
if (node.value.start_timestamp - lastInsertedSpan.value.timestamp < 0.1) {
return;
}
const missingInstrumentationSpan =
new TraceTreeNode<TraceTree.MissingInstrumentationSpan>(
parent,
{
type: 'missing_instrumentation',
start_timestamp: lastInsertedSpan.value.timestamp,
timestamp: node.value.start_timestamp,
},
{
event_id: undefined,
project_slug: undefined,
}
);
parent.spanChildren.push(missingInstrumentationSpan);
}
export class TraceTree {
type: 'loading' | 'trace' = 'trace';
root: TraceTreeNode<null> = TraceTreeNode.Root();
private _spanPromises: Map<TraceTreeNode<TraceTree.NodeValue>, Promise<Event>> =
new Map();
private _list: TraceTreeNode<TraceTree.NodeValue>[] = [];
static Empty() {
const tree = new TraceTree().build();
tree.type = 'trace';
return tree;
}
static Loading(metadata: TraceTree.Metadata): TraceTree {
const tree = makeExampleTrace(metadata);
tree.type = 'loading';
return tree;
}
static FromTrace(trace: TraceTree.Trace): TraceTree {
const tree = new TraceTree();
function visit(
parent: TraceTreeNode<TraceTree.NodeValue | null>,
value: TraceTree.NodeValue
) {
const node = new TraceTreeNode(parent, value, {
project_slug: value && 'project_slug' in value ? value.project_slug : undefined,
event_id: value && 'event_id' in value ? value.event_id : undefined,
});
node.canFetchData = true;
if (parent) {
parent.children.push(node as TraceTreeNode<TraceTree.NodeValue>);
}
if (value && 'children' in value) {
for (const child of value.children) {
visit(node, child);
}
}
return node;
}
const traceNode = new TraceTreeNode(tree.root, trace, {
event_id: undefined,
project_slug: undefined,
});
// Trace is always expanded by default
tree.root.children.push(traceNode);
let traceStart = Number.POSITIVE_INFINITY;
let traceEnd = Number.NEGATIVE_INFINITY;
for (const transaction of trace.transactions) {
if (transaction.start_timestamp < traceStart) {
traceStart = transaction.start_timestamp;
}
if (transaction.timestamp > traceEnd) {
traceEnd = transaction.timestamp;
}
visit(traceNode, transaction);
}
for (const trace_error of trace.orphan_errors) {
visit(traceNode, trace_error);
}
traceNode.space = [traceStart, traceEnd - traceStart];
tree.root.space = [traceStart, traceEnd - traceStart];
return tree.build();
}
static GetTraceType(root: TraceTreeNode<null>): TraceType {
const trace = root.children[0];
if (!trace || !isTraceNode(trace)) {
throw new TypeError('Not trace node');
}
const {transactions, orphan_errors} = trace.value;
const {roots, orphans} = (transactions ?? []).reduce(
(counts, transaction) => {
if (isRootTransaction(transaction)) {
counts.roots++;
} else {
counts.orphans++;
}
return counts;
},
{roots: 0, orphans: 0}
);
if (roots === 0) {
if (orphans > 0) {
return TraceType.NO_ROOT;
}
if (orphan_errors && orphan_errors.length > 0) {
return TraceType.ONLY_ERRORS;
}
return TraceType.EMPTY_TRACE;
}
if (roots === 1) {
if (orphans > 0) {
return TraceType.BROKEN_SUBTRACES;
}
return TraceType.ONE_ROOT;
}
if (roots > 1) {
return TraceType.MULTIPLE_ROOTS;
}
throw new Error('Unknown trace type');
}
static FromSpans(
parent: TraceTreeNode<TraceTree.NodeValue>,
spans: RawSpanType[]
): TraceTreeNode<TraceTree.NodeValue> {
const parentIsSpan = isSpanNode(parent);
const lookuptable: Record<
RawSpanType['span_id'],
TraceTreeNode<TraceTree.Span | TraceTree.Transaction>
> = {};
if (parent.spanChildren.length > 0) {
parent.zoomedIn = true;
return parent;
}
if (parentIsSpan) {
if (parent.value && 'span_id' in parent.value) {
lookuptable[parent.value.span_id] = parent as TraceTreeNode<TraceTree.Span>;
}
}
const transactionsToSpanMap = new Map<string, TraceTreeNode<TraceTree.Transaction>>();
for (const child of parent.children) {
if (
isTransactionNode(child) &&
'parent_span_id' in child.value &&
typeof child.value.parent_span_id === 'string'
) {
transactionsToSpanMap.set(child.value.parent_span_id, child);
}
continue;
}
for (const span of spans) {
const parentNode = transactionsToSpanMap.get(span.span_id);
let node: TraceTreeNode<TraceTree.Span>;
if (parentNode) {
node = parentNode.clone() as unknown as TraceTreeNode<TraceTree.Span>;
} else {
node = new TraceTreeNode(null, span, {
event_id: undefined,
project_slug: undefined,
});
}
node.canFetchData = !!parentNode;
if (parentNode) {
node.metadata = parentNode.metadata;
}
lookuptable[span.span_id] = node;
if (span.parent_span_id) {
const spanParentNode = lookuptable[span.parent_span_id];
if (spanParentNode) {
node.parent = spanParentNode;
maybeInsertMissingInstrumentationSpan(spanParentNode, node);
spanParentNode.spanChildren.push(node);
continue;
}
}
// Orphaned span
maybeInsertMissingInstrumentationSpan(parent, node);
parent.spanChildren.push(node);
node.parent = parent as TraceTreeNode<TraceTree.Span>;
}
parent.zoomedIn = true;
TraceTree.AutogroupSiblingSpanNodes(parent);
TraceTree.AutogroupDirectChildrenSpanNodes(parent);
return parent;
}
get list(): ReadonlyArray<TraceTreeNode<TraceTree.NodeValue>> {
return this._list;
}
static AutogroupDirectChildrenSpanNodes(
root: TraceTreeNode<TraceTree.NodeValue>
): void {
const queue = [root];
while (queue.length > 0) {
const node = queue.pop()!;
if (node.children.length > 1 || !isSpanNode(node)) {
for (const child of node.children) {
queue.push(child);
}
continue;
}
const head = node;
let tail = node;
let groupMatchCount = 0;
while (
tail &&
tail.children.length === 1 &&
isSpanNode(tail.children[0]) &&
tail.children[0].value.op === head.value.op
) {
groupMatchCount++;
tail = tail.children[0];
}
if (groupMatchCount < 1) {
for (const child of head.children) {
queue.push(child);
}
continue;
}
const autoGroupedNode = new ParentAutogroupNode(
node.parent,
{
...head.value,
autogrouped_by: {
op: head.value && 'op' in head.value ? head.value.op ?? '' : '',
},
},
{
event_id: undefined,
project_slug: undefined,
},
head,
tail
);
if (!node.parent) {
throw new Error('Parent node is missing, this should be unreachable code');
}
autoGroupedNode.groupCount = groupMatchCount + 1;
autoGroupedNode.space = [
head.value.start_timestamp,
tail.value.timestamp - head.value.start_timestamp,
];
for (const c of tail.children) {
c.parent = autoGroupedNode;
queue.push(c);
}
const index = node.parent.children.indexOf(node);
node.parent.children[index] = autoGroupedNode;
}
}
static AutogroupSiblingSpanNodes(root: TraceTreeNode<TraceTree.NodeValue>): void {
const queue = [root];
while (queue.length > 0) {
const node = queue.pop()!;
if (node.children.length < 5) {
for (const child of node.children) {
queue.push(child);
}
continue;
}
let index = 0;
let matchCount = 0;
while (index < node.children.length) {
const current = node.children[index] as TraceTreeNode<TraceTree.Span>;
const next = node.children[index + 1] as TraceTreeNode<TraceTree.Span>;
if (
next &&
next.children.length === 0 &&
current.children.length === 0 &&
next.value.op === current.value.op &&
next.value.description === current.value.description
) {
matchCount++;
// If the next node is the last node in the list, we keep iterating
if (index + 1 < node.children.length) {
index++;
continue;
}
}
if (matchCount >= 4) {
const autoGroupedNode = new SiblingAutogroupNode(
node,
{
...current.value,
autogrouped_by: {
op: current.value.op ?? '',
description: current.value.description ?? '',
},
},
{
event_id: undefined,
project_slug: undefined,
}
);
autoGroupedNode.groupCount = matchCount + 1;
const start = index - matchCount;
for (let j = start; j < index - 1; j++) {
autoGroupedNode.children.push(node.children[j]);
autoGroupedNode.children[autoGroupedNode.children.length - 1].parent =
autoGroupedNode;
}
node.children.splice(start, matchCount + 1, autoGroupedNode);
index = start + 1;
matchCount = 0;
} else {
index++;
matchCount = 0;
}
}
}
}
// Returns boolean to indicate if node was updated
expand(node: TraceTreeNode<TraceTree.NodeValue>, expanded: boolean): boolean {
if (expanded === node.expanded) {
return false;
}
// Expanding is not allowed for zoomed in nodes
if (node.zoomedIn) {
return false;
}
if (node instanceof ParentAutogroupNode) {
// In parent autogrouping, we perform a node swap and either point the
// head or tails of the autogrouped sequence to the autogrouped node
if (node.expanded) {
const index = this._list.indexOf(node);
const autogroupedChildren = node.getVisibleChildren();
this._list.splice(index + 1, autogroupedChildren.length);
const newChildren = node.tail.getVisibleChildren();
for (const c of node.tail.children) {
c.parent = node;
}
this._list.splice(index + 1, 0, ...newChildren);
} else {
node.head.parent = node;
const index = this._list.indexOf(node);
const childrenCount = node.getVisibleChildrenCount();
this._list.splice(index + 1, childrenCount);
node.getVisibleChildrenCount();
const newChildren = [node.head].concat(
node.head.getVisibleChildren() as TraceTreeNode<TraceTree.Span>[]
);
for (const c of node.children) {
c.parent = node.tail;
}
this._list.splice(index + 1, 0, ...newChildren);
}
node.invalidate(node);
node.expanded = expanded;
return true;
}
if (node.expanded) {
const index = this._list.indexOf(node);
this._list.splice(index + 1, node.getVisibleChildrenCount());
// Flip expanded after collecting visible children
node.expanded = expanded;
} else {
const index = this._list.indexOf(node);
// Flip expanded so that we can collect visible children
node.expanded = expanded;
this._list.splice(index + 1, 0, ...node.getVisibleChildren());
}
node.expanded = expanded;
return true;
}
zoomIn(
node: TraceTreeNode<TraceTree.NodeValue>,
zoomedIn: boolean,
options: {
api: Client;
organization: Organization;
}
): Promise<Event | null> {
if (zoomedIn === node.zoomedIn) {
return Promise.resolve(null);
}
if (!zoomedIn) {
const index = this._list.indexOf(node);
const childrenCount = node.getVisibleChildrenCount();
this._list.splice(index + 1, childrenCount);
node.zoomedIn = zoomedIn;
if (node.expanded) {
this._list.splice(index + 1, 0, ...node.getVisibleChildren());
}
return Promise.resolve(null);
}
const promise =
this._spanPromises.get(node) ??
fetchTransactionSpans(
options.api,
options.organization,
node.metadata.project_slug!,
node.metadata.event_id!
);
promise.then(data => {
const spans = data.entries.find(s => s.type === 'spans');
if (!spans) {
return data;
}
// Remove existing entries from the list
const index = this._list.indexOf(node);
if (node.expanded) {
const childrenCount = node.getVisibleChildrenCount();
this._list.splice(index + 1, childrenCount);
}
// Api response is not sorted
if (spans.data) {
spans.data.sort((a, b) => a.start_timestamp - b.start_timestamp);
}
TraceTree.FromSpans(node, spans.data);
const spanChildren = node.getVisibleChildren();
this._list.splice(index + 1, 0, ...spanChildren);
return data;
});
this._spanPromises.set(node, promise);
return promise;
}
toList(): TraceTreeNode<TraceTree.NodeValue>[] {
const list: TraceTreeNode<TraceTree.NodeValue>[] = [];
function visit(node: TraceTreeNode<TraceTree.NodeValue>) {
list.push(node);
if (!node.expanded) {
return;
}
for (const child of node.children) {
visit(child);
}
}
for (const child of this.root.children) {
visit(child);
}
return list;
}
/**
* Prints the tree in a human readable format, useful for debugging and testing
*/
print() {
// root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
const print = this.list
.map(t => printNode(t, 0))
.filter(Boolean)
.join('\n');
// eslint-disable-next-line no-console
console.log(print);
}
build() {
this._list = this.toList();
return this;
}
}
export class TraceTreeNode<T extends TraceTree.NodeValue> {
parent: TraceTreeNode<TraceTree.NodeValue> | null = null;
value: T;
expanded: boolean = false;
zoomedIn: boolean = false;
canFetchData: boolean = false;
metadata: TraceTree.Metadata = {
project_slug: undefined,
event_id: undefined,
};
space: [number, number] | null = null;
private _depth: number | undefined;
private _children: TraceTreeNode<TraceTree.NodeValue>[] = [];
private _spanChildren: TraceTreeNode<
TraceTree.Span | TraceTree.MissingInstrumentationSpan
>[] = [];
private _connectors: number[] | undefined = undefined;
constructor(
parent: TraceTreeNode<TraceTree.NodeValue> | null,
value: T,
metadata: TraceTree.Metadata
) {
this.parent = parent ?? null;
this.value = value;
this.metadata = metadata;
if (value && 'timestamp' in value && 'start_timestamp' in value) {
this.space = [value.start_timestamp, value.timestamp - value.start_timestamp];
}
if (isTransactionNode(this) || isTraceNode(this) || isSpanNode(this)) {
this.expanded = true;
}
}
clone(): TraceTreeNode<T> {
const node = new TraceTreeNode(this.parent, this.value, this.metadata);
node.expanded = this.expanded;
node.zoomedIn = this.zoomedIn;
node.canFetchData = this.canFetchData;
node.space = this.space;
node.children = this.children;
return node;
}
get isOrphaned() {
return this.parent?.value && 'orphan_errors' in this.parent.value;
}
get isLastChild() {
return this.parent?.children[this.parent.children.length - 1] === this;
}
/**
* Return a lazily calculated depth of the node in the tree.
* Root node has a value of -1 as it is abstract.
*/
get depth(): number {
if (typeof this._depth === 'number') {
return this._depth;
}
let depth = -2;
let node: TraceTreeNode<any> | null = this;
while (node) {
if (typeof node.parent?.depth === 'number') {
this._depth = node.parent.depth + 1;
return this._depth;
}
depth++;
node = node.parent;
}
this._depth = depth;
return this._depth;
}
/**
* Returns the depth levels at which the row should draw vertical connectors
* negative values mean connector points to an orphaned node
*/
get connectors(): number[] {
if (this._connectors !== undefined) {
return this._connectors!;
}
this._connectors = [];
if (this.parent?.connectors !== undefined) {
this._connectors = [...this.parent.connectors];
if (this.isLastChild || this.value === null) {
return this._connectors;
}
this.connectors.push(this.isOrphaned ? -this.depth : this.depth);
return this._connectors;
}
let node: TraceTreeNode<T> | TraceTreeNode<TraceTree.NodeValue> | null = this.parent;
while (node) {
if (node.value === null) {
break;
}
if (node.isLastChild) {
node = node.parent;
continue;
}
this._connectors.push(node.isOrphaned ? -node.depth : node.depth);
node = node.parent;
}
return this._connectors;
}
/**
* Returns the children that the node currently points to.
* The logic here is a consequence of the tree design, where we want to be able to store
* both transaction and span nodes in the same tree. This results in an annoying API where
* we either store span children separately or transaction children separately. A better design
* would have been to create an invisible meta node that always points to the correct children.
*/
get children(): TraceTreeNode<TraceTree.NodeValue>[] {
if (isAutogroupedNode(this)) {
return this._children;
}
if (isSpanNode(this)) {
return this.canFetchData && !this.zoomedIn ? [] : this.spanChildren;
}
// if a node is zoomed in, return span children, else return transaction children
return this.zoomedIn ? this._spanChildren : this._children;
}
set children(children: TraceTreeNode<TraceTree.NodeValue>[]) {
this._children = children;
}
get spanChildren(): TraceTreeNode<
TraceTree.Span | TraceTree.MissingInstrumentationSpan
>[] {
return this._spanChildren;
}
/**
* Invalidate the visual data used to render the tree, forcing it
* to be recalculated on the next render. This is useful when for example
* the tree is expanded or collapsed, or when the tree is mutated and
* the visual data is no longer valid as the indentation changes
*/
invalidate(root?: TraceTreeNode<TraceTree.NodeValue>) {
this._connectors = undefined;
this._depth = undefined;
if (root) {
const queue = [...this.children];
while (queue.length > 0) {
const next = queue.pop()!;
next.invalidate();
for (let i = 0; i < next.children.length; i++) {
queue.push(next.children[i]);
}
}
}
}
getVisibleChildrenCount(): number {
const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
let count = 0;
if (
this.expanded ||
isParentAutogroupedNode(this) ||
isMissingInstrumentationNode(this)
) {
for (let i = this.children.length - 1; i >= 0; i--) {
stack.push(this.children[i]);
}
}
while (stack.length > 0) {
const node = stack.pop()!;
count++;
// Since we're using a stack and it's LIFO, reverse the children before pushing them
// to ensure they are processed in the original left-to-right order.
if (node.expanded || isParentAutogroupedNode(node)) {
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
return count;
}
getVisibleChildren(): TraceTreeNode<TraceTree.NodeValue>[] {
const stack: TraceTreeNode<TraceTree.NodeValue>[] = [];
const children: TraceTreeNode<TraceTree.NodeValue>[] = [];
if (
this.expanded ||
isParentAutogroupedNode(this) ||
isMissingInstrumentationNode(this)
) {
for (let i = this.children.length - 1; i >= 0; i--) {
stack.push(this.children[i]);
}
}
while (stack.length > 0) {
const node = stack.pop()!;
children.push(node);
// Since we're using a stack and it's LIFO, reverse the children before pushing them
// to ensure they are processed in the original left-to-right order.
if (node.expanded || isParentAutogroupedNode(node)) {
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
return children;
}
print() {
// root nodes are -1 indexed, so we add 1 to the depth so .repeat doesnt throw
const offset = this.depth === -1 ? 1 : 0;
const nodes = [this, ...this.getVisibleChildren()];
const print = nodes
.map(t => printNode(t, offset))
.filter(Boolean)
.join('\n');
// eslint-disable-next-line no-console
console.log(print);
}
static Root() {
return new TraceTreeNode(null, null, {
event_id: undefined,
project_slug: undefined,
});
}
}
export class ParentAutogroupNode extends TraceTreeNode<TraceTree.ChildrenAutogroup> {
head: TraceTreeNode<TraceTree.Span>;
tail: TraceTreeNode<TraceTree.Span>;
groupCount: number = 0;
constructor(
parent: TraceTreeNode<TraceTree.NodeValue> | null,
node: TraceTree.ChildrenAutogroup,
metadata: TraceTree.Metadata,
head: TraceTreeNode<TraceTree.Span>,
tail: TraceTreeNode<TraceTree.Span>
) {
super(parent, node, metadata);
this.head = head;
this.tail = tail;
}
get children() {
if (this.expanded) {
return [this.head];
}
return this.tail.children;
}
}
export class SiblingAutogroupNode extends TraceTreeNode<TraceTree.SiblingAutogroup> {
groupCount: number = 0;
constructor(
parent: TraceTreeNode<TraceTree.NodeValue> | null,
node: TraceTree.SiblingAutogroup,
metadata: TraceTree.Metadata
) {
super(parent, node, metadata);
}
}
function partialTransaction(
partial: Partial<TraceTree.Transaction>
): TraceTree.Transaction {
return {
start_timestamp: 0,
timestamp: 0,
errors: [],
performance_issues: [],
parent_span_id: '',
span_id: '',
parent_event_id: '',
project_id: 0,
'transaction.duration': 0,
'transaction.op': 'db',
'transaction.status': 'ok',
generation: 0,
project_slug: '',
event_id: `event_id`,
transaction: `transaction`,
children: [],
...partial,
};
}
export function makeExampleTrace(metadata: TraceTree.Metadata): TraceTree {
const trace: TraceTree.Trace = {
transactions: [],
orphan_errors: [],
};
function randomBetween(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
let start = new Date().getTime();
for (let i = 0; i < 25; i++) {
const end = start + randomBetween(100, 200);
const nest = i > 0 && Math.random() > 0.5;
if (nest) {
const parent = trace.transactions[trace.transactions.length - 1];
parent.children.push(
partialTransaction({
...metadata,
generation: 0,
start_timestamp: start,
transaction: `parent transaction ${i}`,
timestamp: end,
})
);
parent.timestamp = end;
} else {
trace.transactions.push(
partialTransaction({
...metadata,
generation: 0,
start_timestamp: start,
transaction: 'loading...',
['transaction.op']: 'loading',
timestamp: end,
})
);
}
start = end;
}
const tree = TraceTree.FromTrace(trace);
return tree;
}
function printNode(t: TraceTreeNode<TraceTree.NodeValue>, offset: number): string {
// +1 because we may be printing from the root which is -1 indexed
const padding = ' '.repeat(t.depth + offset);
if (isAutogroupedNode(t)) {
if (isParentAutogroupedNode(t)) {
return padding + `parent autogroup (${t.groupCount})`;
}
if (isSiblingAutogroupedNode(t)) {
return padding + `sibling autogroup (${t.groupCount})`;
}
return padding + 'autogroup';
}
if (isSpanNode(t)) {
return padding + t.value?.op ?? 'unknown span op';
}
if (isTransactionNode(t)) {
return padding + t.value.transaction ?? 'unknown transaction';
}
if (isMissingInstrumentationNode(t)) {
return padding + 'missing_instrumentation';
}
if (isRootNode(t)) {
return padding + 'Root';
}
if (isTraceNode(t)) {
return padding + 'Trace';
}
throw new Error('Not implemented');
}