@zendesk/retrace
Version:
define and capture Product Operation Traces along with computed metrics with an optional friendly React beacon API
307 lines (306 loc) • 19.3 kB
TypeScript
import type { Observable } from 'rxjs';
import { Subject } from 'rxjs';
import type { AddSpanToRecordingEvent, DefinitionModifiedEvent, RequiredSpanSeenEvent, StateTransitionEvent } from './debugTypes';
import { type CPUIdleLongTaskProcessor, type PerformanceEntryLike } from './firstCPUIdle';
import { type SpanMatcherFn } from './matchSpan';
import type { SpanAndAnnotation, SpanAnnotationRecord } from './spanAnnotationTypes';
import type { ActiveTraceConfig, DraftTraceInput, Span } from './spanTypes';
import type { TraceRecording } from './traceRecordingTypes';
import type { CompleteTraceDefinition, DraftTraceContext, RelationSchemasBase, TraceContext, TraceDefinitionModifications, TraceInterruptionReason, TraceManagerUtilities, TraceModifications, TransitionDraftOptions } from './types';
import type { DistributiveOmit, StateHandlerPayloads } from './typeUtils';
declare const INITIAL_STATE = "draft";
type InitialTraceState = typeof INITIAL_STATE;
export type NonTerminalTraceStates = InitialTraceState | 'active' | 'debouncing' | 'waiting-for-interactive';
export declare const TERMINAL_STATES: readonly ["interrupted", "complete"];
type TerminalTraceStates = (typeof TERMINAL_STATES)[number];
export type TraceStates = NonTerminalTraceStates | TerminalTraceStates;
export declare const isTerminalState: (state: TraceStates) => state is TerminalTraceStates;
interface OnEnterActive {
transitionToState: 'active';
transitionFromState: NonTerminalTraceStates;
}
interface OnEnterInterrupted<RelationSchemasT> {
transitionToState: 'interrupted';
transitionFromState: NonTerminalTraceStates;
interruptionReason: TraceInterruptionReason;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
}
interface OnEnterComplete<RelationSchemasT> {
transitionToState: 'complete';
transitionFromState: NonTerminalTraceStates;
interruptionReason?: TraceInterruptionReason;
cpuIdleSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
completeSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRequiredSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
}
export type FinalTransition<RelationSchemasT> = OnEnterInterrupted<RelationSchemasT> | OnEnterComplete<RelationSchemasT>;
interface OnEnterWaitingForInteractive {
transitionToState: 'waiting-for-interactive';
transitionFromState: NonTerminalTraceStates;
}
interface OnEnterDebouncing {
transitionToState: 'debouncing';
transitionFromState: NonTerminalTraceStates;
}
export type OnEnterStatePayload<RelationSchemasT> = OnEnterActive | OnEnterInterrupted<RelationSchemasT> | OnEnterComplete<RelationSchemasT> | OnEnterDebouncing | OnEnterWaitingForInteractive;
export type Transition<RelationSchemasT> = DistributiveOmit<OnEnterStatePayload<RelationSchemasT>, 'transitionFromState'>;
export type States<SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT, VariantsT extends string> = TraceStateMachine<SelectedRelationNameT, RelationSchemasT, VariantsT>['states'];
interface TraceStateMachineSideEffectHandlers<RelationSchemasT> {
readonly addSpanToRecording: (spanAndAnnotation: SpanAndAnnotation<RelationSchemasT>) => void;
readonly prepareAndEmitRecording: (transition: OnEnterStatePayload<RelationSchemasT>) => void;
}
type EntryType<RelationSchemasT> = PerformanceEntryLike & {
entry: SpanAndAnnotation<RelationSchemasT>;
};
interface StateMachineContext<SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT, VariantsT extends string> extends DraftTraceContext<SelectedRelationNameT, RelationSchemasT, VariantsT> {
sideEffectFns: TraceStateMachineSideEffectHandlers<RelationSchemasT>;
eventSubjects: {
'state-transition': Subject<StateTransitionEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
'required-span-seen': Subject<RequiredSpanSeenEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
'add-span-to-recording': Subject<AddSpanToRecordingEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
'definition-modified': Subject<DefinitionModifiedEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
};
}
type DeadlineType = 'global' | 'debounce' | 'interactive' | 'next-quiet-window';
export declare class TraceStateMachine<SelectedRelationNameT extends keyof RelationSchemasT, RelationSchemasT, const VariantsT extends string> {
#private;
constructor(context: StateMachineContext<SelectedRelationNameT, RelationSchemasT, VariantsT>);
readonly successfullyMatchedRequiredSpanMatchers: Set<SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
get sideEffectFns(): TraceStateMachineSideEffectHandlers<RelationSchemasT>;
currentState: TraceStates;
/** the span that ended at the furthest point in time */
lastRelevant: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRequiredSpan: SpanAndAnnotation<RelationSchemasT> | undefined;
/** it is set once the LRS value is established */
completeSpan: SpanAndAnnotation<RelationSchemasT> | undefined;
cpuIdleLongTaskProcessor: CPUIdleLongTaskProcessor<EntryType<RelationSchemasT>> | undefined;
nextDeadlineRef: ReturnType<typeof setTimeout> | undefined;
setDeadline(deadlineType: Exclude<DeadlineType, 'global'>, deadlineEpoch: number): void;
setGlobalDeadline(deadline: number): void;
clearDeadline(): void;
/**
* while debouncing, we need to buffer any spans that come in so they can be re-processed
* once we transition to the 'waiting-for-interactive' state
* otherwise we might miss out on spans that are relevant to calculating the interactive
*
* if we have long tasks before FMP, we want to use them as a potential grouping post FMP.
*/
debouncingSpanBuffer: SpanAndAnnotation<RelationSchemasT>[];
readonly states: {
draft: {
onEnterState: () => void;
onMakeActive: () => {
transitionToState: "active";
};
onProcessSpan: (spanAndAnnotation: SpanAndAnnotation<RelationSchemasT>) => {
transitionToState: "interrupted";
interruptionReason: "timeout";
lastRelevantSpanAndAnnotation: undefined;
} | {
transitionToState: "interrupted";
interruptionReason: "matched-on-interrupt" | "matched-on-required-span-with-error";
lastRelevantSpanAndAnnotation: undefined;
} | undefined;
onInterrupt: (reason: TraceInterruptionReason) => {
transitionToState: "interrupted";
interruptionReason: TraceInterruptionReason;
lastRelevantSpanAndAnnotation: undefined;
};
onDeadline: (deadlineType: DeadlineType) => {
transitionToState: "interrupted";
interruptionReason: "timeout";
lastRelevantSpanAndAnnotation: undefined;
} | undefined;
};
active: {
onEnterState: (_transition: OnEnterActive) => Transition<RelationSchemasT> | undefined;
onProcessSpan: (spanAndAnnotation: SpanAndAnnotation<RelationSchemasT>) => {
transitionToState: "interrupted";
interruptionReason: "timeout";
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
} | {
transitionToState: "interrupted";
interruptionReason: "matched-on-interrupt" | "matched-on-required-span-with-error";
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
} | {
transitionToState: "debouncing";
interruptionReason?: undefined;
lastRelevantSpanAndAnnotation?: undefined;
} | undefined;
onInterrupt: (reason: TraceInterruptionReason) => {
transitionToState: "interrupted";
interruptionReason: TraceInterruptionReason;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
};
onDeadline: (deadlineType: DeadlineType) => {
transitionToState: "interrupted";
interruptionReason: "timeout";
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
} | undefined;
};
debouncing: {
onEnterState: (_payload: OnEnterDebouncing) => {
transitionToState: "interrupted";
interruptionReason: "invalid-state-transition";
lastRelevantSpanAndAnnotation: undefined;
} | {
transitionToState: "waiting-for-interactive";
interruptionReason?: undefined;
lastRelevantSpanAndAnnotation?: undefined;
} | undefined;
onDeadline: (deadlineType: DeadlineType) => {
transitionToState: "interrupted";
interruptionReason: "timeout";
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
} | {
transitionToState: "waiting-for-interactive";
interruptionReason?: undefined;
lastRelevantSpanAndAnnotation?: undefined;
} | undefined;
onProcessSpan: (spanAndAnnotation: SpanAndAnnotation<RelationSchemasT>) => {
transitionToState: "interrupted";
interruptionReason: "timeout";
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
} | {
transitionToState: "waiting-for-interactive";
interruptionReason?: undefined;
lastRelevantSpanAndAnnotation?: undefined;
} | {
transitionToState: "interrupted";
interruptionReason: "idle-component-no-longer-idle";
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
} | undefined;
onInterrupt: (reason: TraceInterruptionReason) => {
transitionToState: "interrupted";
interruptionReason: TraceInterruptionReason;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
};
};
'waiting-for-interactive': {
onEnterState: (_payload: OnEnterWaitingForInteractive) => Transition<RelationSchemasT> | undefined;
onDeadline: (deadlineType: DeadlineType) => {
transitionToState: "complete";
interruptionReason: "timeout";
completeSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRequiredSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
cpuIdleSpanAndAnnotation: undefined;
} | {
transitionToState: "complete";
lastRequiredSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
completeSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
cpuIdleSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT>;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
interruptionReason?: undefined;
} | undefined;
onProcessSpan: (spanAndAnnotation: SpanAndAnnotation<RelationSchemasT>) => {
transitionToState: "complete";
lastRequiredSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
completeSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
cpuIdleSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT>;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
interruptionReason?: undefined;
} | {
transitionToState: "complete";
interruptionReason: "timeout";
lastRequiredSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
completeSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
cpuIdleSpanAndAnnotation: undefined;
} | {
transitionToState: "complete";
interruptionReason: "waiting-for-interactive-timeout";
lastRequiredSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
completeSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
cpuIdleSpanAndAnnotation: undefined;
} | {
transitionToState: "complete";
interruptionReason: "matched-on-interrupt" | "matched-on-required-span-with-error";
lastRequiredSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
completeSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
cpuIdleSpanAndAnnotation: undefined;
} | undefined;
onInterrupt: (reason: TraceInterruptionReason) => {
transitionToState: "complete";
interruptionReason: TraceInterruptionReason;
lastRequiredSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
completeSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
lastRelevantSpanAndAnnotation: SpanAndAnnotation<RelationSchemasT> | undefined;
cpuIdleSpanAndAnnotation: undefined;
};
};
interrupted: {
onEnterState: (transition: OnEnterInterrupted<RelationSchemasT>) => void;
};
complete: {
onEnterState: (transition: OnEnterComplete<RelationSchemasT>) => void;
};
};
/**
* @returns the last OnEnterState event if a transition was made
*/
emit<EventName extends keyof StateHandlerPayloads<SelectedRelationNameT, RelationSchemasT, VariantsT>>(event: EventName, payload: StateHandlerPayloads<SelectedRelationNameT, RelationSchemasT, VariantsT>[EventName]): OnEnterStatePayload<RelationSchemasT> | undefined;
}
export declare class Trace<const SelectedRelationNameT extends keyof RelationSchemasT, const RelationSchemasT extends RelationSchemasBase<RelationSchemasT>, const VariantsT extends string> implements TraceContext<SelectedRelationNameT, RelationSchemasT, VariantsT> {
readonly sourceDefinition: CompleteTraceDefinition<SelectedRelationNameT, RelationSchemasT, VariantsT>;
/** the final, mutable definition of this specific trace */
definition: CompleteTraceDefinition<SelectedRelationNameT, RelationSchemasT, VariantsT>;
wasActivated: boolean;
get activeInput(): ActiveTraceConfig<SelectedRelationNameT, RelationSchemasT, VariantsT>;
set activeInput(value: ActiveTraceConfig<SelectedRelationNameT, RelationSchemasT, VariantsT>);
input: DraftTraceInput<RelationSchemasT[SelectedRelationNameT], VariantsT>;
readonly traceUtilities: TraceManagerUtilities<RelationSchemasT>;
get isDraft(): boolean;
recordedItems: Set<SpanAndAnnotation<RelationSchemasT>>;
occurrenceCounters: Map<string, number>;
processedPerformanceEntries: WeakMap<PerformanceEntry, SpanAndAnnotation<RelationSchemasT>>;
persistedDefinitionModifications: Set<TraceDefinitionModifications<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
readonly recordedItemsByLabel: {
[label: string]: SpanAndAnnotation<RelationSchemasT>[];
};
stateMachine: TraceStateMachine<SelectedRelationNameT, RelationSchemasT, VariantsT>;
eventSubjects: {
'state-transition': Subject<StateTransitionEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
'required-span-seen': Subject<RequiredSpanSeenEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
'add-span-to-recording': Subject<AddSpanToRecordingEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
'definition-modified': Subject<DefinitionModifiedEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
};
when(event: 'state-transition'): Observable<StateTransitionEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
when(event: 'required-span-seen'): Observable<RequiredSpanSeenEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
when(event: 'add-span-to-recording'): Observable<AddSpanToRecordingEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
when(event: 'definition-modified'): Observable<DefinitionModifiedEvent<SelectedRelationNameT, RelationSchemasT, VariantsT>>;
constructor(data: {
definition: CompleteTraceDefinition<SelectedRelationNameT, RelationSchemasT, VariantsT>;
input: DraftTraceInput<RelationSchemasT[SelectedRelationNameT], VariantsT>;
definitionModifications?: TraceDefinitionModifications<SelectedRelationNameT, RelationSchemasT, VariantsT>;
traceUtilities: TraceManagerUtilities<RelationSchemasT>;
} | {
importFrom: Trace<SelectedRelationNameT, RelationSchemasT, VariantsT>;
definitionModifications: TraceDefinitionModifications<SelectedRelationNameT, RelationSchemasT, VariantsT>;
});
/**
* all requiredSpans implicitly interrupt the trace if they error, unless explicitly ignored
*/
mapRequiredSpanMatchersToInterruptOnMatchers(requiredSpans: readonly SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT>[]): readonly SpanMatcherFn<SelectedRelationNameT, RelationSchemasT, VariantsT>[];
sideEffectFns: TraceStateMachineSideEffectHandlers<RelationSchemasT>;
onEnd(traceRecording: TraceRecording<SelectedRelationNameT, RelationSchemasT>): void;
interrupt(reason: TraceInterruptionReason): void;
transitionDraftToActive(inputAndDefinitionModifications: TraceModifications<SelectedRelationNameT, RelationSchemasT, VariantsT>, { previouslyActivatedBehavior, invalidRelatedToBehavior, }?: TransitionDraftOptions): void;
/**
* The additions to the definition may come from either the variant at transition from draft to active
* @param definitionModifications
*/
private applyDefinitionModifications;
/**
* This is used for importing spans when recreating a Trace from another Trace
* if the definition was modified
*/
private replayItems;
processSpan(span: Span<RelationSchemasT>): SpanAnnotationRecord | undefined;
private getSpanLabels;
}
export type AllPossibleTraces<RelationSchemasT extends RelationSchemasBase<RelationSchemasT>> = Trace<any, RelationSchemasT, any>;
export {};