autotel
Version:
Write Once, Observe Anywhere
1 lines • 70.4 kB
Source Map (JSON)
{"version":3,"file":"functional-Cj7PSItS.cjs","names":["nodeUrl","nodeFs","SpanStatusCode","getInitConfig","otelTrace","createTraceContext","getEventQueue","getForceFlushableProvider","getSdk","getContextStorage","context","getConfig","AlwaysSampler","getActiveContextWithBaggage","runInOperationContext","AUTOTEL_SAMPLING_RATE","AUTOTEL_SAMPLING_TAIL_KEEP","AUTOTEL_SAMPLING_TAIL_EVALUATED","propagation"],"sources":["../src/variable-name-inference.ts","../src/functional.ts"],"sourcesContent":["/**\n * Variable Name Inference Utility\n *\n * Attempts to infer variable names from const/export const assignments\n * by analyzing the call stack and parsing source code.\n *\n * This is a best-effort approach with graceful degradation - if inference\n * fails for any reason, it returns undefined without breaking the application.\n */\n\n// namespace imports for browser-bundler compat — see node-require.ts\nimport * as nodeFs from 'node:fs';\nimport * as nodeUrl from 'node:url';\n\ninterface CallLocation {\n file: string;\n line: number;\n column: number;\n}\n\n/**\n * LRU Cache for inferred variable names\n * Key: \"file:line\" (e.g., \"/path/to/file.ts:42\")\n * Value: inferred variable name or undefined\n */\nconst inferenceCache = new Map<string, string | undefined>();\nconst MAX_CACHE_SIZE = 50;\n\n/**\n * Captures the current call stack\n */\nfunction captureStackTrace(): string {\n const originalStackTraceLimit = Error.stackTraceLimit;\n Error.stackTraceLimit = 10; // Only need first few frames\n\n const err = new Error('Stack trace capture');\n const stack = err.stack || '';\n\n Error.stackTraceLimit = originalStackTraceLimit;\n return stack;\n}\n\n/**\n * Parses the stack trace to find where trace() was called\n *\n * Stack trace format (Node.js):\n * at functionName (file:line:column)\n * at file:line:column\n *\n * We skip frames until we find one that's NOT in functional.ts or this file.\n * We also need to skip one additional frame (the trace/span/instrument function itself)\n * to get to the actual user code.\n */\nfunction parseCallLocation(stack: string): CallLocation | undefined {\n const lines = stack.split('\\n');\n let skippedExternalFrame = false;\n\n for (const line of lines) {\n // Skip if line contains this file or functional.ts (internal frames)\n // Be specific about the filename to avoid matching test files\n if (\n line.includes('variable-name-inference.ts') ||\n line.includes('variable-name-inference.js') ||\n line.includes('functional.ts') ||\n line.includes('functional.js')\n ) {\n continue;\n }\n\n // Match various stack trace formats\n // Format 1: at functionName (file:line:column)\n // Format 2: at file:line:column\n const match =\n line.match(/at\\s+(?:.*\\s+)?\\(?([^:]+):(\\d+):(\\d+)\\)?/) ||\n line.match(/^.*?([^:]+):(\\d+):(\\d+)/);\n\n if (match) {\n let filePath = match[1]!.trim();\n\n // Handle file:// URLs (convert to paths)\n if (filePath.startsWith('file://')) {\n try {\n filePath = nodeUrl.fileURLToPath(filePath);\n } catch {\n continue;\n }\n }\n\n // Skip the first external frame (the trace/span function itself)\n // We want the frame where the user CALLS trace(), not inside trace()\n if (!skippedExternalFrame) {\n skippedExternalFrame = true;\n continue;\n }\n\n return {\n file: filePath,\n line: Number.parseInt(match[2]!, 10),\n column: Number.parseInt(match[3]!, 10),\n };\n }\n }\n\n return undefined;\n}\n\n/**\n * Reads a specific line from a source file\n */\nfunction readSourceLine(\n filePath: string,\n lineNumber: number,\n): string | undefined {\n try {\n // Check if we can access the file system (not available in edge runtimes)\n if (typeof nodeFs.readFileSync !== 'function') {\n return undefined;\n }\n\n const content = nodeFs.readFileSync(filePath, 'utf8');\n const lines = content.split('\\n');\n\n // Line numbers are 1-based\n return lines[lineNumber - 1];\n } catch {\n // File doesn't exist, permission denied, or other error\n return undefined;\n }\n}\n\n/**\n * Extracts variable name from source code line using regex patterns\n *\n * Supported patterns:\n * - const varName = anyFunction(\n * - export const varName = anyFunction(\n * - let varName = anyFunction(\n * - var varName = anyFunction(\n *\n * Note: This won't work with destructuring assignments or complex patterns\n */\nfunction extractVariableName(sourceLine: string): string | undefined {\n // Remove leading/trailing whitespace\n const trimmed = sourceLine.trim();\n\n // Pattern: (export)? (const|let|var) varName = anyFunctionCall(\n // We match any function call, not just trace(), to support wrapper functions\n const patterns = [\n // export const varName = anyFunction(\n /export\\s+const\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=\\s*[a-zA-Z_$][a-zA-Z0-9_$]*\\s*\\(/,\n // const varName = anyFunction(\n /const\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=\\s*[a-zA-Z_$][a-zA-Z0-9_$]*\\s*\\(/,\n // export let varName = anyFunction(\n /export\\s+let\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=\\s*[a-zA-Z_$][a-zA-Z0-9_$]*\\s*\\(/,\n // let varName = anyFunction(\n /let\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=\\s*[a-zA-Z_$][a-zA-Z0-9_$]*\\s*\\(/,\n // export var varName = anyFunction(\n /export\\s+var\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=\\s*[a-zA-Z_$][a-zA-Z0-9_$]*\\s*\\(/,\n // var varName = anyFunction(\n /var\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*=\\s*[a-zA-Z_$][a-zA-Z0-9_$]*\\s*\\(/,\n ];\n\n for (const pattern of patterns) {\n const match = trimmed.match(pattern);\n if (match && match[1]) {\n return match[1];\n }\n }\n\n return undefined;\n}\n\n/**\n * Adds an entry to the cache with LRU eviction\n */\nfunction cacheInference(key: string, value: string | undefined): void {\n // If cache is full, remove oldest entry (first entry in Map)\n if (inferenceCache.size >= MAX_CACHE_SIZE) {\n const firstKey = inferenceCache.keys().next().value;\n if (firstKey) {\n inferenceCache.delete(firstKey);\n }\n }\n\n inferenceCache.set(key, value);\n}\n\n/**\n * Main entry point: Attempts to infer the variable name from the call stack\n *\n * This function:\n * 1. Captures the call stack\n * 2. Parses it to find where trace() was called (file + line)\n * 3. Reads that line from the source file\n * 4. Extracts the variable name using regex\n *\n * Returns undefined if inference fails at any step (graceful degradation).\n * Results are cached to avoid repeated file I/O.\n *\n * @returns The inferred variable name, or undefined if inference failed\n */\nexport function inferVariableNameFromCallStack(): string | undefined {\n try {\n // Capture stack trace\n const stack = captureStackTrace();\n\n // Parse stack to find trace() call location\n const callLocation = parseCallLocation(stack);\n if (!callLocation) {\n return undefined;\n }\n\n // Check cache\n const cacheKey = `${callLocation.file}:${callLocation.line}`;\n if (inferenceCache.has(cacheKey)) {\n return inferenceCache.get(cacheKey);\n }\n\n // Read source line\n const sourceLine = readSourceLine(callLocation.file, callLocation.line);\n if (!sourceLine) {\n return undefined;\n }\n\n // Extract variable name\n const variableName = extractVariableName(sourceLine);\n\n // Cache result (even if undefined, to avoid repeated failed attempts)\n cacheInference(cacheKey, variableName);\n\n return variableName;\n } catch {\n // Graceful degradation - don't break the app if inference fails\n return undefined;\n }\n}\n\n/**\n * Clears the inference cache (useful for testing)\n */\nexport function clearInferenceCache(): void {\n inferenceCache.clear();\n}\n","/**\n * Functional API for non-class code\n *\n * Three approaches for different use cases:\n * 1. trace() - Zero-ceremony HOF for single functions\n * 2. withTracing() - Middleware-style composable wrapper\n * 3. instrument() - Batch auto-instrumentation for modules\n *\n * @example trace() - Single function\n * ```typescript\n * export const createUser = trace(async (data) => {\n * getActiveTraceContext()?.setAttribute('user.id', data.id)\n * return await db.users.create(data)\n * })\n * ```\n *\n * @example withTracing() - Composable middleware\n * ```typescript\n * export const createUser = withTracing({\n * name: 'user.create'\n * })(ctx => async (data) => {\n * ctx.setAttribute('user.id', data.id)\n * return await db.users.create(data)\n * })\n * ```\n *\n * @example instrument() - Batch instrumentation\n * ```typescript\n * export default instrument({\n * createUser: async (data) => { },\n * updateUser: async (id, data) => { }\n * }, { serviceName: 'user' })\n * ```\n */\n\nimport {\n SpanStatusCode,\n trace as otelTrace,\n context,\n propagation,\n type Span,\n type Counter,\n type Histogram,\n type Attributes,\n} from '@opentelemetry/api';\nimport { getConfig } from './config';\nimport { getConfig as getInitConfig, getSdk } from './init';\nimport { getForceFlushableProvider } from './tracer-provider';\nimport {\n type Sampler,\n type SamplingContext,\n AlwaysSampler,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n AUTOTEL_SAMPLING_RATE,\n} from './sampling';\nimport { getEventQueue } from './track';\nimport type { TraceContext } from './trace-context';\nimport {\n createTraceContext,\n getActiveContextWithBaggage,\n getContextStorage,\n} from './trace-context';\nimport { setSpanName } from './trace-helpers';\nimport { runInOperationContext } from './operation-context';\nimport { inferVariableNameFromCallStack } from './variable-name-inference';\n\n/**\n * Complete trace context containing trace identifiers and span methods\n *\n * The ctx parameter in trace() functions provides:\n * - traceId, spanId, correlationId from the active span\n * - Span manipulation methods (setAttribute, setAttributes, setStatus, recordException)\n *\n * For custom context, access it directly in your functions (standard OpenTelemetry pattern).\n *\n * @example\n * ```typescript\n * import { trace } from 'autotel'\n *\n * export const createUser = withTracing({})((ctx) => async (data: CreateUserData) => {\n * // Get custom context directly (standard OTel approach)\n * const userId = getCurrentUserId()\n * const tenantId = getCurrentTenant()\n *\n * // Use ctx for span operations and trace IDs\n * ctx.setAttribute('user.id', data.id)\n * ctx.setAttribute('user.tenant', tenantId)\n * console.log(ctx.traceId) // Trace IDs available\n * })\n * ```\n */\nexport type { TraceContext } from './trace-context';\n\nlet unknownSpanNameWarningEmitted = false;\n\ntype WrappedFunction<TArgs extends unknown[], TReturn> = (\n ...args: TArgs\n) => TReturn | Promise<TReturn>;\n\nfunction resolveVariableName(\n named: InstrumentableFunction,\n variableName?: string,\n): string | undefined {\n const suppliedFunctionName = inferFunctionName(named);\n const callStackVariableName = suppliedFunctionName\n ? undefined\n : inferVariableNameFromCallStack();\n return variableName || suppliedFunctionName || callStackVariableName;\n}\n\n/**\n * Wrap an explicit context factory `(ctx) => (...args) => result`.\n * Used by {@link withTracing}; the input is a factory by contract, so there is\n * no plain-vs-factory detection. The factory is never invoked at construction\n * time — only when the wrapper is called.\n */\nfunction wrapFactoryWithTracing<TArgs extends unknown[], TReturn>(\n factory: (\n ctx: TraceContext,\n ) => (...args: TArgs) => TReturn | Promise<TReturn>,\n options: TracingOptions<TArgs, TReturn>,\n variableName?: string,\n): WrappedFunction<TArgs, TReturn> {\n const effectiveVariableName = resolveVariableName(\n factory as InstrumentableFunction,\n variableName,\n );\n return wrapWithTracingSync(\n factory,\n options,\n effectiveVariableName,\n ) as WrappedFunction<TArgs, TReturn>;\n}\n\n/**\n * Wrap a plain function `(...args) => result`. Used by {@link trace} and\n * {@link instrument}: the function receives its real arguments and no context\n * is injected. Reach the active span via {@link getActiveTraceContext} inside\n * the body (or any helper it calls).\n */\nfunction wrapPlainWithTracing<TArgs extends unknown[], TReturn>(\n fn: (...args: TArgs) => TReturn | Promise<TReturn>,\n options: TracingOptions<TArgs, TReturn>,\n variableName?: string,\n): WrappedFunction<TArgs, TReturn> {\n const effectiveVariableName = resolveVariableName(\n fn as InstrumentableFunction,\n variableName,\n );\n const factory = (_ctx: TraceContext) => fn;\n return wrapWithTracingSync(\n factory,\n options,\n effectiveVariableName,\n ) as WrappedFunction<TArgs, TReturn>;\n}\n\n/**\n * Common options for functional tracing\n */\nexport interface TracingOptions<\n TArgs extends unknown[] = unknown[],\n TReturn = unknown,\n> {\n /**\n * Span name (highest priority)\n * If provided, this is used as the span name\n */\n name?: string;\n\n /**\n * Service name (used to compose final span name)\n * If name not provided, span name becomes: ${serviceName}.${functionName}\n */\n serviceName?: string;\n\n /**\n * Sampling strategy\n * @default AlwaysSampler\n */\n sampler?: Sampler;\n\n /**\n * Enable metrics collection (counter, histogram)\n * @default false\n */\n withMetrics?: boolean;\n\n /**\n * Extract attributes from function arguments\n */\n attributesFromArgs?: (args: TArgs) => Record<string, unknown>;\n\n /**\n * Extract attributes from function result\n */\n attributesFromResult?: (result: TReturn) => Record<string, unknown>;\n\n /**\n * Capture the function arguments onto the span as `autotel.input`\n * (JSON, truncated). One arg is captured directly; multiple are captured as\n * an array. Off by default — opt in per call. Tools (visualizers, devtools)\n * read this alongside `ai.toolCall.args` to show function I/O uniformly.\n * Avoid on args with secrets/PII, or pair with a redacting processor.\n */\n captureInput?: boolean;\n\n /**\n * Capture the function return value onto the span as `autotel.output`\n * (JSON, truncated). Off by default. Same caveats as {@link captureInput}.\n */\n captureOutput?: boolean;\n\n /**\n * Start a new root span instead of creating a child\n * Useful for serverless entry points\n * @default false\n */\n startNewRoot?: boolean;\n\n /**\n * Flush events queue when span ends\n * Only flushes on root spans (to avoid excessive flushing)\n * @default true\n */\n flushOnRootSpanEnd?: boolean;\n\n /**\n * Span kind for semantic convention compliance\n * Used for messaging (PRODUCER/CONSUMER), HTTP (CLIENT/SERVER), etc.\n * @default SpanKind.INTERNAL\n */\n spanKind?: import('@opentelemetry/api').SpanKind;\n\n /**\n * Classify a thrown value as a real error. Return `false` to treat the throw\n * as expected control flow: the span is marked OK, no exception is recorded,\n * and the value is rethrown untouched. Use this for framework control-flow\n * signals that propagate via `throw` but are not failures — e.g. TanStack\n * Router / Next.js `redirect()` and `notFound()`.\n * @default every throw is treated as an error\n */\n isError?: (error: unknown) => boolean;\n}\n\n/**\n * Options for instrument() batch instrumentation\n */\nexport interface InstrumentOptions<\n T extends Record<string, AnyInstrumentable> = Record<\n string,\n AnyInstrumentable\n >,\n> extends TracingOptions {\n /** Object whose function properties should be instrumented */\n functions: T;\n /**\n * Per-function configuration overrides\n */\n overrides?: Record<string, Partial<TracingOptions>>;\n\n /**\n * Functions to skip (won't be instrumented)\n * Supports:\n * - String keys: 'functionName'\n * - RegExp: /^_internal/\n * - Predicate: (key, fn) => boolean\n *\n * By default, functions starting with _ are skipped\n */\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n skip?: (string | RegExp | ((key: string, fn: Function) => boolean))[];\n}\n\n/**\n * Options for instrumenting one function with an explicit stable key.\n */\nexport interface SingleInstrumentOptions<\n TFunction extends AnyInstrumentable = AnyInstrumentable,\n> extends TracingOptions {\n /** Stable function key used for span naming */\n key: string;\n /** Function to instrument */\n fn: TFunction;\n}\n\n// Maximum error message length to prevent span bloat\nconst MAX_ERROR_MESSAGE_LENGTH = 500;\n\nfunction createDummyCtx<\n TBaggage extends Record<string, unknown> | undefined = undefined,\n>(): TraceContext<TBaggage> {\n // `recordException` / `addEvent` are no-op shims kept for the same\n // compatibility window as `createTraceContext` (see trace-context.ts).\n return {\n traceId: '',\n spanId: '',\n correlationId: '',\n setAttribute: () => {},\n setAttributes: () => {},\n setStatus: () => {},\n recordException: () => {},\n addEvent: () => {},\n addLink: () => {},\n addLinks: () => {},\n updateName: () => {},\n isRecording: () => false,\n getBaggage: () => {},\n setBaggage: () => '',\n deleteBaggage: () => {},\n getAllBaggage: () => new Map(),\n } as unknown as TraceContext<TBaggage>;\n}\n\n/** Attribute keys for opt-in function I/O capture (see TracingOptions). */\nconst AUTOTEL_INPUT_ATTR = 'autotel.input';\nconst AUTOTEL_OUTPUT_ATTR = 'autotel.output';\nconst CAPTURE_MAX_CHARS = 4096;\n\n/** JSON-serialize a captured value, defensively (truncate, swallow cycles). */\nfunction serializeCapture(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n try {\n const json = typeof value === 'string' ? value : JSON.stringify(value);\n if (json === undefined) return undefined;\n return json.length > CAPTURE_MAX_CHARS\n ? `${json.slice(0, CAPTURE_MAX_CHARS)}…[truncated]`\n : json;\n } catch {\n return undefined;\n }\n}\n\n/** `autotel.input` from args (single arg captured directly, else the array). */\nfunction captureInputAttrs(\n args: unknown[],\n enabled?: boolean,\n): Record<string, unknown> {\n if (!enabled) return {};\n const s = serializeCapture(args.length === 1 ? args[0] : args);\n return s === undefined ? {} : { [AUTOTEL_INPUT_ATTR]: s };\n}\n\n/** `autotel.output` from the return value. */\nfunction captureOutputAttrs(\n result: unknown,\n enabled?: boolean,\n): Record<string, unknown> {\n if (!enabled) return {};\n const s = serializeCapture(result);\n return s === undefined ? {} : { [AUTOTEL_OUTPUT_ATTR]: s };\n}\n\n// Symbol to prevent double-instrumentation (idempotency flag)\nconst INSTRUMENTED_SYMBOL = Symbol.for('autotel.functional.instrumented');\n\ntype InstrumentedFlag = {\n [INSTRUMENTED_SYMBOL]?: true;\n};\n\nfunction hasInstrumentationFlag(value: unknown): value is InstrumentedFlag {\n return (\n (typeof value === 'function' || typeof value === 'object') &&\n value !== null &&\n Boolean((value as InstrumentedFlag)[INSTRUMENTED_SYMBOL])\n );\n}\n\n/**\n * Truncate error message to prevent span bloat\n */\nfunction truncateErrorMessage(message: string): string {\n if (message.length <= MAX_ERROR_MESSAGE_LENGTH) {\n return message;\n }\n return `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}... (truncated)`;\n}\n\n/** Per-invocation state the terminal span handlers close over. */\ninterface SpanFinalizeContext {\n span: Span;\n spanName: string;\n duration: number;\n callCounter?: Counter;\n durationHistogram?: Histogram;\n handleTailSampling: (\n success: boolean,\n duration: number,\n error?: Error,\n ) => void;\n /** Extra attributes to merge (e.g. captured args); absent on immediate-exec paths. */\n extraAttributes?: Attributes;\n}\n\n/**\n * Terminal handling for a value thrown by a traced function. Shared by all four\n * `trace()` execution paths (immediate + wrapper, sync + async) so the outcome\n * rules live in exactly one place. Ends the span; the caller flushes (await/void)\n * and rethrows.\n *\n * If `isError` classifies the throw as expected control flow — e.g. a framework\n * `redirect()` / `notFound()` signal — it is recorded as a success outcome with\n * an OK status and no exception. Otherwise it is recorded as an error.\n */\nfunction finalizeThrownSpan(\n error: unknown,\n isError: ((error: unknown) => boolean) | undefined,\n ctx: SpanFinalizeContext,\n): void {\n const {\n span,\n spanName,\n duration,\n callCounter,\n durationHistogram,\n handleTailSampling,\n extraAttributes,\n } = ctx;\n\n const baseAttributes: Attributes = {\n ...extraAttributes,\n 'operation.name': spanName,\n 'code.function': spanName,\n 'operation.duration': duration,\n };\n\n if (isError && !isError(error)) {\n callCounter?.add(1, { operation: spanName, status: 'success' });\n durationHistogram?.record(duration, {\n operation: spanName,\n status: 'success',\n });\n span.setStatus({ code: SpanStatusCode.OK });\n span.setAttributes({ ...baseAttributes, 'operation.success': true });\n handleTailSampling(true, duration);\n span.end();\n return;\n }\n\n callCounter?.add(1, { operation: spanName, status: 'error' });\n durationHistogram?.record(duration, { operation: spanName, status: 'error' });\n\n const truncatedMessage = truncateErrorMessage(\n error instanceof Error ? error.message : 'Unknown error',\n );\n\n span.setStatus({ code: SpanStatusCode.ERROR, message: truncatedMessage });\n span.setAttributes({\n ...baseAttributes,\n 'operation.success': false,\n error: true,\n 'exception.type': error instanceof Error ? error.constructor.name : 'Error',\n 'exception.message': truncatedMessage,\n });\n if (error instanceof Error && error.stack) {\n span.setAttribute(\n 'exception.stack',\n error.stack.slice(0, MAX_ERROR_MESSAGE_LENGTH),\n );\n }\n const thrown = error instanceof Error ? error : new Error(String(error));\n span.recordException(thrown);\n // Samplers read result.error, so hand them a real Error rather than\n // whatever the code threw.\n handleTailSampling(false, duration, thrown);\n span.end();\n}\n\ntype InstrumentableFunction<\n TArgs extends unknown[] = unknown[],\n TReturn = unknown,\n> = ((...args: TArgs) => TReturn | Promise<TReturn>) & {\n displayName?: string;\n name?: string;\n};\n\n/**\n * Constraint alias for `instrument()` and friends. `never[]` parameters make\n * every concretely-typed function (e.g. `(name: string) => Promise<User>`)\n * satisfy the constraint under `strictFunctionTypes` without resorting to\n * `any`. Use ONLY in constraint positions — the concrete `T` is still\n * inferred from the actual argument, so call-site argument and return types\n * are fully preserved.\n */\ntype AnyInstrumentable = ((...args: never[]) => unknown) & {\n displayName?: string;\n name?: string;\n};\n\n/**\n * Try to infer function name from function properties\n * Checks for displayName, name, or other metadata that might be set\n */\nfunction inferFunctionName<\n TArgs extends unknown[] = unknown[],\n TReturn = unknown,\n>(fn: InstrumentableFunction<TArgs, TReturn>): string | undefined {\n // Check for displayName property (sometimes set by bundlers)\n const displayName = (fn as { displayName?: string }).displayName;\n if (displayName) {\n return displayName;\n }\n\n // Check function.name (works for named functions and modern arrow function assignment)\n // Note: Empty string is falsy, so this handles both undefined and ''\n if (fn.name && fn.name !== 'anonymous' && fn.name !== '') {\n return fn.name;\n }\n\n // Try to extract name from function source (for function declarations)\n const source = Function.prototype.toString.call(fn);\n const match = source.match(/function\\s+([^(\\s]+)/);\n if (match && match[1] && match[1] !== 'anonymous') {\n return match[1];\n }\n\n return undefined;\n}\n\n/**\n * Determine span name using priority:\n * 1. Explicit name option\n * 2. serviceName + functionName\n * 3. Inferred from function/variable name (including stack trace fallback)\n * 4. Fallback to 'unknown'\n */\nfunction getSpanName<TArgs extends unknown[], TReturn>(\n options: TracingOptions<TArgs, TReturn>,\n fn: InstrumentableFunction<TArgs, TReturn>,\n variableName?: string,\n): string {\n // 1. Explicit name\n if (options.name) {\n return options.name;\n }\n\n // 2. Try variable name, function name, or function properties\n let fnName = variableName || inferFunctionName(fn);\n\n // Default to 'anonymous' if still no name\n fnName = fnName || 'anonymous';\n\n // 2. serviceName + functionName\n if (options.serviceName) {\n return `${options.serviceName}.${fnName}`;\n }\n\n // 3. Inferred from function name\n if (fnName && fnName !== 'anonymous') {\n return fnName;\n }\n\n // 4. Fallback\n const initConfig = getInitConfig();\n if (\n typeof process !== 'undefined' &&\n process.env.NODE_ENV !== 'production' &&\n !unknownSpanNameWarningEmitted &&\n initConfig?.logger?.warn\n ) {\n unknownSpanNameWarningEmitted = true;\n initConfig.logger.warn(\n {},\n '[autotel] Span name resolved to \"unknown\". Pass an explicit name, for example trace(\"operation.name\", fn).',\n );\n }\n return 'unknown';\n}\n\n/**\n * Check if function should be skipped\n */\nfunction shouldSkip(\n key: string,\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n fn: Function,\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n skip?: (string | RegExp | ((key: string, fn: Function) => boolean))[],\n): boolean {\n // Default: skip functions starting with _\n if (key.startsWith('_')) {\n return true;\n }\n\n if (!skip || skip.length === 0) {\n return false;\n }\n\n for (const rule of skip) {\n if (typeof rule === 'string' && key === rule) {\n return true;\n } else if (rule instanceof RegExp && rule.test(key)) {\n return true;\n } else if (typeof rule === 'function' && rule(key, fn)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Get current trace context value (internal helper)\n *\n * Returns base context (trace IDs) + span methods from the active span.\n */\nfunction getCtxValue<\n TBaggage extends Record<string, unknown> | undefined = undefined,\n>(): TraceContext<TBaggage> | null {\n const activeSpan = otelTrace.getActiveSpan();\n if (!activeSpan) return null;\n\n // Use shared utility to create trace context\n return createTraceContext<TBaggage>(activeSpan);\n}\n\n/**\n * Get the autotel {@link TraceContext} for the currently active span.\n *\n * This is the ambient accessor for the functional API: instead of threading a\n * `ctx` parameter through a factory, call this inside any traced function (or a\n * helper it calls) to reach `setAttribute`, `setUser`, `getBaggage`, and the\n * rest of the context surface. Returns `undefined` when no span is active.\n *\n * @example\n * ```typescript\n * const getUser = trace('getUser', async (id: string) => {\n * getActiveTraceContext()?.setAttribute('user.id', id);\n * return db.users.find(id);\n * });\n * ```\n *\n * @see getActiveSpan for the raw OpenTelemetry span\n * @see getRequestLogger which reads the active context when called with no args\n */\nexport function getActiveTraceContext<\n TBaggage extends Record<string, unknown> | undefined = undefined,\n>(): TraceContext<TBaggage> | undefined {\n return getCtxValue<TBaggage>() ?? undefined;\n}\n\n/**\n * Context object that lazily evaluates the active span on property access\n *\n * Access trace context directly without function call syntax.\n *\n * @example\n * ```typescript\n * import { trace, ctx } from 'autotel'\n *\n * export const createUser = trace(async (data) => {\n * // Direct property access - no function call!\n * if (ctx.traceId) {\n * ctx.setAttribute('user.id', data.id)\n * console.log('Trace:', ctx.traceId)\n * }\n * })\n * ```\n */\nexport const ctx = new Proxy({} as TraceContext, {\n get(_target, prop) {\n const ctxValue = getCtxValue();\n if (!ctxValue) {\n return;\n }\n return ctxValue[prop as keyof typeof ctxValue];\n },\n\n has(_target, prop) {\n const ctxValue = getCtxValue();\n if (!ctxValue) {\n return false;\n }\n return prop in ctxValue;\n },\n\n ownKeys() {\n const ctxValue = getCtxValue();\n if (!ctxValue) {\n return [];\n }\n return Object.keys(ctxValue);\n },\n\n getOwnPropertyDescriptor(_target, prop) {\n const ctxValue = getCtxValue();\n if (!ctxValue) {\n return;\n }\n return Object.getOwnPropertyDescriptor(ctxValue, prop);\n },\n});\n\n/**\n * Build the root-operation flush policy once and reuse it for wrapped and\n * immediate execution. Async operations await this function; sync operations\n * trigger it without blocking.\n */\nfunction createRootTelemetryFlusher(\n options: Pick<TracingOptions, 'flushOnRootSpanEnd'>,\n isRootSpan: boolean,\n): () => Promise<void> {\n const initConfig = getInitConfig();\n const shouldFlush =\n options.flushOnRootSpanEnd ?? initConfig?.flushOnRootSpanEnd ?? true;\n const shouldFlushSpans = initConfig?.forceFlushOnShutdown ?? false;\n\n return async () => {\n if (!shouldFlush || !isRootSpan) return;\n\n try {\n const queue = getEventQueue();\n if (queue && queue.size() > 0) {\n await queue.flush();\n }\n\n if (shouldFlushSpans) {\n const tracerProvider = getForceFlushableProvider(getSdk());\n await tracerProvider?.forceFlush();\n }\n } catch (error) {\n const logger = getInitConfig()?.logger;\n logger?.error?.(\n { err: error instanceof Error ? error : undefined },\n `[autotel] Auto-flush failed${error instanceof Error ? '' : `: ${String(error)}`}`,\n );\n }\n };\n}\n\n/**\n * Give every root operation its own baggage context without replacing a\n * caller's existing scope. AsyncLocalStorage keeps the scope alive until an\n * async result settles and restores the previous store afterwards.\n */\nfunction runWithTraceContextStorage<TResult>(fn: () => TResult): TResult {\n const storage = getContextStorage();\n if (storage.getStore()) return fn();\n return storage.run({ value: context.active() }, fn);\n}\n\n/**\n * Core tracing wrapper for sync functions (internal implementation)\n */\nfunction wrapWithTracingSync<TArgs extends unknown[], TReturn>(\n fnFactory: (\n ctx: TraceContext,\n ) => (...args: TArgs) => TReturn | Promise<TReturn>,\n options: TracingOptions<TArgs, TReturn>,\n variableName?: string,\n): WrappedFunction<TArgs, TReturn> {\n // Idempotency check: if already instrumented, return as-is\n if (hasInstrumentationFlag(fnFactory)) {\n // If already instrumented, we need to extract the original factory\n // For now, we'll just proceed - this edge case is handled by the wrapped function check\n }\n\n const config = getConfig();\n const tracer = config.tracer;\n const meter = config.meter;\n // Annotate as Sampler so the optional hooks stay visible. Inference would\n // narrow to AlwaysSampler and hide them behind `in` checks.\n const sampler: Sampler = options.sampler || new AlwaysSampler();\n\n const spanName = getSpanName(\n options,\n fnFactory as unknown as InstrumentableFunction<TArgs, TReturn>,\n variableName,\n );\n\n // Metrics setup (if enabled)\n const callCounter = options.withMetrics\n ? meter.createCounter(`${spanName}.calls`, {\n description: `Call count for ${spanName}`,\n unit: '1',\n })\n : undefined;\n\n const durationHistogram = options.withMetrics\n ? meter.createHistogram(`${spanName}.duration`, {\n description: `Duration for ${spanName}`,\n unit: 'ms',\n })\n : undefined;\n\n // Return wrapped function\n function wrappedFunction(\n this: unknown,\n ...args: TArgs\n ): TReturn | Promise<TReturn> {\n const samplingContext: SamplingContext = {\n operationName: spanName,\n args,\n metadata: {},\n };\n\n const shouldSample = sampler.shouldSample(samplingContext);\n // Read the rate next to the decision that produced it: a windowed sampler\n // can move on to a new rate before the span closes.\n const sampleRate = sampler.sampleRate?.(samplingContext);\n const needsTailSampling = sampler.needsTailSampling?.() ?? false;\n\n // If not sampling and no tail sampling, execute without tracing\n if (!shouldSample && !needsTailSampling) {\n const fn = fnFactory(createDummyCtx());\n return fn.call(this, ...args);\n }\n\n const startTime = performance.now();\n\n const isRootSpan =\n options.startNewRoot || otelTrace.getActiveSpan() === undefined;\n const flushRootTelemetry = createRootTelemetryFlusher(options, isRootSpan);\n\n // Build span options including root and kind\n const spanOptions: import('@opentelemetry/api').SpanOptions = {};\n if (options.startNewRoot) {\n spanOptions.root = true;\n }\n if (options.spanKind !== undefined) {\n spanOptions.kind = options.spanKind;\n }\n\n const parentContext = getActiveContextWithBaggage();\n return tracer.startActiveSpan(\n spanName,\n spanOptions,\n parentContext,\n (span) => {\n // Run within operation context so events can auto-capture operation.name\n return runInOperationContext(spanName, () =>\n runWithTraceContextStorage(() => {\n let shouldKeepSpan = true;\n\n // Store span name for trace context helpers\n setSpanName(span, spanName);\n\n // Only worth recording when the sampler actually dropped events.\n if (sampleRate !== undefined && sampleRate > 1) {\n span.setAttribute(AUTOTEL_SAMPLING_RATE, sampleRate);\n }\n\n // Create trace context for this span using shared utility\n const ctxValue = createTraceContext(span);\n\n // Get the actual function from the factory\n const fn = fnFactory(ctxValue);\n\n // Extract attributes only when actually tracing\n // This avoids expensive preprocessing when sampling rejects the trace\n const argsAttributes: Attributes = {\n ...captureInputAttrs(args, options.captureInput),\n ...(options.attributesFromArgs\n ? options.attributesFromArgs(args)\n : {}),\n } as Attributes;\n\n const handleTailSampling = (\n success: boolean,\n duration: number,\n error?: Error,\n ) => {\n if (needsTailSampling && sampler.shouldKeepTrace) {\n shouldKeepSpan = sampler.shouldKeepTrace(samplingContext, {\n success,\n duration,\n error,\n });\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);\n span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);\n }\n };\n\n const onSuccess = (result: TReturn) => {\n const duration = performance.now() - startTime;\n\n callCounter?.add(1, {\n operation: spanName,\n status: 'success',\n });\n\n durationHistogram?.record(duration, {\n operation: spanName,\n status: 'success',\n });\n\n const resultAttributes = {\n ...captureOutputAttrs(result, options.captureOutput),\n ...(options.attributesFromResult\n ? options.attributesFromResult(result)\n : {}),\n };\n\n span.setStatus({ code: SpanStatusCode.OK });\n span.setAttributes({\n ...argsAttributes,\n ...resultAttributes,\n 'operation.name': spanName,\n 'code.function': spanName,\n 'operation.duration': duration,\n 'operation.success': true,\n });\n\n handleTailSampling(true, duration);\n\n span.end();\n return result;\n };\n\n const onError = (error: unknown): never => {\n finalizeThrownSpan(error, options.isError, {\n span,\n spanName,\n duration: performance.now() - startTime,\n callCounter,\n durationHistogram,\n handleTailSampling,\n extraAttributes: argsAttributes,\n });\n throw error;\n };\n\n try {\n callCounter?.add(1, {\n operation: spanName,\n status: 'started',\n });\n\n const result = context.with(getActiveContextWithBaggage(), () =>\n fn.call(this, ...args),\n );\n\n if (result instanceof Promise) {\n return result.then(\n async (value) => {\n const completed = onSuccess(value);\n await flushRootTelemetry();\n return completed;\n },\n async (error) => {\n try {\n return onError(error);\n } finally {\n await flushRootTelemetry();\n }\n },\n );\n }\n\n const completed = onSuccess(result);\n void flushRootTelemetry();\n return completed;\n } catch (error) {\n try {\n return onError(error);\n } finally {\n void flushRootTelemetry();\n }\n }\n }),\n );\n },\n );\n }\n\n // Mark as instrumented to prevent double-wrapping\n (wrappedFunction as InstrumentedFlag)[INSTRUMENTED_SYMBOL] = true;\n\n Object.defineProperty(wrappedFunction, 'name', {\n value: variableName || 'trace',\n configurable: true,\n });\n\n return wrappedFunction as WrappedFunction<TArgs, TReturn>;\n}\n\n/**\n * Approach 1: trace() - Zero-ceremony HOF\n *\n * Wrap a plain function with automatic tracing. The function receives its real\n * arguments; no context parameter is injected. Use\n * {@link getActiveTraceContext} inside the function, or use {@link withTracing}\n * for the explicit `(ctx) => (...args) => result` factory form.\n *\n * @example Auto-inferred name - Plain function\n * ```typescript\n * export const createUser = trace(async (data) => {\n * return await db.users.create(data)\n * })\n * // → Traced as \"createUser\"\n * ```\n *\n * @example Ambient context access\n * ```typescript\n * export const createUser = trace(async (data) => {\n * getActiveTraceContext()?.setAttribute('user.id', data.id)\n * return await db.users.create(data)\n * })\n * ```\n *\n * @example Custom name - Plain function\n * ```typescript\n * export const createUser = trace('user.create', async (data) => {\n * return await db.users.create(data)\n * })\n * // → Traced as \"user.create\"\n * ```\n *\n * @example Explicit factory context with withTracing()\n * ```typescript\n * export const createUser = withTracing({ name: 'user.create' })((ctx) => async (data) => {\n * ctx.setAttribute('user.id', data.id)\n * return await db.users.create(data)\n * })\n * ```\n *\n * @example Full options - Plain function\n * ```typescript\n * export const createUser = trace({\n * name: 'user.create',\n * sampler: new AdaptiveSampler(),\n * withMetrics: true\n * }, async (data) => {\n * return await db.users.create(data)\n * })\n * ```\n *\n */\n// Plain-function overloads. `trace()` always wraps a plain function that\n// receives its real arguments; it never injects a context parameter and never\n// inspects the function. Reach the active span via getActiveTraceContext()\n// inside the body. For the explicit `(ctx) => (args) => result` factory form,\n// use withTracing(). `TReturn` captures the return type verbatim, so async\n// functions infer `(...args) => Promise<...>` with no separate overload.\n\n// trace(fn)\nexport function trace<TArgs extends unknown[], TReturn>(\n fn: (...args: TArgs) => TReturn,\n): (...args: TArgs) => TReturn;\n// trace(name, fn)\nexport function trace<TArgs extends unknown[], TReturn>(\n name: string,\n fn: (...args: TArgs) => TReturn,\n): (...args: TArgs) => TReturn;\n// trace(options, fn)\nexport function trace<TArgs extends unknown[], TReturn>(\n options: TracingOptions<TArgs, TReturn>,\n fn: (...args: TArgs) => TReturn,\n): (...args: TArgs) => TReturn;\n// Implementation\nexport function trace<TArgs extends unknown[] = unknown[], TReturn = unknown>(\n fnOrNameOrOptions:\n | ((...args: TArgs) => TReturn)\n | ((...args: TArgs) => Promise<TReturn>)\n | string\n | TracingOptions<TArgs, TReturn>,\n maybeFn?:\n ((...args: TArgs) => TReturn) | ((...args: TArgs) => Promise<TReturn>),\n): WrappedFunction<TArgs, TReturn> {\n // trace(fn) - the function is plain; it receives its real arguments and no\n // context is injected. Reach the active span via getActiveTraceContext().\n if (typeof fnOrNameOrOptions === 'function') {\n return wrapPlainWithTracing(\n fnOrNameOrOptions as (...args: TArgs) => TReturn,\n {} as TracingOptions<TArgs, TReturn>,\n );\n }\n\n // trace(name, fn) or trace(options, fn)\n if (!maybeFn) {\n throw new Error('trace(name|options, fn): fn is required');\n }\n\n const options: TracingOptions<TArgs, TReturn> =\n typeof fnOrNameOrOptions === 'string'\n ? ({ name: fnOrNameOrOptions } as TracingOptions<TArgs, TReturn>)\n : fnOrNameOrOptions;\n\n return wrapPlainWithTracing(maybeFn as (...args: TArgs) => TReturn, options);\n}\n\n/**\n * Approach 2: withTracing() - Middleware-style composable wrapper\n *\n * Returns a HOF that wraps functions with tracing.\n * Perfect for composition and reusable configuration.\n *\n * @example Standard usage\n * ```typescript\n * export const createUser = withTracing({\n * name: 'user.create'\n * })(ctx => async (data) => {\n * ctx.setAttribute('user.id', data.id)\n * return await db.users.create(data)\n * })\n * ```\n *\n * @example Composable\n * ```typescript\n * const tracer = withTracing({ serviceName: 'user' })\n *\n * export const createUser = tracer(ctx => async (data) => { })\n * export const updateUser = tracer(ctx => async (id, data) => { })\n * ```\n *\n * @example With other middleware\n * ```typescript\n * export const createUser = compose(\n * withAuth({ role: 'admin' }),\n * withTracing({ name: 'user.create' }),\n * withRateLimit({ max: 100 })\n * )(ctx => async (data) => { })\n * ```\n */\nexport function withTracing<\n TCfgArgs extends unknown[] = unknown[],\n TCfgReturn = unknown,\n>(options: TracingOptions<TCfgArgs, TCfgReturn> = {}) {\n return <TArgs extends TCfgArgs, TReturn extends TCfgReturn>(\n fnFactory: (\n ctx: TraceContext,\n ) => (...args: TArgs) => TReturn | Promise<TReturn>,\n ): WrappedFunction<TArgs, TReturn> =>\n wrapFactoryWithTracing<TArgs, TReturn>(fnFactory, options);\n}\n\n/**\n * Approach 3: instrument() - Batch auto-instrumentation\n *\n * Instrument an entire module/object at once.\n * Closest to @Instrumented decorator pattern.\n *\n * @example Basic usage\n * ```typescript\n * export default instrument({\n * functions: {\n * createUser: async (data) => { },\n * updateUser: async (id, data) => { },\n * deleteUser: async (id) => { }\n * },\n * serviceName: 'user',\n * sampler: new AdaptiveSampler()\n * })\n * // → Traced as \"user.createUser\", \"user.updateUser\", \"user.deleteUser\"\n * ```\n *\n * @example Per-function overrides\n * ```typescript\n * export default instrument({\n * functions: {\n * createUser: async (data) => { },\n * deleteUser: async (id) => { }\n * },\n * serviceName: 'user',\n * overrides: {\n * deleteUser: {\n * sampler: new AlwaysSampler(),\n * withMetrics: true\n * }\n * }\n * })\n * ```\n *\n * @example Skip functions\n * ```typescript\n * export default instrument({\n * functions: {\n * createUser: async (data) => { },\n * _internal: async () => { }, // Auto-skipped (_-prefix)\n * deleteUser: async (id) => { }\n * },\n * serviceName: 'user',\n * skip: [/^test/, (key) => key.includes('debug')]\n * })\n * ```\n */\nexport function instrument<TFunction extends AnyInstrumentable>(\n options: SingleInstrumentOptions<TFunction>,\n): TFunction;\nexport function instrument<T extends Record<string, AnyInstrumentable>>(\n options: InstrumentOptions<T>,\n): T;\nexport function instrument<\n T extends Record<string, AnyInstrumentable>,\n TFunction extends AnyInstrumentable,\n>(\n options: InstrumentOptions<T> | SingleInstrumentOptions<TFunction>,\n): T | TFunction {\n if (!options || typeof options !== 'object') {\n throw new TypeError(\n 'instrument: expected { key, fn } or { functions: { name: fn } }',\n );\n }\n\n if ('key' in options || 'fn' in options) {\n const { key, fn, ...tracingOptions } =\n options as SingleInstrumentOptions<TFunction>;\n if (typeof key !== 'string' || key.trim() === '') {\n throw new TypeError(\n 'instrument: \"key\" must be a non-empty string in the { key, fn } form',\n );\n }\n if (typeof fn !== 'function') {\n throw new TypeError(\n 'instrument: \"fn\" must be a function in the { key, fn } form',\n );\n }\n return wrapPlainWithTracing(fn, tracingOptions, key) as TFunction;\n }\n\n const { functions, ...tracingOptions } = options as InstrumentOptions<T>;\n if (!functions || typeof functions !== 'object') {\n throw new TypeError(\n 'instrument: expected { key, fn } or { functions: { name: fn } }',\n );\n }\n const instrumented: Partial<T> = {};\n\n for (const key of Object.keys(functions)) {\n const typedKey = key as keyof T;\n const fn = functions[typedKey];\n\n // Skip if not a function or undefined - just pass through the value\n if (!fn || typeof fn !== 'function') {\n instrumented[typedKey] = fn as T[typeof typedKey];\n continue;\n }\n\n // Only instrument own enumerable async functions\n // Check if should skip\n if (shouldSkip(key, fn, tracingOptions.skip)) {\n instrumented[typedKey] = fn as T[typeof typedKey];\n continue;\n }\n\n // Merge base options with per-function overrides\n const fnOptions: TracingOptions = {\n ...tracingOptions,\n ...tracingOptions.overrides?.[key],\n // If no explicit name, use key as function name\n name: tracingOptions.overrides?.[key]?.name,\n };\n\n // Bind function to original object to preserve 'this' context\n // This ensures methods can access state on the original object\n const boundFn = fn.bind(functions);\n\n // Convert plain function to factory pattern for trace()\n // For instrument(), we create a factory that ignores ctx and returns the original function\n const fnFactory = (ctx: TraceContext) => {\n void ctx;\n return boundFn;\n };\n\n // Wrap with tracing (sync or async based on implementation)\n instrumented[typedKey] = wrapFactoryWithTracing(\n fnFactory,\n fnOptions,\n key,\n ) as unknown as T[typeof typedKey];\n }\n\n return instrumented as T;\n}\n\n/**\n * Options for span() function\n */\nexport interface SpanOptions {\n /** Span name */\n name: string;\n /** Attributes to set on the span */\n attributes?: Record<string, string | number | boolean>;\n /** OpenTelemetry span kind */\n spanKind?: import('@opentelemetry/api').SpanKind;\n}\n\n/**\n * Execute a function within a named span\n *\n * Useful for adding tracing to specific code blocks without wrapping\n * the entire function. Supports both synchronous and asynchronous functions.\n *\n * Mirrors `trace()`: pass a span name as the first argument for the common\n * case, or full `SpanOptions` when you need to attach attributes.\n *\n * @example\n * ```typescript\n * // Name shorthand\n * await span('payment.charge', async (span) => {\n * await chargeCustomer(order);\n * })\n *\n * // Full options when attributes are needed\n * await span(\n * { name: 'payment.charge', attributes: { amount: order.total } },\n * async (span) => {\n * await chargeCustomer(order);\n * },\n * )\n *\n * // Sync\n * const total = span('calculateTotal', (span) => {\n * return items.reduce((sum, item) => sum + item.price, 0);\n * })\n * ```\n */\n// Overloads — sync first (more specific match), then async.\n// Each shape is offered with a string name OR a full SpanOptions object so\n// span() aligns with trace()'s argument flexibility.\nexport function span<T = unknown>(name: string, fn: (span: Span) => T): T;\nexport function span<T = unknown>(\n name: string,\n fn: (span: Span) => Promise<T>,\n): Promise<T>;\nexport function span<T = unknown>(\n options: SpanOptions,\n fn: (span: Span) => T,\n): T;\nexport function span<T = unknown>(\n options: SpanOptions,\n fn: (span: Span) => Promise<T>,\n): Promise<T>;\n// Implementation\nexport function span<T = unknown>(\n nameOrOptions: string | SpanOptions,\n fn: (span: Span) => T | Promise<T>,\n): T | Promise<T> {\n const options: SpanOptions =\n typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions;\n const config =