UNPKG

@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
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 ?? '?'}>` }