autotel
Version:
Write Once, Observe Anywhere
1 lines • 22.9 kB
Source Map (JSON)
{"version":3,"file":"webhook.cjs","names":["otelTrace","trace","SpanKind"],"sources":["../src/webhook.ts"],"sourcesContent":["/**\n * Webhook and callback tracing with the \"Parking Lot\" pattern\n *\n * When initiating async operations that return hours/days later (webhooks,\n * payment callbacks, human approvals), you can't keep a span open. This module\n * provides utilities to \"park\" trace context and retrieve it when callbacks arrive.\n *\n * @example Stripe payment webhook\n * ```typescript\n * import { createParkingLot, InMemoryTraceContextStore } from 'autotel/webhook';\n *\n * const parkingLot = createParkingLot({\n * store: new InMemoryTraceContextStore(),\n * defaultTTLMs: 24 * 60 * 60 * 1000, // 24 hours\n * });\n *\n * // When initiating payment\n * export const initiatePayment = trace(ctx => async (orderId: string) => {\n * await parkingLot.park(`payment:${orderId}`, { orderId });\n * await stripeClient.createPaymentIntent({ metadata: { orderId } });\n * });\n *\n * // When Stripe webhook arrives (hours later)\n * export const handleStripeWebhook = parkingLot.traceCallback({\n * name: 'stripe.webhook.payment_intent.succeeded',\n * correlationKeyFrom: (event) => `payment:${event.data.object.metadata.orderId}`,\n * })(ctx => async (event: Stripe.Event) => {\n * // ctx.parkedContext contains the original trace context\n * // ctx.elapsedMs shows time since payment was initiated\n * await fulfillOrder(event.data.object);\n * });\n * ```\n *\n * @module\n */\n\nimport { SpanKind, trace as otelTrace } from '@opentelemetry/api';\nimport type { SpanContext, Link } from '@opentelemetry/api';\nimport { emitCorrelatedEvent } from './correlated-events';\nimport { trace } from './functional';\nimport type { AttributeValue, TraceContext } from './trace-context';\nimport { recordStructuredError } from './structured-error';\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Stored trace context for parking lot pattern\n */\nexport interface StoredTraceContext {\n /** Trace ID from the original span */\n traceId: string;\n\n /** Span ID from the original span */\n spanId: string;\n\n /** Trace flags (sampling decision) */\n traceFlags: number;\n\n /** When the context was parked */\n parkedAt: number;\n\n /** Optional TTL in milliseconds */\n ttlMs?: number;\n\n /** User-provided metadata */\n metadata?: Record<string, string>;\n}\n\n/**\n * Interface for trace context storage backends\n *\n * Implement this interface to use different storage backends (Redis, DynamoDB, etc.)\n */\nexport interface TraceContextStore {\n /**\n * Save trace context with a correlation key\n *\n * @param key - Unique correlation key (e.g., \"payment:order-123\")\n * @param context - The trace context to store\n */\n save(key: string, context: StoredTraceContext): Promise<void>;\n\n /**\n * Load trace context by correlation key\n *\n * @param key - The correlation key used when parking\n * @returns The stored context, or null if not found/expired\n */\n load(key: string): Promise<StoredTraceContext | null>;\n\n /**\n * Delete trace context by correlation key\n *\n * @param key - The correlation key to delete\n */\n delete(key: string): Promise<void>;\n}\n\n/**\n * Configuration for creating a parking lot\n */\nexport interface ParkingLotConfig {\n /** Storage backend for parked contexts */\n store: TraceContextStore;\n\n /** Default TTL in milliseconds (default: 24 hours) */\n defaultTTLMs?: number;\n\n /** Prefix for all correlation keys (default: \"parkingLot:\") */\n keyPrefix?: string;\n\n /** Whether to auto-delete after retrieval (default: true) */\n autoDeleteOnRetrieve?: boolean;\n\n /** Callback when context expires or is not found */\n onMiss?: (correlationKey: string) => void;\n}\n\n/**\n * Configuration for traceCallback wrapper\n */\nexport interface CallbackConfig {\n /** Span name for the callback handler */\n name: string;\n\n /**\n * Extract correlation key from callback arguments\n *\n * @example\n * ```typescript\n * correlationKeyFrom: (event) => `payment:${event.data.orderId}`\n * ```\n */\n correlationKeyFrom: (args: unknown[]) => string;\n\n /** Additional span attributes */\n attributes?: Record<string, string | number | boolean>;\n\n /** Whether to fail if parked context is not found (default: false) */\n requireParkedContext?: boolean;\n}\n\n/**\n * Extended context for callback handlers\n */\nexport interface CallbackContext extends TraceContext {\n /** The retrieved parked context, if found */\n parkedContext: StoredTraceContext | null;\n\n /** Time elapsed since context was parked (ms), or null if not found */\n elapsedMs: number | null;\n\n /** The correlation key used for retrieval */\n correlationKey: string;\n}\n\n/**\n * The parking lot instance\n */\nexport interface ParkingLot {\n /**\n * Park current trace context before initiating async operation\n *\n * Call this before sending a webhook, initiating a payment, or starting\n * any operation that will complete via callback.\n *\n * @param correlationKey - Unique key to retrieve context later (e.g., \"payment:order-123\")\n * @param metadata - Optional metadata to store with the context\n * @returns The correlation key (with prefix applied)\n *\n * @example\n * ```typescript\n * await parkingLot.park(`payment:${orderId}`, {\n * customerId: customer.id,\n * amount: payment.amount.toString(),\n * });\n * ```\n */\n park(\n correlationKey: string,\n metadata?: Record<string, string>,\n ): Promise<string>;\n\n /**\n * Retrieve parked context when callback arrives\n *\n * @param correlationKey - The key used when parking\n * @returns The stored context, or null if not found/expired\n */\n retrieve(correlationKey: string): Promise<StoredTraceContext | null>;\n\n /**\n * Wrap a callback handler with automatic context retrieval and linking\n *\n * Creates a traced function that:\n * 1. Extracts correlation key from arguments\n * 2. Retrieves parked context from storage\n * 3. Creates a span link to the original trace\n * 4. Provides elapsed time since parking\n *\n * @param config - Callback configuration\n * @returns Factory function for the callback handler\n *\n * @example\n * ```typescript\n * export const handleWebhook = parkingLot.traceCallback({\n * name: 'webhook.payment.completed',\n * correlationKeyFrom: (args) => `payment:${args[0].orderId}`,\n * })(ctx => async (event) => {\n * console.log(`Payment completed after ${ctx.elapsedMs}ms`);\n * await processPayment(event);\n * });\n * ```\n */\n traceCallback<TArgs extends unknown[], TReturn>(\n config: CallbackConfig,\n ): (\n fnFactory: (ctx: CallbackContext) => (...args: TArgs) => Promise<TReturn>,\n ) => (...args: TArgs) => Promise<TReturn>;\n\n /**\n * Manually create a span link from stored context\n *\n * Useful when you need more control over span creation.\n *\n * @param storedContext - The stored trace context\n * @returns A span link that can be added to a span\n */\n createLink(storedContext: StoredTraceContext): Link;\n\n /**\n * Check if a parked context exists (without retrieving/deleting it)\n *\n * @param correlationKey - The key to check\n * @returns True if context exists and hasn't expired\n */\n exists(correlationKey: string): Promise<boolean>;\n}\n\n// ============================================================================\n// In-Memory Store (for testing and development)\n// ============================================================================\n\n/**\n * In-memory trace context store\n *\n * Useful for testing and development. For production, use a persistent\n * store like Redis or DynamoDB.\n *\n * @example\n * ```typescript\n * const store = new InMemoryTraceContextStore();\n * const parkingLot = createParkingLot({ store });\n * ```\n */\nexport class InMemoryTraceContextStore implements TraceContextStore {\n private store = new Map<string, StoredTraceContext>();\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n\n constructor(\n private options: {\n /** Cleanup interval in ms (default: 60000) */\n cleanupIntervalMs?: number;\n } = {},\n ) {\n // Start periodic cleanup of expired entries\n const cleanupMs = options.cleanupIntervalMs ?? 60_000;\n if (cleanupMs > 0) {\n this.cleanupInterval = setInterval(() => this.cleanup(), cleanupMs);\n // Don't prevent process exit\n if (this.cleanupInterval.unref) {\n this.cleanupInterval.unref();\n }\n }\n }\n\n async save(key: string, context: StoredTraceContext): Promise<void> {\n this.store.set(key, context);\n }\n\n async load(key: string): Promise<StoredTraceContext | null> {\n const context = this.store.get(key);\n if (!context) {\n return null;\n }\n\n // Check TTL expiration\n if (context.ttlMs) {\n const age = Date.now() - context.parkedAt;\n if (age > context.ttlMs) {\n this.store.delete(key);\n return null;\n }\n }\n\n return context;\n }\n\n async delete(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n /**\n * Get number of stored contexts (for testing)\n */\n get size(): number {\n return this.store.size;\n }\n\n /**\n * Clear all stored contexts (for testing)\n */\n clear(): void {\n this.store.clear();\n }\n\n /**\n * Stop the cleanup interval\n */\n destroy(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n }\n\n private cleanup(): void {\n const now = Date.now();\n for (const [key, context] of this.store.entries()) {\n if (context.ttlMs) {\n const age = now - context.parkedAt;\n if (age > context.ttlMs) {\n this.store.delete(key);\n }\n }\n }\n }\n}\n\n// ============================================================================\n// Parking Lot Factory\n// ============================================================================\n\n/**\n * Create a parking lot for trace context storage and retrieval\n *\n * @param config - Parking lot configuration\n * @returns A parking lot instance\n *\n * @example Basic usage\n * ```typescript\n * const parkingLot = createParkingLot({\n * store: new InMemoryTraceContextStore(),\n * defaultTTLMs: 24 * 60 * 60 * 1000, // 24 hours\n * });\n * ```\n *\n * @example With Redis store\n * ```typescript\n * class RedisTraceContextStore implements TraceContextStore {\n * constructor(private redis: Redis) {}\n *\n * async save(key: string, context: StoredTraceContext) {\n * const ttlSeconds = context.ttlMs ? Math.ceil(context.ttlMs / 1000) : 86400;\n * await this.redis.setex(key, ttlSeconds, JSON.stringify(context));\n * }\n *\n * async load(key: string) {\n * const data = await this.redis.get(key);\n * return data ? JSON.parse(data) : null;\n * }\n *\n * async delete(key: string) {\n * await this.redis.del(key);\n * }\n * }\n *\n * const parkingLot = createParkingLot({\n * store: new RedisTraceContextStore(redis),\n * });\n * ```\n */\nexport function createParkingLot(config: ParkingLotConfig): ParkingLot {\n const {\n store,\n defaultTTLMs = 24 * 60 * 60 * 1000, // 24 hours\n keyPrefix = 'parkingLot:',\n autoDeleteOnRetrieve = true,\n onMiss,\n } = config;\n\n /**\n * Get current span context from active context\n */\n function getCurrentSpanContext(): SpanContext | null {\n const activeSpan = otelTrace.getActiveSpan();\n if (!activeSpan) {\n return null;\n }\n return activeSpan.spanContext();\n }\n\n /**\n * Apply key prefix\n */\n function prefixKey(key: string): string {\n return `${keyPrefix}${key}`;\n }\n\n const parkingLot: ParkingLot = {\n async park(\n correlationKey: string,\n metadata?: Record<string, string>,\n ): Promise<string> {\n const spanContext = getCurrentSpanContext();\n const fullKey = prefixKey(correlationKey);\n\n const storedContext: StoredTraceContext = {\n traceId: spanContext?.traceId ?? '',\n spanId: spanContext?.spanId ?? '',\n traceFlags: spanContext?.traceFlags ?? 0,\n parkedAt: Date.now(),\n ttlMs: defaultTTLMs,\n metadata,\n };\n\n await store.save(fullKey, storedContext);\n\n const activeSpan = otelTrace.getActiveSpan();\n if (activeSpan) {\n const parkAttrs: Record<string, AttributeValue> = {\n 'parking_lot.correlation_key': correlationKey,\n 'parking_lot.ttl_ms': defaultTTLMs,\n ...(metadata &&\n Object.fromEntries(\n Object.entries(metadata).map(([k, v]) => [\n `parking_lot.metadata.${k}`,\n v,\n ]),\n )),\n };\n emitCorrelatedEvent(\n {\n setAttribute: (k, v) => activeSpan.setAttribute(k, v),\n setAttributes: (a) => activeSpan.setAttributes(a),\n addEvent: (n, a) => activeSpan.addEvent(n, a),\n },\n 'trace_context_parked',\n parkAttrs,\n );\n }\n\n // Return the unprefixed key so callers can use the same key for retrieve()\n return correlationKey;\n },\n\n async retrieve(correlationKey: string): Promise<StoredTraceContext | null> {\n const fullKey = prefixKey(correlationKey);\n const storedContext = await store.load(fullKey);\n\n if (!storedContext) {\n onMiss?.(correlationKey);\n return null;\n }\n\n if (autoDeleteOnRetrieve) {\n await store.delete(fullKey);\n }\n\n return storedContext;\n },\n\n traceCallback<TArgs extends unknown[], TReturn>(\n callbackConfig: CallbackConfig,\n ): (\n fnFactory: (ctx: CallbackContext) => (...args: TArgs) => Promise<TReturn>,\n ) => (...args: TArgs) => Promise<TReturn> {\n return (\n fnFactory: (\n ctx: CallbackContext,\n ) => (...args: TArgs) => Promise<TReturn>,\n ): ((...args: TArgs) => Promise<TReturn>) => {\n return trace<TArgs, TReturn>(\n {\n name: callbackConfig.name,\n spanKind: SpanKind.SERVER,\n },\n (baseCtx) => {\n return async (...args: TArgs) => {\n // Extract correlation key from arguments\n const correlationKey = callbackConfig.correlationKeyFrom(args);\n\n // Retrieve parked context\n const parkedContext = await parkingLot.retrieve(correlationKey);\n\n // Calculate elapsed time\n const elapsedMs = parkedContext\n ? Date.now() - parkedContext.parkedAt\n : null;\n\n // Set span attributes\n baseCtx.setAttribute(\n 'parking_lot.correlation_key',\n correlationKey,\n );\n\n if (parkedContext) {\n baseCtx.setAttribute('parking_lot.elapsed_ms', elapsedMs!);\n baseCtx.setAttribute(\n 'parking_lot.original_trace_id',\n parkedContext.traceId,\n );\n baseCtx.setAttribute(\n 'parking_lot.original_span_id',\n parkedContext.spanId,\n );\n\n // Add metadata as attributes\n if (parkedContext.metadata) {\n for (const [key, value] of Object.entries(\n parkedContext.metadata,\n )) {\n baseCtx.setAttribute(`parking_lot.metadata.${key}`, value);\n }\n }\n\n // Create span link to original trace\n const link = parkingLot.createLink(parkedContext);\n baseCtx.addLinks([link]);\n\n emitCorrelatedEvent(baseCtx, 'parked_context_retrieved', {\n 'parking_lot.correlation_key': correlationKey,\n 'parking_lot.elapsed_ms': elapsedMs!,\n 'parking_lot.original_trace_id': parkedContext.traceId,\n });\n } else {\n baseCtx.setAttribute('parking_lot.context_found', false);\n\n if (callbackConfig.requireParkedContext) {\n const error = new Error(\n `Required parked context not found for key: ${correlationKey}`,\n );\n recordStructuredError(baseCtx, error);\n throw error;\n }\n }\n\n // Apply custom attributes\n if (callbackConfig.attributes) {\n for (const [key, value] of Object.entries(\n callbackConfig.attributes,\n )) {\n baseCtx.setAttribute(key, value);\n }\n }\n\n // Create extended context\n const callbackCtx: CallbackContext = {\n ...baseCtx,\n parkedContext,\n elapsedMs,\n correlationKey,\n };\n\n // Execute user's function\n const userFn = fnFactory(callbackCtx);\n return userFn(...args);\n };\n },\n );\n };\n },\n\n createLink(storedContext: StoredTraceContext): Link {\n return {\n context: {\n traceId: storedContext.traceId,\n spanId: storedContext.spanId,\n traceFlags: storedContext.traceFlags,\n isRemote: true,\n },\n attributes: {\n 'link.type': 'parking_lot',\n 'parking_lot.parked_at': storedContext.parkedAt,\n ...(storedContext.metadata && {\n 'parking_lot.has_metadata': true,\n }),\n },\n };\n },\n\n async exists(correlationKey: string): Promise<boolean> {\n const fullKey = prefixKey(correlationKey);\n const context = await store.load(fullKey);\n return context !== null;\n },\n };\n\n return parkingLot;\n}\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Create a correlation key from multiple parts\n *\n * @param parts - Key parts to join\n * @returns A correlation key string\n *\n * @example\n * ```typescript\n * const key = createCorrelationKey('payment', orderId, 'stripe');\n * // Returns: \"payment:order-123:stripe\"\n * ```\n */\nexport function createCorrelationKey(...parts: (string | number)[]): string {\n return parts.map(String).join(':');\n}\n\n/**\n * Extract span context from stored context for manual linking\n *\n * @param storedContext - The stored trace context\n * @returns SpanContext compatible object\n */\nexport function toSpanContext(storedContext: StoredTraceContext): SpanContext {\n return {\n traceId: storedContext.traceId,\n spanId: storedContext.spanId,\n traceFlags: storedContext.traceFlags,\n isRemote: true,\n };\n}\n"],"mappings":"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"}