UNPKG

@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
"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