@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
271 lines (245 loc) • 8.41 kB
text/typescript
import {
DEFAULT_DEBOUNCE_DURATION,
DEFAULT_INTERACTIVE_TIMEOUT_DURATION,
} from './constants'
import type { SpanMatch, SpanMatcherFn } from './matchSpan'
import { createTraceRecording } from './recordingComputeUtils'
import type { SpanAndAnnotation } from './spanAnnotationTypes'
import type { FinalTransition, OnEnterStatePayload } from './Trace'
import type { TraceRecording } from './traceRecordingTypes'
import type {
DraftTraceContext,
RelationSchemasBase,
TraceContext,
} from './types'
// Helper to check if error is suppressed
export function isSuppressedError<
RelationSchemasT extends RelationSchemasBase<RelationSchemasT>,
>(
trace: DraftTraceContext<keyof RelationSchemasT, RelationSchemasT, string>,
spanAndAnnotation: SpanAndAnnotation<RelationSchemasT>,
) {
return !!trace.definition.suppressErrorStatusPropagationOnSpans?.some((fn) =>
fn(spanAndAnnotation, trace),
)
}
// Helper to format ms
export function formatMs(ms?: number): string {
if (ms == null) return 'n/a'
if (ms < 1_000) return `${ms.toFixed(0)}ms`
return `${(ms / 1_000).toFixed(2)}s`
}
// Helper to get config summary from traceContext or definition
export function getConfigSummary<
SelectedRelationNameT extends keyof RelationSchemasT,
RelationSchemasT extends RelationSchemasBase<RelationSchemasT>,
VariantsT extends string,
>(
traceContext: Pick<
DraftTraceContext<SelectedRelationNameT, RelationSchemasT, VariantsT>,
'definition' | 'input'
>,
) {
const def = traceContext.definition
const variant = def.variants[traceContext.input.variant]
const timeout = variant?.timeout
const debounce =
(def.debounceOnSpans ?? []).length > 0
? def.debounceWindow ?? DEFAULT_DEBOUNCE_DURATION
: undefined
const interactive =
typeof def.captureInteractive === 'object'
? def.captureInteractive.timeout
: def.captureInteractive
? DEFAULT_INTERACTIVE_TIMEOUT_DURATION
: undefined
return { timeout, debounce, interactive }
}
// Helper to get computed values/spans for completed/interrupted traces
export function getComputedResults<
RelationSchemasT extends RelationSchemasBase<RelationSchemasT>,
>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
traceContext: TraceContext<any, RelationSchemasT, any>,
finalTransition: FinalTransition<RelationSchemasT>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): TraceRecording<any, RelationSchemasT> | undefined {
try {
const recording = createTraceRecording(traceContext, finalTransition)
return recording
} catch {
return undefined
}
}
/**
* Extract timing offsets from a transition object
*/
export const extractTimingOffsets = <
RelationSchemasT extends RelationSchemasBase<RelationSchemasT>,
>(
transition: OnEnterStatePayload<RelationSchemasT>,
) => {
let lastRequiredSpanOffset: number | undefined
let completeSpanOffset: number | undefined
let cpuIdleSpanOffset: number | undefined
if (
'lastRequiredSpanAndAnnotation' in transition &&
transition.lastRequiredSpanAndAnnotation
) {
lastRequiredSpanOffset =
transition.lastRequiredSpanAndAnnotation.annotation
.operationRelativeEndTime
}
if (
'completeSpanAndAnnotation' in transition &&
transition.completeSpanAndAnnotation
) {
completeSpanOffset =
transition.completeSpanAndAnnotation.annotation.operationRelativeEndTime
}
if (
'cpuIdleSpanAndAnnotation' in transition &&
transition.cpuIdleSpanAndAnnotation
) {
cpuIdleSpanOffset =
transition.cpuIdleSpanAndAnnotation.annotation.operationRelativeEndTime
}
return { lastRequiredSpanOffset, completeSpanOffset, cpuIdleSpanOffset }
}
/**
* Attempt to create a more descriptive name from the definition
* This part needs customization based on how 'fromDefinition' is structured.
* Example: Check for specific properties like 'name', 'type', 'label' etc.
*/
export function getMatcherLabelFromCombinator<
RelationSchemasT extends RelationSchemasBase<RelationSchemasT>,
SelectedRelationNameT extends keyof RelationSchemasT,
VariantsT extends string,
>(
def: SpanMatch<SelectedRelationNameT, RelationSchemasT, VariantsT>,
index?: number,
): string {
if ('fromDefinition' in def && def.fromDefinition) {
return getMatcherLabelFromCombinator(def.fromDefinition, index)
}
const parts: string[] = []
// Example: Prioritize 'label' if it exists
if ('label' in def && typeof def.label === 'string') {
return `label="${def.label}"`
}
// Example: Use 'name' if it exists
if ('name' in def) {
if (typeof def.name === 'string') {
parts.push(`name="${def.name}"`)
} else if (def.name instanceof RegExp) {
parts.push(`name=/${def.name.source}/${def.name.flags}`)
}
}
// Example: Add type if present
if ('type' in def && typeof def.type === 'string') {
parts.push(`type="${def.type}"`)
}
// Add other relevant properties from your definition structure
if ('oneOf' in def && Array.isArray(def.oneOf)) {
parts.push(
`( ${def.oneOf
.map((item, i) => getMatcherLabelFromCombinator(item, i))
.join(' OR ')} )`,
)
}
// performanceEntryName
if ('performanceEntryName' in def && def.performanceEntryName !== undefined) {
if (typeof def.performanceEntryName === 'string') {
parts.push(`performanceEntryName="${def.performanceEntryName}"`)
} else if (def.performanceEntryName instanceof RegExp) {
parts.push(
`performanceEntryName=/${def.performanceEntryName.source}/${def.performanceEntryName.flags}`,
)
}
}
// status
if ('status' in def && typeof def.status === 'string') {
parts.push(`status="${def.status}"`)
}
// attributes
if (
'attributes' in def &&
typeof def.attributes === 'object' &&
def.attributes !== null
) {
const attrStr = Object.entries(def.attributes)
.map(([k, v]) => `${k}:${JSON.stringify(v)}`)
.join(', ')
if (attrStr) parts.push(`attributes={${attrStr}}`)
}
// matchingRelations
if ('matchingRelations' in def && def.matchingRelations !== undefined) {
if (Array.isArray(def.matchingRelations)) {
parts.push(`matchingRelations=[${def.matchingRelations.join(', ')}]`)
} else if (
typeof def.matchingRelations === 'boolean' &&
def.matchingRelations
) {
parts.push(`matchingRelations`)
}
}
// occurrence
if ('occurrence' in def && def.occurrence !== undefined) {
if (typeof def.occurrence === 'number') {
parts.push(`occurrence=${def.occurrence}`)
} else if (typeof def.occurrence === 'function') {
parts.push('occurrence=<fn>')
}
}
// isIdle
if ('isIdle' in def && typeof def.isIdle === 'boolean' && def.isIdle) {
parts.push(`isIdle`)
}
// fn
if ('fn' in def && typeof def.fn === 'function') {
parts.push('<customMatcherFn>')
}
// nthMatch
if ('nthMatch' in def && typeof def.nthMatch === 'number') {
parts.push(`nthMatch=${def.nthMatch}`)
}
if (parts.length > 0) {
return parts.join(' AND ')
}
// Fallback: Stringify the definition (can be verbose)
try {
const defString = JSON.stringify(def)
// Limit length to avoid overly long strings
return defString.length > 100 ? `${defString.slice(0, 97)}...` : defString
} catch {
// Fallback if stringify fails
return `<matcher#${index ?? '?'}>`
}
}
/**
* Formats a SpanMatcherFn into a more readable string representation.
* This is a basic implementation and can be significantly improved
* based on the actual structure of `matcher.fromDefinition`.
*
* @param matcher The matcher function to format.
* @param index Optional index for generic naming.
* @returns A string representation of the matcher.
*/
export function formatMatcher<
SelectedRelationNameT extends keyof RelationSchemasT,
RelationSchemasT extends RelationSchemasBase<RelationSchemasT>,
VariantsT extends string,
>(
matcher: SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT>,
index?: number,
): string {
// Check if the matcher has attached definition info
if (matcher.fromDefinition) {
const def = matcher.fromDefinition
if (def && typeof def === 'object') {
return getMatcherLabelFromCombinator(def, index)
}
}
// Fallback if no definition info is available
return `<matcher#${index ?? '?'}>`
}