@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
293 lines • 12.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tracer = void 0;
const ensureMatcherFn_1 = require("./ensureMatcherFn");
const ensureTimestamp_1 = require("./ensureTimestamp");
const Trace_1 = require("./Trace");
/**
* Look for an adopting parent for the given trace definition
*/
function lookForAdoptingParent(tracerDef, globalUtils) {
const maybeParent = globalUtils.getCurrentTrace();
if (!maybeParent)
return undefined;
// First check if the immediate parent can adopt
if (maybeParent.canAdoptChild(tracerDef.name)) {
return maybeParent;
}
// Breadth-first search through children
const queue = [...maybeParent.children];
while (queue.length > 0) {
const current = queue.shift();
if (current.canAdoptChild(tracerDef.name)) {
return current;
}
// Add current trace's children to the end of the queue for breadth-first traversal
queue.push(...current.children);
}
return undefined;
}
function buildTraceUtilities(utilities) {
const traceUtilities = {
...utilities,
// every trace gets its own deduplication strategy instance:
performanceEntryDeduplicationStrategy: utilities.getPerformanceEntryDeduplicationStrategy?.(),
parentTraceRef: undefined,
};
// TODO: make traceUtilities into a class instance,
// and require that instance as the Trace property
// to indicate to TS this object needs to be passed by reference,
// and can not be spread into another one
// (because that looses the reference to the parentTraceRef, causing bugs!)
return traceUtilities;
}
/**
* Build child-scoped trace utilities that delegate getCurrentTrace and replaceCurrentTrace
* to work properly with child traces while maintaining parent-child relationships
*/
function buildChildUtilities(getChildTrace, parent) {
const utilities = {
// reporting and errors continue to use the original functions
...parent.traceUtilities,
// parent can be swapped out, so we store it here:
parentTraceRef: parent,
// redirect "current trace" queries to return the itself when asked
getCurrentTrace: getChildTrace,
};
utilities.onTraceEnd = (trace, finalTransition, recording) => {
utilities.parentTraceRef.onChildEnd(trace, finalTransition, recording);
};
// handle replacing the current trace in the context of parent-child relationships
utilities.replaceCurrentTrace = (getNewTrace, reason) => {
switch (reason) {
case 'another-trace-started': {
const newTrace = getNewTrace();
// as a child, starting another trace doesn't actually replace it,
// only adds a sibiling to the parent
utilities.parentTraceRef.adoptChild(newTrace);
return newTrace;
}
case 'definition-changed': {
// For other reasons, interrupt the current child and adopt the new one
const currentChild = getChildTrace();
if (currentChild) {
currentChild.interrupt({ reason });
}
const newTrace = getNewTrace();
utilities.parentTraceRef.adoptChild(newTrace); // adds to children
return newTrace;
}
default: {
const newTrace = getNewTrace();
utilities.parentTraceRef.traceUtilities.reportErrorFn(new Error(`Unexpected reason for replacing current trace: ${reason}`), {
definition: newTrace.sourceDefinition,
});
return newTrace;
}
}
};
return utilities;
}
/**
* Recursively search for a child trace with the specified definition
*/
function findChildWithDefinition(trace, targetDefinition) {
// TOOD: switch to breadth-first
for (const child of trace.children) {
if (child.sourceDefinition === targetDefinition) {
return child;
}
// Recursively search in grandchildren
const found = findChildWithDefinition(child, targetDefinition);
if (found) {
return found;
}
}
return undefined;
}
/**
* Tracer can create draft traces and start traces
*/
class Tracer {
definition;
rootTraceUtilities;
constructor(definition, rootTraceUtilities) {
this.definition = definition;
this.rootTraceUtilities = rootTraceUtilities;
}
/**
* @returns The ID of the trace.
*/
start = (input, definitionModifications) => {
const trace = this.createDraftInternal(input);
trace?.transitionDraftToActive({
relatedTo: input.relatedTo,
...definitionModifications,
});
return trace?.input.id;
};
createDraft = (input, definitionModifications) => this.createDraftInternal(input, definitionModifications)?.input.id;
createDraftInternal = (input, definitionModifications) => {
const id = input.id ?? this.rootTraceUtilities.generateId('trace');
// Look for an adopting parent according to the nested proposal
let parentTrace = lookForAdoptingParent(this.definition, this.rootTraceUtilities);
let trace;
if (parentTrace) {
// if the child trace started, the parent *must* wait for it to end
// update the parentTrace to requireToEnd the new child trace:
parentTrace = parentTrace.recreateTraceWithDefinitionModifications({
additionalRequiredSpans: [
{ type: 'operation', name: this.definition.name, id },
],
});
// Create child utilities with a getter function that will return the child trace
let childTrace;
const getChildTrace = () => childTrace;
const utilities = buildChildUtilities(getChildTrace, parentTrace);
// Create the trace with child utilities
trace = new Trace_1.Trace({
definition: this.definition,
input: {
...input,
// relatedTo will be overwritten later during initialization of the trace
relatedTo: undefined,
startTime: (0, ensureTimestamp_1.ensureTimestamp)(input.startTime),
id,
parentTraceId: parentTrace.input.id,
},
definitionModifications,
traceUtilities: utilities,
});
// Store reference for the getter function
childTrace = trace;
parentTrace.adoptChild(trace); // F-1/F-2 behaviour
// we do not replace the singleton currentTrace in TraceManager
}
else {
// it's a new root trace
trace = this.rootTraceUtilities.replaceCurrentTrace(
// Create the trace with normal utilities
() => new Trace_1.Trace({
definition: this.definition,
input: {
...input,
// relatedTo will be overwritten later during initialization of the trace
relatedTo: undefined,
startTime: (0, ensureTimestamp_1.ensureTimestamp)(input.startTime),
id,
},
definitionModifications,
traceUtilities: buildTraceUtilities(this.rootTraceUtilities),
}), 'another-trace-started');
}
return trace;
};
interrupt = ({ error } = {}) => {
const trace = this.getCurrentTraceOrWarn();
if (!trace)
return;
if (error) {
trace.processSpan({
id: this.rootTraceUtilities.generateId('span'),
name: error.name,
startTime: (0, ensureTimestamp_1.ensureTimestamp)(),
type: 'error',
status: 'error',
relatedTo: { ...trace.input.relatedTo },
attributes: {},
duration: 0,
error,
getParentSpan: () => undefined,
});
trace.interrupt({ reason: 'aborted' });
return;
}
if (trace.isDraft) {
trace.interrupt({ reason: 'draft-cancelled' });
return;
}
trace.interrupt({ reason: 'aborted' });
};
/**
* Adds additional required spans or debounce spans to the current trace *only*.
* Note: This recreates the Trace instance with the modified definition and replays all the spans.
*/
addRequirementsToCurrentTraceOnly = (definitionModifications) => {
const trace = this.getCurrentTraceOrWarn();
if (!trace)
return;
trace.recreateTraceWithDefinitionModifications(definitionModifications);
};
// can have config changed until we move into active
// from input: relatedTo (required), attributes (optional, merge into)
// from definition, can add items to: requiredSpans (additionalRequiredSpans), debounceOnSpans (additionalDebounceOnSpans)
// documentation: interruption still works and all the other events are buffered
transitionDraftToActive = (inputAndDefinitionModifications, opts) => {
const trace = this.getCurrentTraceOrWarn();
if (!trace)
return;
trace.transitionDraftToActive(inputAndDefinitionModifications, opts);
};
getCurrentTraceInternal = () => {
const rootTrace = this.rootTraceUtilities.getCurrentTrace();
if (!rootTrace) {
return undefined;
}
// verify that trace is the same definition as the Tracer's definition
if (rootTrace.sourceDefinition === this.definition) {
return rootTrace;
}
const foundChild = findChildWithDefinition(rootTrace, this.definition);
if (foundChild) {
return foundChild;
}
return undefined;
};
/**
* @returns The current Trace's context if it exists anywhere in the trace tree,
* and matches the Tracer's definition.
*/
getCurrentTrace = () => this.getCurrentTraceInternal();
// same as getCurrentTrace, but with a warning if no trace or a different trace is found
getCurrentTraceOrWarn = () => {
const trace = this.getCurrentTraceInternal();
if (trace) {
return trace;
}
const rootTrace = this.rootTraceUtilities.getCurrentTrace();
if (!rootTrace) {
// No active trace at all
this.rootTraceUtilities.reportWarningFn(new Error(`No current active trace when initializing a trace. Call tracer.start(...) or tracer.createDraft(...) beforehand.`), { definition: this.definition });
return undefined;
}
this.rootTraceUtilities.reportWarningFn(new Error(`Trying to find an active '${this.definition.name}' trace, however the started root trace (${rootTrace.sourceDefinition.name}) has a different definition`), { definition: this.definition });
return undefined;
};
/**
* Dynamically add a computed span to the trace definition.
* Will apply to any trace created *after* calling this function.
*/
defineComputedSpan = (definition) => {
this.definition.computedSpanDefinitions[definition.name] = {
startSpan: typeof definition.startSpan === 'string'
? definition.startSpan
: (0, ensureMatcherFn_1.ensureMatcherFn)(definition.startSpan),
endSpan: typeof definition.endSpan === 'string'
? definition.endSpan
: (0, ensureMatcherFn_1.ensureMatcherFn)(definition.endSpan),
};
};
/**
* Dynamically add a computed value to the trace definition.
* Will apply to any trace created *after* calling this function.
*/
defineComputedValue = (definition) => {
const convertedMatches = definition.matches.map((m) => (0, ensureMatcherFn_1.ensureMatcherFn)(m));
this.definition.computedValueDefinitions[definition.name] = {
matches: convertedMatches,
computeValueFromMatches: definition.computeValueFromMatches,
};
};
}
exports.Tracer = Tracer;
//# sourceMappingURL=Tracer.js.map