@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
178 lines (159 loc) • 6.15 kB
text/typescript
import { INHERIT_FROM_PARENT } from './constants'
import {
findMatchingSpan,
fromDefinition,
type ParentSpanMatcher,
} from './matchSpan'
import { type GetParentSpanFn, PARENT_SPAN, type Span } from './spanTypes'
import { TICK_META, TICK_META_END } from './TickParentResolver'
import type { RelationSchemasBase } from './types'
export function ensureSpanHasInheritedAttributes<
const RelationSchemasT extends RelationSchemasBase<RelationSchemasT>,
SpanT extends Span<RelationSchemasT>,
>(span: SpanT, heritableSpanAttributes?: readonly string[]): boolean {
let allRequestedAttributesInheritedFromLineage = true
for (const [key, value] of Object.entries(span.attributes)) {
const inheritanceType =
value === INHERIT_FROM_PARENT
? 'span-request'
: heritableSpanAttributes?.includes(key)
? 'always-requested'
: undefined
if (!inheritanceType) {
// eslint-disable-next-line no-continue
continue
}
allRequestedAttributesInheritedFromLineage = false
let ancestorSpan: Span<RelationSchemasT> | undefined = span
const lineageRequiringAttributes: Span<RelationSchemasT>[] = []
// eslint-disable-next-line no-cond-assign
while ((ancestorSpan = ancestorSpan[PARENT_SPAN])) {
if (
ancestorSpan.attributes[key] === INHERIT_FROM_PARENT ||
inheritanceType === 'always-requested'
) {
// parent also requests inheritance, so we need to keep going
lineageRequiringAttributes.push(ancestorSpan)
} else if (ancestorSpan.attributes[key] !== undefined) {
// found an ancestor with the attribute, let's assign it
// eslint-disable-next-line no-param-reassign
span.attributes[key] = ancestorSpan.attributes[key]
// let's also assign it to all the intermediary ancestors that requested inheritance
// as a performance optimization (less work to do later)
for (const ancestor of lineageRequiringAttributes) {
ancestor.attributes[key] = ancestorSpan.attributes[key]
}
allRequestedAttributesInheritedFromLineage = true
break
}
}
}
return allRequestedAttributesInheritedFromLineage
}
export function createParentSpanResolver<
const RelationSchemasT extends RelationSchemasBase<RelationSchemasT>,
SpanT extends Span<RelationSchemasT>,
>(
span: SpanT,
parentSpanMatcher?: ParentSpanMatcher<
keyof RelationSchemasT,
RelationSchemasT,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>,
heritableSpanAttributes?: readonly string[],
): GetParentSpanFn<RelationSchemasT> {
let shouldAttemptToResolveParentAgain = true
let inheritedRequestedAttributes = false
return (context, recursive) => {
const getParentSpan = () => {
if (span[PARENT_SPAN] || !parentSpanMatcher) {
// short-circuit if we have the parent span, or if there is nowhere to search for it
return span[PARENT_SPAN]
}
if (!shouldAttemptToResolveParentAgain) {
return undefined
}
const tickSource =
parentSpanMatcher.search === 'span-created-tick' ||
!context.traceContext
? context.thisSpanAndAnnotation.span[TICK_META]
: parentSpanMatcher.search === 'span-ended-tick'
? context.thisSpanAndAnnotation.span[TICK_META_END]
: undefined
if (tickSource?.spansInCurrentTick.tickCompleted) {
// do not attempt to resolve parent span again - if we haven't found in this iteration,
// we will not find it, because now more data will be added to the tick
shouldAttemptToResolveParentAgain = false
}
const spanAndAnnotations = tickSource
? tickSource?.spansInCurrentTick.map(
(sp) =>
context.traceContext?.recordedItems.get(sp.id) ?? { span: sp },
) ?? []
: // note: parentSpanMatcher.search === 'entire-recording' only works if the traceContext is available
[...context.traceContext!.recordedItems.values()]
const parentSpanMatchFn =
typeof parentSpanMatcher.match === 'object'
? fromDefinition(parentSpanMatcher.match)
: parentSpanMatcher.match
// memoize the matcher function to avoid re-creating it on every call
// eslint-disable-next-line no-param-reassign
parentSpanMatcher.match = parentSpanMatchFn
const thisSpanIndex =
parentSpanMatcher.search === 'entire-recording'
? spanAndAnnotations.findIndex(
(spanAndAnnotation) => spanAndAnnotation.span.id === span.id,
)
: tickSource?.thisSpanInCurrentTickIndex ?? -1
if (thisSpanIndex === -1) {
// invalid
return undefined
}
const found = findMatchingSpan(
parentSpanMatchFn,
spanAndAnnotations,
context.traceContext,
{
...(parentSpanMatcher.searchDirection === 'after-self' && {
nthMatch: 0,
lowestIndexToConsider: thisSpanIndex + 1,
}),
...(parentSpanMatcher.searchDirection === 'before-self' && {
nthMatch: -1,
highestIndexToConsider: thisSpanIndex - 1,
}),
},
)
if (found) {
// cache the found parent span on the span itself
// so we don't have to search for it again
// eslint-disable-next-line no-param-reassign
span[PARENT_SPAN] = found.span
return found.span
}
return undefined
}
const parentSpan = getParentSpan()
if (parentSpan) {
if (recursive) {
parentSpan.getParentSpan?.(
{
traceContext: context.traceContext,
thisSpanAndAnnotation: context.traceContext?.recordedItems.get(
parentSpan.id,
) ?? { span: parentSpan },
},
recursive,
)
}
if (!inheritedRequestedAttributes) {
inheritedRequestedAttributes = ensureSpanHasInheritedAttributes<
RelationSchemasT,
SpanT
>(span, heritableSpanAttributes)
}
}
return parentSpan
}
}