UNPKG

@zendesk/retrace

Version:

define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API

347 lines 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.INACTIVE_CONTEXT = void 0; exports.withName = withName; exports.withLabel = withLabel; exports.withPerformanceEntryName = withPerformanceEntryName; exports.withType = withType; exports.withStatus = withStatus; exports.withAttributes = withAttributes; exports.withId = withId; exports.withMatchingRelations = withMatchingRelations; exports.withOccurrence = withOccurrence; exports.withComponentRenderCount = withComponentRenderCount; exports.whenIdle = whenIdle; exports.requiredSpanWithErrorStatus = requiredSpanWithErrorStatus; exports.continueWithErrorStatus = continueWithErrorStatus; exports.withAllConditions = withAllConditions; exports.withOneOfConditions = withOneOfConditions; exports.not = not; exports.fromDefinition = fromDefinition; exports.findMatchingSpan = findMatchingSpan; // eslint-disable-next-line @typescript-eslint/no-explicit-any exports.INACTIVE_CONTEXT = { definition: { computedSpanDefinitions: {}, computedValueDefinitions: {}, name: 'NO_TRACE_ACTIVE', relationSchema: {}, relationSchemaName: 'NONE', requiredSpans: [], variants: { none: { timeout: 0 } }, }, input: { id: 'NO_TRACE_ACTIVE', relatedTo: {}, startTime: { now: 0, epoch: 0 }, variant: 'none', }, recordedItems: new Map(), recordedItemsByLabel: {}, }; /** * The common name of the span to match. Can be a string, RegExp, or function. */ function withName(value) { const matcher = ({ span }, { input: { relatedTo } } = exports.INACTIVE_CONTEXT) => { if (typeof value === 'string') return span.name === value; if (value instanceof RegExp) return value.test(span.name); return value(span.name, relatedTo); }; matcher.fromDefinition = { name: value }; return matcher; } // DRAFT TODO: make test case if one doesnt exist yet // withName((name, relatedTo) => !relatedTo ? false : name === `OmniLog/${relatedTo.ticketId}`) function withLabel(value) { const matcher = ({ annotation }) => annotation?.labels?.includes(value) ?? false; matcher.fromDefinition = { label: value }; return matcher; } /** * The PerformanceEntry.name of the entry to match. Can be a string, RegExp, or function. */ function withPerformanceEntryName(value) { const matcher = ({ span }, { input: { relatedTo } } = exports.INACTIVE_CONTEXT) => { const entryName = span.performanceEntry?.name; if (!entryName) return false; if (typeof value === 'string') return entryName === value; if (value instanceof RegExp) return value.test(entryName); return value(entryName, relatedTo); }; matcher.fromDefinition = { performanceEntryName: value }; return matcher; } function withType(value) { const matcher = ({ span }) => span.type === value; matcher.fromDefinition = { type: value }; return matcher; } function withStatus(value) { const matcher = ({ span }) => span.status === value; matcher.fromDefinition = { status: value }; return matcher; } /** * The subset of attributes (metadata) to match against the span. */ function withAttributes(attrs) { const matcher = ({ span }) => { if (!span.attributes) return false; return Object.entries(attrs).every(([key, value]) => span.attributes[key] === value); }; matcher.fromDefinition = { attributes: attrs }; return matcher; } function withId(value) { const matcher = ({ span }) => span.id === value; matcher.fromDefinition = { id: value }; return matcher; } /** * A list of keys of trace's relations to match against the span's. */ function withMatchingRelations(keys = true) { const matcher = ({ span }, { input: { relatedTo: r }, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment definition: { relationSchema }, } = exports.INACTIVE_CONTEXT) => { // DRAFT TODO: add test case when relatedTo is missing // if the relatedTo isn't set on the trace yet, we can't match against it, so we return early // similarly, if the span doesn't have any relatedTo set const relatedToInput = r; if (!span.relatedTo || !relatedToInput) return false; const spanRelatedTo = span.relatedTo; const resolvedKeys = typeof keys === 'boolean' && keys ? Object.keys(relationSchema) : keys; if (!resolvedKeys) return false; return resolvedKeys.every((key) => key in spanRelatedTo && spanRelatedTo[key] === relatedToInput[key]); }; matcher.fromDefinition = { matchingRelations: keys }; return matcher; } /** * The occurrence of the span with the same name and type within the operation. */ function withOccurrence(value) { const matcher = ({ annotation }) => { if (!annotation) return false; if (typeof value === 'number') return annotation.occurrence === value; return value(annotation.occurrence); }; matcher.fromDefinition = { occurrence: value }; return matcher; } function withComponentRenderCount(name, renderCount) { const nameMatcher = withName(name); const matcher = (spanAndAnnotation, context) => { if (!('renderCount' in spanAndAnnotation.span)) return false; return (nameMatcher(spanAndAnnotation, context) && spanAndAnnotation.span.renderCount === renderCount); }; matcher.fromDefinition = { name, renderCount }; return matcher; } /** * only applicable for component-lifecycle entries */ function whenIdle(value = true) { const matcherFn = ({ span }) => ('isIdle' in span ? span.isIdle === value : false); const result = Object.assign(matcherFn, // add a tag to the function if set to true value ? { idleCheck: value } : {}); result.fromDefinition = { isIdle: value }; return result; } /** * @internal * tag matcher with a special, internal matcher tag, and match on span.status === 'error' */ function requiredSpanWithErrorStatus() { const matcherFn = ({ span }) => span.status === 'error'; const result = Object.assign(matcherFn, // add a tag to the function if set to true { requiredSpan: true }); return result; } /** * Only applicable for 'requiredSpans' list: it will opt-out of the default behavior, * which interrupts the trace if the requiredSpan has an error status. */ function continueWithErrorStatus() { const matcherFn = () => true; const result = Object.assign(matcherFn, // add a tag to the function if set to true { continueWithErrorStatus: true }); return result; } // logical combinators: // AND function withAllConditions(...matchers) { const tags = {}; const definition = {}; for (const matcher of matchers) { // carry over tags from sub-matchers Object.assign(tags, matcher); if (matcher.fromDefinition) { // carry over definition from sub-matchers Object.assign(definition, matcher.fromDefinition); } } const matcherFn = (...args) => matchers.every((matcher) => matcher(...args)); return Object.assign(matcherFn, tags, { fromDefinition: definition }); } // OR function withOneOfConditions(...matchers) { const tags = {}; for (const matcher of matchers) { // carry over tags from sub-matchers Object.assign(tags, matcher); } const matcherFn = (...args) => matchers.some((matcher) => matcher(...args)); return Object.assign(matcherFn, tags, { fromDefinition: { oneOf: matchers } }); } function not(matcher) { // Create a new matcher function that negates the input matcher const notMatcher = (...args) => !matcher(...args); // If the original matcher has a fromDefinition property, create a new one for the negated matcher if (matcher.fromDefinition) { notMatcher.fromDefinition = { not: matcher.fromDefinition }; } return notMatcher; } function fromDefinition(definition) { const matchers = []; // Handle special case: if both name and renderCount are present, use withComponentRenderCount // instead of separate withName and other matchers if (definition.renderCount !== undefined && definition.name) { matchers.push(withComponentRenderCount(definition.name, definition.renderCount)); } else if (definition.name) { matchers.push(withName(definition.name)); } if (definition.performanceEntryName) { matchers.push(withPerformanceEntryName(definition.performanceEntryName)); } if (definition.type) { matchers.push(withType(definition.type)); } if (definition.status) { matchers.push(withStatus(definition.status)); } if (definition.attributes) { matchers.push(withAttributes(definition.attributes)); } if (definition.id) { matchers.push(withId(definition.id)); } if (definition.matchingRelations) { matchers.push(withMatchingRelations(definition.matchingRelations)); } if (definition.occurrence) { matchers.push(withOccurrence(definition.occurrence)); } if (definition.isIdle) { matchers.push(whenIdle(definition.isIdle)); } if (definition.label) { matchers.push(withLabel(definition.label)); } if (definition.fn) { matchers.push(definition.fn); } let combined; // Check if definition has oneOf property if (definition.oneOf) { // Convert each definition in oneOf array to a matcher and combine with OR const oneOfMatchers = definition.oneOf.map((def) => fromDefinition(def)); combined = withAllConditions(...matchers, withOneOfConditions(...oneOfMatchers)); } else if (definition.not) { // Handle the negation case const notMatcher = fromDefinition(definition.not); // If there are other matchers, combine them with AND and then negate the result // eslint-disable-next-line unicorn/prefer-ternary if (matchers.length > 0) { combined = withAllConditions(...matchers, not(notMatcher)); } else { // If there are no other matchers, just negate the single matcher combined = not(notMatcher); } } else { combined = withAllConditions(...matchers); } combined.fromDefinition = definition; // add public tags: if (typeof definition.continueWithErrorStatus === 'boolean') { combined.continueWithErrorStatus = definition.continueWithErrorStatus; } if (typeof definition.nthMatch === 'number') { combined.nthMatch = definition.nthMatch; } if (typeof definition.lowestIndexToConsider === 'number') { combined.lowestIndexToConsider = definition.lowestIndexToConsider; } if (typeof definition.highestIndexToConsider === 'number') { combined.highestIndexToConsider = definition.highestIndexToConsider; } return combined; } /** * Evaluates a span matcher against an entry array. * Respects matching index, lowestIndexToConsider, and highestIndexToConsider. */ function findMatchingSpan(matcher, recordedItemsArray, context, /** config argument can be used to override tags from matcher: */ { lowestIndexToConsider = matcher.lowestIndexToConsider ?? 0, highestIndexToConsider: highestIndexToConsiderInput = matcher.highestIndexToConsider, nthMatch = matcher.nthMatch, } = {}) { const highestIndexToConsider = highestIndexToConsiderInput === undefined ? recordedItemsArray.length - 1 : Math.min(highestIndexToConsiderInput, recordedItemsArray.length - 1); let matchedCount = 0; // For positive or undefined indices - find with specified index offset if (nthMatch === undefined || nthMatch >= 0) { for (let i = lowestIndexToConsider; i <= highestIndexToConsider; i++) { const spanAndAnnotation = recordedItemsArray[i]; if (matcher(spanAndAnnotation, context)) { if (nthMatch === undefined || nthMatch === matchedCount) { return spanAndAnnotation; } matchedCount++; } } // we didn't find a match with the specified index return undefined; } // For negative indices - iterate from the end // If nthMatch is -1, we need the last match (index 0 from reverse) // If nthMatch is -2, we need the second-to-last match (index 1 from reverse), etc. const targetIndex = Math.abs(nthMatch) - 1; // Iterate from the end of the array // TODO: I'm wondering if we should sort recordedItemsArrayReversed by the end time...? // For that matter, should recordedItemsArray be sorted by their start time? // If yes, it might be good to do this in createTraceRecording and pass in both recordedItemsArray and recordedItemsArrayReversed pre-sorted, so we don't sort every time we need to calculate a computed span. for (let i = highestIndexToConsider; i >= 0; i--) { const spanAndAnnotation = recordedItemsArray[i]; if (matcher(spanAndAnnotation, context)) { if (matchedCount === targetIndex) { return spanAndAnnotation; } matchedCount++; } } return undefined; } //# sourceMappingURL=matchSpan.js.map