UNPKG

@zendesk/retrace

Version:

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

806 lines (719 loc) 25.5 kB
import type { CPUIdleProcessorOptions } from './firstCPUIdle' import type { SpanMatch, SpanMatcherFn } from './matchSpan' import type { SpanAndAnnotation } from './spanAnnotationTypes' import type { ActiveTraceInput, Attributes, DraftTraceInput, Span, SpanStatus, } from './spanTypes' import type { AllPossibleTraces, FinalTransition, Trace } from './Trace' import type { TraceRecording } from './traceRecordingTypes' import type { ArrayWithAtLeastOneElement, MapTuple, UnionToIntersection, UnionToTuple, } from './typeUtils' export interface Timestamp { // absolute count of ms from epoch epoch: number // performance.now() time now: number } export type RelationSchemaValue = | StringConstructor | NumberConstructor | BooleanConstructor | readonly (string | number | boolean)[] export type MapSchemaToTypesBase<T> = keyof T extends never ? Record<string, never> : { [K in keyof T]: T[K] extends StringConstructor ? string : T[K] extends NumberConstructor ? number : T[K] extends BooleanConstructor ? boolean : T[K] extends readonly (infer U)[] ? U : never } export type MapSchemaToTypes<RelationSchemasT> = RelationSchemasT extends RelationSchemasT ? MapSchemaToTypesBase<RelationSchemasT> : never // a span can have any combination of relations export type RelationsOnASpan<RelationSchemasT> = Partial< MapSchemaToTypes< UnionToIntersection<RelationSchemasT[keyof RelationSchemasT]> > > export type RelatedTo<RelationSchemasT> = MapSchemaToTypes< RelationSchemasT[keyof RelationSchemasT] > /** * Reverse of MapSchemaToTypes: * - boolean => BooleanConstructor * - string => StringConstructor (if it's the wide string) * - number => NumberConstructor (if it's the wide number) * - union of string/number *literals* => a readonly tuple of those literals * - otherwise => never */ export type MapTypesToSchema<T> = { [K in keyof T]: T[K] extends boolean // 1) If it's (wide) boolean or effectively boolean => BooleanConstructor ? BooleanConstructor : T[K] extends string | number | boolean ? string extends T[K] ? StringConstructor : number extends T[K] ? NumberConstructor : boolean extends T[K] ? BooleanConstructor : readonly [...UnionToTuple<T[K]>] : never } export type RelationSchemasBase<RelationSchemasT> = { [SchemaNameT in keyof RelationSchemasT]: { [K in keyof RelationSchemasT[SchemaNameT]]: RelationSchemaValue } } /** * for now this is always 'operation', but in the future we could also implement tracing 'process' types */ export type TraceType = 'operation' export type TraceStatus = SpanStatus | 'interrupted' // trace interruptions that we consider 'invalid' export const INVALID_TRACE_INTERRUPTION_REASONS = [ 'timeout', 'draft-cancelled', 'invalid-state-transition', 'parent-interrupted', 'child-interrupted', 'child-timeout', ] as const export type TraceInterruptionReasonForInvalidTraces = (typeof INVALID_TRACE_INTERRUPTION_REASONS)[number] export const TRACE_REPLACE_INTERRUPTION_REASONS = [ 'another-trace-started', // if definition changes, we need to recreate the Trace instance and replay the spans 'definition-changed', ] as const export type TraceReplaceInterruptionReason = (typeof TRACE_REPLACE_INTERRUPTION_REASONS)[number] export const VALID_TRACE_INTERRUPTION_REASONS = [ 'waiting-for-interactive-timeout', 'aborted', 'idle-component-no-longer-idle', 'matched-on-interrupt', 'matched-on-required-span-with-error', ...TRACE_REPLACE_INTERRUPTION_REASONS, ] as const export type TraceInterruptionReasonForValidTraces = (typeof VALID_TRACE_INTERRUPTION_REASONS)[number] export type TraceInterruptionReason = | TraceInterruptionReasonForInvalidTraces | TraceInterruptionReasonForValidTraces export type PublicTraceInterruptionReason = | 'aborted' | 'another-trace-started' | 'parent-interrupted' | 'draft-cancelled' | 'definition-changed' // New payload types for different interruption reasons export interface AnotherTraceStartedInterruptionPayload< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > { readonly reason: 'another-trace-started' readonly anotherTrace: { readonly id: string readonly name: string } } export interface GenericInterruptionPayload { readonly reason: Exclude<TraceInterruptionReason, 'another-trace-started'> } // Union type for all possible interruption payloads export type InterruptionReasonPayload< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > = | AnotherTraceStartedInterruptionPayload<RelationSchemasT> | GenericInterruptionPayload // Internal interruption payloads that include all possible reasons export interface InternalAnotherTraceStartedInterruptionPayload< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > { reason: 'another-trace-started' anotherTraceContext: TraceContext< SelectedRelationNameT, RelationSchemasT, VariantsT > } export interface InternalGenericInterruptionPayload { reason: Exclude<TraceInterruptionReason, 'another-trace-started'> } export type InternalInterruptionReasonPayload< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > = | InternalAnotherTraceStartedInterruptionPayload< SelectedRelationNameT, RelationSchemasT, VariantsT > | InternalGenericInterruptionPayload export type SingleTraceReportFn< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > = ( trace: TraceRecording<SelectedRelationNameT, RelationSchemasT>, context: TraceContext<SelectedRelationNameT, RelationSchemasT, VariantsT>, ) => void export type AnyPossibleReportFn< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > = <SelectedRelationNameT extends keyof RelationSchemasT>( trace: TraceRecording<SelectedRelationNameT, RelationSchemasT>, // eslint-disable-next-line @typescript-eslint/no-explicit-any context: TraceContext<SelectedRelationNameT, RelationSchemasT, any>, ) => void export type PartialPossibleTraceContext< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > = Partial<AllPossibleTraceContexts<RelationSchemasT, string>> export type ReportErrorFn< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > = ( error: Error, currentTraceContext?: PartialPossibleTraceContext<RelationSchemasT>, ) => void export type GenerateIdFn = (kind: 'span' | 'tick' | 'trace') => string export interface TraceManagerConfig< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > { reportFn: AnyPossibleReportFn<RelationSchemasT> generateId: GenerateIdFn relationSchemas: RelationSchemasT /** * IMPLEMENTATION TODO: The span types that should be omitted from the trace report. Or maybe a more general way to filter spans? */ // spanTypesOmittedFromReport?: SpanType[] /** * Strategy for deduplicating performance entries. * If not provided, no deduplication will be performed. */ getPerformanceEntryDeduplicationStrategy?: () => SpanDeduplicationStrategy<RelationSchemasT> reportErrorFn: ReportErrorFn<RelationSchemasT> reportWarningFn: ReportErrorFn<RelationSchemasT> /** * Whether to track tickId on spans. * Useful for grouping spans that were recorded in the same event loop tick. * If true, the tickId will be set on the span. * This enables finding the parent relative to the event-loop tick, * when using the `getParentSpan` function or the `parentSpanMatcher`. * Useful for creating hierarchies from React components or hooks, or attributing and propagating errors. */ enableTickTracking?: boolean /** * Sometimes a span is processed after the trace has started. * This setting defines how much older than the trace the span can be, and still be accepted into the trace. * Defaults to 100ms. */ acceptSpansStartedBeforeTraceStartThreshold?: number /** * A list of span attributes that should be inherited by * the children spans (propagated downwards). * This is useful for ensuring that certain attributes are available on all children spans, * for example, to ensure that `team` ownership information is available on descendant spans, * even if they didn't explicitly define it. * Note that a children span only inherits the attribute if it doesn't already have them defined. * * This inheritance occurs only after a trace is completed, * or when manually requested, once the parent spans are resolved. */ heritableSpanAttributes?: readonly string[] } export interface TraceManagerUtilities< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > extends TraceManagerConfig<RelationSchemasT> { /** * interrupts the active trace (if any) and replaces it with a new one * returns the new Trace */ replaceCurrentTrace: < const SelectedRelationNameT extends keyof RelationSchemasT, const VariantsT extends string, >( getNewTrace: () => Trace< SelectedRelationNameT, RelationSchemasT, VariantsT >, reason: TraceReplaceInterruptionReason, ) => Trace<SelectedRelationNameT, RelationSchemasT, VariantsT> onTraceEnd: ( trace: AllPossibleTraces<RelationSchemasT>, finalTransition: FinalTransition<RelationSchemasT>, traceRecording: | TraceRecording<keyof RelationSchemasT, RelationSchemasT> | undefined, ) => void getCurrentTrace: () => AllPossibleTraces<RelationSchemasT> | undefined onTraceConstructed: (trace: AllPossibleTraces<RelationSchemasT>) => void acceptSpansStartedBeforeTraceStartThreshold: number } export interface TraceUtilities< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > extends TraceManagerUtilities<RelationSchemasT> { performanceEntryDeduplicationStrategy?: SpanDeduplicationStrategy<RelationSchemasT> parentTraceRef: AllPossibleTraces<RelationSchemasT> | undefined } export interface TraceChildUtilities< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > extends TraceUtilities<RelationSchemasT> { parentTraceRef: AllPossibleTraces<RelationSchemasT> } export interface TraceDefinitionModifications< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > { additionalRequiredSpans?: readonly SpanMatch< SelectedRelationNameT, RelationSchemasT, VariantsT >[] additionalInterruptOnSpans?: readonly SpanMatch< SelectedRelationNameT, RelationSchemasT, VariantsT >[] additionalDebounceOnSpans?: readonly SpanMatch< SelectedRelationNameT, RelationSchemasT, VariantsT >[] } export interface TraceModifications< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > extends TraceDefinitionModifications< SelectedRelationNameT, RelationSchemasT, VariantsT > { relatedTo: MapSchemaToTypes<RelationSchemasT[SelectedRelationNameT]> attributes?: Attributes } type ErrorBehavior = 'error' | 'error-and-continue' | 'warn-and-continue' export interface TransitionDraftOptions { previouslyActivatedBehavior?: ErrorBehavior invalidRelatedToBehavior?: ErrorBehavior } export interface CaptureInteractiveConfig extends CPUIdleProcessorOptions { /** * How long to wait for CPU Idle before giving up and interrupting the trace. */ timeout?: number } export type LabelMatchingInputRecord< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > = Record< string, SpanMatch<SelectedRelationNameT, RelationSchemasT, VariantsT> > export type LabelMatchingFnsRecord< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > = Record< string, SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT> > export interface TraceVariant< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > extends TraceDefinitionModifications< SelectedRelationNameT, RelationSchemasT, VariantsT > { /** * How long before we give up and cancel the trace if the required spans have not been seen * In milliseconds. */ timeout: number } export interface PromoteSpanAttributesDefinition< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > { span: SpanMatch<SelectedRelationNameT, RelationSchemasT, VariantsT> attributes: string[] } /** * Definition of a trace that includes conditions on when to end, debounce, and interrupt. * The "input" version will be transformed into the standardized version internally, * converting all matchers into functions. */ export interface TraceDefinition< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, ComputedValueDefinitionsT extends { [K in keyof ComputedValueDefinitionsT]: ComputedValueDefinitionInput< NoInfer<SelectedRelationNameT>, RelationSchemasT, NoInfer<VariantsT>, // eslint-disable-next-line @typescript-eslint/no-explicit-any any > }, > { /** * The name of the trace. */ name: string type?: TraceType relationSchemaName: SelectedRelationNameT // TypeScript TODO: typing this so that the span labels are inferred? labelMatching?: LabelMatchingInputRecord< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT > /** * A list of trace names that instead of interrupting the current trace, * will be adopted as children of this trace. */ adoptAsChildren?: readonly string[] /** * This may include renders spans of components that have to be rendered with all data * to consider the operation as visually complete * this is close to the idea of "Largest Contentful Paint" * but rather than using "Largest" as a shorthand, * we're giving the power to the engineer to manually define * which parts of the product are "critical" or most important */ requiredSpans: ArrayWithAtLeastOneElement< SpanMatch<NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT> > debounceOnSpans?: ArrayWithAtLeastOneElement< SpanMatch<NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT> > interruptOnSpans?: ArrayWithAtLeastOneElement< SpanMatch<NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT> > /** * How long should we wait after the last required span (or debounced span). * in anticipation of more spans * @default DEFAULT_DEBOUNCE_DURATION (500) */ debounceWindow?: number /** * variants are used to describe slightly different versions of the same tracer * e.g. if a trace is started due to cold boot navigation, it may have a different timeout * than if it was started due to a user click * the trace might also lead to different places in the app * due to different conditions / options related to the action that the user is taking, * which is something that might be reflected by providing additional span requirements to that variant. * The key is the name of the variant, and the value is the configuration for that variant. * * We recommend naming the variant by using the following descriptor: * - `on_XYZ` - what caused the trace to start, or where it was started * - `till_XYZ` - where we ended up, or what else happened that led to the end of the trace * * You can do either one, or both. * Add variants whenever you want to be able to distinguish the trace data * based on different triggers or different contexts. * * For example: * - `on_submit_press` * - `on_cold_boot` * - `till_navigated_home` * - `till_logged_in` * - `on_submit_press_till_reloaded` * - `on_search_till_results` */ variants: { [VariantName in VariantsT]: TraceVariant< SelectedRelationNameT, RelationSchemasT, VariantsT > } /** * Indicates the operation should continue capturing events after the trace is complete, * until the page is considered fully interactive. * Provide 'true' for defaults, or a custom configuration object. */ captureInteractive?: boolean | CaptureInteractiveConfig /** * A list of span matchers that will suppress error status propagation to the trace level. * If a span with `status: error` matches any of these matchers, * its error status will not affect the overall trace status. */ suppressErrorStatusPropagationOnSpans?: readonly SpanMatch< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT >[] /** * A record of computed span definitions that will be converted to their final form. * The key is the name of the computed span. You can add more computed spans later using tracer.defineComputedSpan(). */ computedSpanDefinitions?: Record< string, ComputedSpanDefinitionInput< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT > > /** * A record of computed value definitions that will be converted to their final form. * The key is the name of the computed value. You can add more computed values later using tracer.defineComputedValue(). */ computedValueDefinitions?: ComputedValueDefinitionsT /** * Define attributes that should be promoted from the span to the trace level, along with the matchers for the spans. * In case of conflicts, last attribute wins. */ promoteSpanAttributes?: PromoteSpanAttributesDefinition< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT >[] } /** * Trace Definition with added fields and converting all matchers into functions. * Used internally by the TraceManager. */ export interface CompleteTraceDefinition< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > extends Omit< TraceDefinition<SelectedRelationNameT, RelationSchemasT, VariantsT, {}>, | 'computedSpanDefinitions' | 'computedValueDefinitions' | 'requiredSpans' | 'debounceOnSpans' | 'interruptOnSpans' > { computedSpanDefinitions: Record< string, ComputedSpanDefinition< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT > > computedValueDefinitions: Record< string, ComputedValueDefinition< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT > > relationSchema: NoInfer<RelationSchemasT[SelectedRelationNameT]> labelMatching?: LabelMatchingFnsRecord< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT > requiredSpans: readonly SpanMatcherFn< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT >[] debounceOnSpans?: readonly SpanMatcherFn< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT >[] interruptOnSpans?: readonly SpanMatcherFn< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT >[] suppressErrorStatusPropagationOnSpans?: readonly SpanMatcherFn< NoInfer<SelectedRelationNameT>, RelationSchemasT, VariantsT >[] } /** * Strategy for deduplicating performance entries */ export interface SpanDeduplicationStrategy< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, > { /** * Returns an existing span annotation if the span should be considered a duplicate */ findDuplicate: ( span: Span<RelationSchemasT>, recordedItems: Map<string, SpanAndAnnotation<RelationSchemasT>>, ) => SpanAndAnnotation<RelationSchemasT> | undefined /** * Called when a span is recorded to update deduplication state */ recordSpan: (spanAndAnnotation: SpanAndAnnotation<RelationSchemasT>) => void /** * Called when trace recording is complete to clean up any deduplication state */ reset: () => void /** * Selects which span should be used when a duplicate is found. * @returns the span that should be used in the annotation */ selectPreferredSpan: ( existingSpan: Span<RelationSchemasT>, newSpan: Span<RelationSchemasT>, ) => Span<RelationSchemasT> } export type SpecialStartToken = 'operation-start' export type SpecialEndToken = 'operation-end' | 'interactive' export type SpecialToken = SpecialStartToken | SpecialEndToken /** * Definition of custom spans */ export interface ComputedSpanDefinition< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > { /** * the *first* span matching the condition that will be considered as the start of the computed span * if you want the *last* matching span, use `nthMatch: -1` */ startSpan: | SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT> | SpecialStartToken /** * the *first* span matching the condition that will be considered as the end of the computed span * if you want the *last* matching span, use `nthMatch: -1` */ endSpan: | SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT> | SpecialEndToken /** * If true, we will attempt to compute the span even if the trace was interrupted. * Alternatively, specify an array of InterruptionReasons in which the span should be computed. */ // TODO: forceCompute: boolean | InterruptionReason[] } /** * Definition of custom values */ export interface ComputedValueDefinition< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > { matches: SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT>[] /** if returns undefined, will not report the computed value */ computeValueFromMatches: NoInfer< ( ...matchers: (readonly SpanAndAnnotation<RelationSchemasT>[])[] ) => number | string | boolean | undefined > /** * If true, we will attempt to compute the span even if the trace was interrupted. * Alternatively, specify an array of InterruptionReasons in which the span should be computed. */ // TODO: forceCompute: boolean | InterruptionReason[] } /** * Definition of custom spans input */ export interface ComputedSpanDefinitionInput< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > { startSpan: | SpanMatch<SelectedRelationNameT, RelationSchemasT, VariantsT> | 'operation-start' endSpan: | SpanMatch<SelectedRelationNameT, RelationSchemasT, VariantsT> | 'operation-end' | 'interactive' } /** * Definition of custom values input */ export interface ComputedValueDefinitionInput< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, MatchersT extends NoInfer< SpanMatch<SelectedRelationNameT, RelationSchemasT, VariantsT> >[], > { matches: [...MatchersT] computeValueFromMatches: NoInfer< ( ...matches: MapTuple< MatchersT, readonly SpanAndAnnotation<RelationSchemasT>[] > ) => number | string | boolean | undefined > } export type DeriveRelationsFromPerformanceEntryFn<RelationSchemasT> = ( entry: PerformanceEntry, ) => RelationsOnASpan<RelationSchemasT> | undefined export interface DraftTraceContext< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > extends TraceContext<SelectedRelationNameT, RelationSchemasT, VariantsT> { readonly input: DraftTraceInput< RelationSchemasT[SelectedRelationNameT], VariantsT > } export type AllPossibleTraceContexts< RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > = { [SelectedRelationNameT in keyof RelationSchemasT]: DraftTraceContext< SelectedRelationNameT, RelationSchemasT, VariantsT > }[keyof RelationSchemasT] export interface TraceContext< SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, VariantsT extends string, > { readonly definition: CompleteTraceDefinition< SelectedRelationNameT, RelationSchemasT, VariantsT > readonly input: | ActiveTraceInput<RelationSchemasT[SelectedRelationNameT], VariantsT> | DraftTraceInput<RelationSchemasT[SelectedRelationNameT], VariantsT> // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style readonly recordedItemsByLabel: { readonly [label: string]: ReadonlySet<SpanAndAnnotation<RelationSchemasT>> } readonly recordedItems: ReadonlyMap< string, SpanAndAnnotation<RelationSchemasT> > }