autotel
Version:
Write Once, Observe Anywhere
300 lines (296 loc) • 9.93 kB
text/typescript
import { Link, SpanContext } from '@opentelemetry/api';
import { T as TraceContext } from './trace-context-DbGKd1Rn.cjs';
/**
* Webhook and callback tracing with the "Parking Lot" pattern
*
* When initiating async operations that return hours/days later (webhooks,
* payment callbacks, human approvals), you can't keep a span open. This module
* provides utilities to "park" trace context and retrieve it when callbacks arrive.
*
* @example Stripe payment webhook
* ```typescript
* import { createParkingLot, InMemoryTraceContextStore } from 'autotel/webhook';
*
* const parkingLot = createParkingLot({
* store: new InMemoryTraceContextStore(),
* defaultTTLMs: 24 * 60 * 60 * 1000, // 24 hours
* });
*
* // When initiating payment
* export const initiatePayment = trace(ctx => async (orderId: string) => {
* await parkingLot.park(`payment:${orderId}`, { orderId });
* await stripeClient.createPaymentIntent({ metadata: { orderId } });
* });
*
* // When Stripe webhook arrives (hours later)
* export const handleStripeWebhook = parkingLot.traceCallback({
* name: 'stripe.webhook.payment_intent.succeeded',
* correlationKeyFrom: (event) => `payment:${event.data.object.metadata.orderId}`,
* })(ctx => async (event: Stripe.Event) => {
* // ctx.parkedContext contains the original trace context
* // ctx.elapsedMs shows time since payment was initiated
* await fulfillOrder(event.data.object);
* });
* ```
*
* @module
*/
/**
* Stored trace context for parking lot pattern
*/
interface StoredTraceContext {
/** Trace ID from the original span */
traceId: string;
/** Span ID from the original span */
spanId: string;
/** Trace flags (sampling decision) */
traceFlags: number;
/** When the context was parked */
parkedAt: number;
/** Optional TTL in milliseconds */
ttlMs?: number;
/** User-provided metadata */
metadata?: Record<string, string>;
}
/**
* Interface for trace context storage backends
*
* Implement this interface to use different storage backends (Redis, DynamoDB, etc.)
*/
interface TraceContextStore {
/**
* Save trace context with a correlation key
*
* @param key - Unique correlation key (e.g., "payment:order-123")
* @param context - The trace context to store
*/
save(key: string, context: StoredTraceContext): Promise<void>;
/**
* Load trace context by correlation key
*
* @param key - The correlation key used when parking
* @returns The stored context, or null if not found/expired
*/
load(key: string): Promise<StoredTraceContext | null>;
/**
* Delete trace context by correlation key
*
* @param key - The correlation key to delete
*/
delete(key: string): Promise<void>;
}
/**
* Configuration for creating a parking lot
*/
interface ParkingLotConfig {
/** Storage backend for parked contexts */
store: TraceContextStore;
/** Default TTL in milliseconds (default: 24 hours) */
defaultTTLMs?: number;
/** Prefix for all correlation keys (default: "parkingLot:") */
keyPrefix?: string;
/** Whether to auto-delete after retrieval (default: true) */
autoDeleteOnRetrieve?: boolean;
/** Callback when context expires or is not found */
onMiss?: (correlationKey: string) => void;
}
/**
* Configuration for traceCallback wrapper
*/
interface CallbackConfig {
/** Span name for the callback handler */
name: string;
/**
* Extract correlation key from callback arguments
*
* @example
* ```typescript
* correlationKeyFrom: (event) => `payment:${event.data.orderId}`
* ```
*/
correlationKeyFrom: (args: unknown[]) => string;
/** Additional span attributes */
attributes?: Record<string, string | number | boolean>;
/** Whether to fail if parked context is not found (default: false) */
requireParkedContext?: boolean;
}
/**
* Extended context for callback handlers
*/
interface CallbackContext extends TraceContext {
/** The retrieved parked context, if found */
parkedContext: StoredTraceContext | null;
/** Time elapsed since context was parked (ms), or null if not found */
elapsedMs: number | null;
/** The correlation key used for retrieval */
correlationKey: string;
}
/**
* The parking lot instance
*/
interface ParkingLot {
/**
* Park current trace context before initiating async operation
*
* Call this before sending a webhook, initiating a payment, or starting
* any operation that will complete via callback.
*
* @param correlationKey - Unique key to retrieve context later (e.g., "payment:order-123")
* @param metadata - Optional metadata to store with the context
* @returns The correlation key (with prefix applied)
*
* @example
* ```typescript
* await parkingLot.park(`payment:${orderId}`, {
* customerId: customer.id,
* amount: payment.amount.toString(),
* });
* ```
*/
park(correlationKey: string, metadata?: Record<string, string>): Promise<string>;
/**
* Retrieve parked context when callback arrives
*
* @param correlationKey - The key used when parking
* @returns The stored context, or null if not found/expired
*/
retrieve(correlationKey: string): Promise<StoredTraceContext | null>;
/**
* Wrap a callback handler with automatic context retrieval and linking
*
* Creates a traced function that:
* 1. Extracts correlation key from arguments
* 2. Retrieves parked context from storage
* 3. Creates a span link to the original trace
* 4. Provides elapsed time since parking
*
* @param config - Callback configuration
* @returns Factory function for the callback handler
*
* @example
* ```typescript
* export const handleWebhook = parkingLot.traceCallback({
* name: 'webhook.payment.completed',
* correlationKeyFrom: (args) => `payment:${args[0].orderId}`,
* })(ctx => async (event) => {
* console.log(`Payment completed after ${ctx.elapsedMs}ms`);
* await processPayment(event);
* });
* ```
*/
traceCallback<TArgs extends unknown[], TReturn>(config: CallbackConfig): (fnFactory: (ctx: CallbackContext) => (...args: TArgs) => Promise<TReturn>) => (...args: TArgs) => Promise<TReturn>;
/**
* Manually create a span link from stored context
*
* Useful when you need more control over span creation.
*
* @param storedContext - The stored trace context
* @returns A span link that can be added to a span
*/
createLink(storedContext: StoredTraceContext): Link;
/**
* Check if a parked context exists (without retrieving/deleting it)
*
* @param correlationKey - The key to check
* @returns True if context exists and hasn't expired
*/
exists(correlationKey: string): Promise<boolean>;
}
/**
* In-memory trace context store
*
* Useful for testing and development. For production, use a persistent
* store like Redis or DynamoDB.
*
* @example
* ```typescript
* const store = new InMemoryTraceContextStore();
* const parkingLot = createParkingLot({ store });
* ```
*/
declare class InMemoryTraceContextStore implements TraceContextStore {
private options;
private store;
private cleanupInterval;
constructor(options?: {
/** Cleanup interval in ms (default: 60000) */
cleanupIntervalMs?: number;
});
save(key: string, context: StoredTraceContext): Promise<void>;
load(key: string): Promise<StoredTraceContext | null>;
delete(key: string): Promise<void>;
/**
* Get number of stored contexts (for testing)
*/
get size(): number;
/**
* Clear all stored contexts (for testing)
*/
clear(): void;
/**
* Stop the cleanup interval
*/
destroy(): void;
private cleanup;
}
/**
* Create a parking lot for trace context storage and retrieval
*
* @param config - Parking lot configuration
* @returns A parking lot instance
*
* @example Basic usage
* ```typescript
* const parkingLot = createParkingLot({
* store: new InMemoryTraceContextStore(),
* defaultTTLMs: 24 * 60 * 60 * 1000, // 24 hours
* });
* ```
*
* @example With Redis store
* ```typescript
* class RedisTraceContextStore implements TraceContextStore {
* constructor(private redis: Redis) {}
*
* async save(key: string, context: StoredTraceContext) {
* const ttlSeconds = context.ttlMs ? Math.ceil(context.ttlMs / 1000) : 86400;
* await this.redis.setex(key, ttlSeconds, JSON.stringify(context));
* }
*
* async load(key: string) {
* const data = await this.redis.get(key);
* return data ? JSON.parse(data) : null;
* }
*
* async delete(key: string) {
* await this.redis.del(key);
* }
* }
*
* const parkingLot = createParkingLot({
* store: new RedisTraceContextStore(redis),
* });
* ```
*/
declare function createParkingLot(config: ParkingLotConfig): ParkingLot;
/**
* Create a correlation key from multiple parts
*
* @param parts - Key parts to join
* @returns A correlation key string
*
* @example
* ```typescript
* const key = createCorrelationKey('payment', orderId, 'stripe');
* // Returns: "payment:order-123:stripe"
* ```
*/
declare function createCorrelationKey(...parts: (string | number)[]): string;
/**
* Extract span context from stored context for manual linking
*
* @param storedContext - The stored trace context
* @returns SpanContext compatible object
*/
declare function toSpanContext(storedContext: StoredTraceContext): SpanContext;
export { type CallbackConfig, type CallbackContext, InMemoryTraceContextStore, type ParkingLot, type ParkingLotConfig, type StoredTraceContext, type TraceContextStore, createCorrelationKey, createParkingLot, toSpanContext };