@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
JavaScript
;
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