UNPKG

autotel

Version:
1 lines 113 kB
{"version":3,"file":"functional-C8B0Qa7o.cjs","names":["nodeUrl","nodeFs","otelTrace","createTraceContext","getConfig","AlwaysSampler","getInitConfig","getEventQueue","getSdk","getActiveContextWithBaggage","runInOperationContext","context","getContextStorage","AUTOTEL_SAMPLING_TAIL_KEEP","AUTOTEL_SAMPLING_TAIL_EVALUATED","SpanStatusCode","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(ctx => async (data) => {\n * ctx.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} from '@opentelemetry/api';\nimport { getConfig } from './config';\nimport { getConfig as getInitConfig, getSdk } from './init';\nimport {\n type Sampler,\n type SamplingContext,\n AlwaysSampler,\n AUTOTEL_SAMPLING_TAIL_KEEP,\n AUTOTEL_SAMPLING_TAIL_EVALUATED,\n} from './sampling';\nimport { getEventQueue } from './track';\nimport type { TraceContext } from './trace-context';\nimport {\n createTraceContext,\n enterOrRun,\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 = trace(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\n/**\n * Helper type to extract function signature from factory pattern\n * This helps TypeScript infer types correctly for factory functions\n */\ntype ExtractFunctionSignature<T> = T extends (ctx: TraceContext) => infer F\n ? F extends (...args: infer Args) => infer Return\n ? (...args: Args) => Return\n : never\n : never;\n\n/**\n * Helper type to exclude functions that return functions from immediate execution overloads\n */\ntype ExcludeFactoryReturn<T> = T extends (ctx: TraceContext) => infer F\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any\n F extends (...args: any[]) => any\n ? never\n : T\n : T;\n\ntype GenericFunction = (...args: unknown[]) => unknown;\n\nconst FACTORY_NAME_HINTS = new Set([\n 'ctx',\n '_ctx',\n 'context',\n 'tracecontext',\n 'tracectx',\n]);\nconst TRACE_FACTORY_SET = new WeakSet<object>();\n\nconst SINGLE_LINE_COMMENT_REGEX = /\\/\\/.*$/gm;\nconst MULTI_LINE_COMMENT_REGEX = /\\/\\*[\\s\\S]*?\\*\\//gm;\nconst PARAM_TOKEN_SANITIZE_REGEX = new RegExp(String.raw`[{}\\[\\]\\s]`, 'g');\n\nfunction markAsTraceFactory(fn: object): void {\n TRACE_FACTORY_SET.add(fn);\n}\n\nfunction hasFactoryMark(fn: object): boolean {\n return TRACE_FACTORY_SET.has(fn);\n}\n\nfunction sanitizeParameterToken(token: string): string {\n const [firstToken] = token.split('=');\n return (firstToken ?? '').replaceAll(PARAM_TOKEN_SANITIZE_REGEX, '').trim();\n}\n\nfunction getFirstParameterToken(fn: GenericFunction): string | null {\n let source = Function.prototype.toString.call(fn);\n source = source\n .replaceAll(MULTI_LINE_COMMENT_REGEX, '')\n .replaceAll(SINGLE_LINE_COMMENT_REGEX, '')\n .trim();\n\n // Arrow functions\n const arrowMatch = source.match(\n /^(?:async\\s*)?(?:\\(([^)]*)\\)|([^=()]+))\\s*=>/,\n );\n if (arrowMatch) {\n const params = (arrowMatch[1] ?? arrowMatch[2] ?? '').split(',');\n const first = params[0]?.trim();\n if (first) {\n return sanitizeParameterToken(first);\n }\n return null;\n }\n\n // Function declarations/expressions\n const functionMatch = source.match(/^[^(]*\\(([^)]*)\\)/);\n if (functionMatch) {\n const params = functionMatch[1]?.split(',');\n const first = params?.[0]?.trim();\n if (first) {\n return sanitizeParameterToken(first);\n }\n }\n\n return null;\n}\n\n/**\n * Symbol that explicitly marks a function as immediate-execution-with-ctx\n * (`(ctx) => result`), bypassing parameter-name introspection. Library\n * authors who wrap user handlers — like `autotel-aws/lambda`'s `wrapHandler`\n * — should mark their inner trace function with this so dispatch survives\n * downstream bundlers that minify parameter names.\n */\nconst IMMEDIATE_EXECUTION_SYMBOL = Symbol.for('autotel.immediate-execution');\n\ntype ImmediateExecutionFlag = {\n [IMMEDIATE_EXECUTION_SYMBOL]?: true;\n};\n\nfunction hasImmediateExecutionMark(fn: unknown): boolean {\n return (\n typeof fn === 'function' &&\n (fn as ImmediateExecutionFlag)[IMMEDIATE_EXECUTION_SYMBOL] === true\n );\n}\n\n/**\n * Mark a function as immediate-execution-with-ctx so `trace(name, fn)`\n * dispatch doesn't depend on the first parameter being named `ctx`.\n *\n * Necessary when the function will be bundled by a minifier (esbuild,\n * terser, etc.) that renames identifiers. The name-allowlist heuristic in\n * `looksLikeTraceFactory` cannot recover from that; the marker can.\n *\n * @example\n * ```ts\n * import { markAsImmediate, trace } from 'autotel';\n *\n * const inner = markAsImmediate(async (ctx) => {\n * ctx.setAttribute('user.id', '123');\n * return { ok: true };\n * });\n * const result = await trace('user.read', inner);\n * ```\n */\nexport function markAsImmediate<F>(fn: F): F {\n if (typeof fn === 'function') {\n (fn as unknown as ImmediateExecutionFlag)[IMMEDIATE_EXECUTION_SYMBOL] =\n true;\n }\n return fn;\n}\n\nfunction looksLikeTraceFactory(fn: GenericFunction): boolean {\n if (hasFactoryMark(fn)) {\n return true;\n }\n if (hasImmediateExecutionMark(fn)) {\n return true;\n }\n\n if (fn.length === 0) {\n if (!isAsyncFunction(fn)) {\n try {\n const result = fn();\n return typeof result === 'function';\n } catch {\n return false;\n }\n }\n return false;\n }\n\n const firstParam = getFirstParameterToken(fn);\n if (!firstParam) {\n return false;\n }\n\n const normalized = firstParam.toLowerCase();\n if (\n FACTORY_NAME_HINTS.has(normalized) ||\n normalized.startsWith('ctx') ||\n normalized.startsWith('_ctx') ||\n normalized.startsWith('trace') ||\n normalized.endsWith('ctx') || // Match baseCtx, spanCtx, etc.\n normalized.includes('context') // Match traceContext, spanContext, etc.\n ) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Check if a function that takes ctx returns another function (factory pattern)\n * vs returning a value directly (immediate execution pattern)\n *\n * IMPORTANT: For async functions, we skip probing entirely and assume immediate execution.\n * This is because:\n * - Factory pattern: `(ctx) => async (...args) => result` - outer function is SYNC\n * - Immediate execution: `async (ctx) => result` - function itself is ASYNC\n *\n * Probing async functions by executing them causes side effects (like creating orphan spans)\n * because the async function starts executing synchronously until the first await.\n */\nfunction isFactoryReturningFunction(\n fnWithCtx: (ctx: TraceContext) => unknown,\n): boolean {\n // Async functions with ctx parameter are always immediate execution\n // because factory patterns have a sync outer function that returns the async inner\n if (isAsyncFunction(fnWithCtx)) {\n return false;\n }\n\n try {\n const result = fnWithCtx(createDummyCtx());\n return typeof result === 'function';\n } catch {\n // If the function throws when called with dummy ctx, assume it's immediate execution\n // since factory functions typically just return a function and don't execute logic\n return false;\n }\n}\n\nfunction isTraceFactoryFunction<TArgs extends unknown[], TReturn>(\n fn:\n | ((...args: TArgs) => TReturn)\n | ((ctx: TraceContext) => (...args: TArgs) => TReturn),\n): fn is (ctx: TraceContext) => (...args: TArgs) => TReturn {\n if (typeof fn !== 'function') {\n return false;\n }\n\n if (hasFactoryMark(fn)) {\n return true;\n }\n\n if (looksLikeTraceFactory(fn as GenericFunction)) {\n markAsTraceFactory(fn);\n return true;\n }\n\n return false;\n}\n\nfunction ensureTraceFactory<TArgs extends unknown[], TReturn>(\n fnOrFactory:\n | ((...args: TArgs) => TReturn | Promise<TReturn>)\n | ((ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn>),\n): (ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn> {\n if (isTraceFactoryFunction(fnOrFactory)) {\n return fnOrFactory;\n }\n\n const plainFn = fnOrFactory as (...args: TArgs) => TReturn | Promise<TReturn>;\n const factory = (ctx: TraceContext) => {\n void ctx;\n return plainFn;\n };\n markAsTraceFactory(factory);\n return factory;\n}\n\ntype WrappedFunction<TArgs extends unknown[], TReturn> = (\n ...args: TArgs\n) => TReturn | Promise<TReturn>;\n\nfunction wrapFactoryWithTracing<TArgs extends unknown[], TReturn>(\n fnOrFactory:\n | ((...args: TArgs) => TReturn | Promise<TReturn>)\n | ((ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn>),\n options: TracingOptions<TArgs, TReturn>,\n variableName?: string,\n): WrappedFunction<TArgs, TReturn> {\n const factory = ensureTraceFactory(fnOrFactory);\n\n // Get the inner function (the actual function being traced)\n const sampleFn = factory(createDummyCtx());\n\n // Infer function name with priority:\n // 1. Explicit variable name (from instrument() or explicit name parameter)\n // 2. Inner function name (named function expressions - e.g., \"async function createUser\")\n // 3. Variable name from call stack (inferred from const assignment, for arrow functions)\n // 4. Factory function name (for cases where factory itself is named)\n const innerFunctionName = inferFunctionName(\n sampleFn as InstrumentableFunction,\n );\n const callStackVariableName = innerFunctionName\n ? undefined\n : inferVariableNameFromCallStack(); // Only infer from call stack if no inner function name\n const factoryName = inferFunctionName(factory as InstrumentableFunction);\n const effectiveVariableName =\n variableName || innerFunctionName || callStackVariableName || factoryName;\n\n const useAsyncWrapper = isAsyncFunction(sampleFn);\n\n if (useAsyncWrapper) {\n return wrapWithTracing(\n factory as (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,\n options,\n effectiveVariableName,\n ) as WrappedFunction<TArgs, TReturn>;\n }\n\n return wrapWithTracingSync(\n factory as (ctx: TraceContext) => (...args: TArgs) => TReturn,\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/**\n * Options for instrument() batch instrumentation\n */\nexport interface InstrumentOptions<\n T extends Record<string, InstrumentableFunction> = Record<\n string,\n InstrumentableFunction\n >,\n> extends TracingOptions {\n /** Functions to instrument */\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// 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\nfunction isAsyncFunction(fn: unknown): boolean {\n return typeof fn === 'function' && fn.constructor?.name === 'AsyncFunction';\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\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 * 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 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 * 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(\n {},\n {\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/**\n * Core tracing wrapper for async functions (internal implementation)\n */\nfunction wrapWithTracing<TArgs extends unknown[], TReturn>(\n fnFactory: (\n ctx: TraceContext,\n ) => (...args: TArgs) => TReturn | Promise<TReturn>,\n options: TracingOptions<TArgs, TReturn>,\n variableName?: string,\n): (...args: TArgs) => Promise<TReturn> {\n // Idempotency check: if already instrumented, return as-is\n if (hasInstrumentationFlag(fnFactory)) {\n // Already instrumented - proceed\n }\n\n const config = getConfig();\n const tracer = config.tracer;\n const meter = config.meter;\n const sampler = options.sampler || new AlwaysSampler();\n\n const tempFn = fnFactory(createDummyCtx());\n const spanName = getSpanName(options, tempFn, variableName);\n\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 const wrappedFunction = async function wrappedFunction(\n this: unknown,\n ...args: TArgs\n ): Promise<TReturn> {\n const samplingContext: SamplingContext = {\n operationName: spanName,\n args,\n metadata: {},\n };\n\n const shouldSample = sampler.shouldSample(samplingContext);\n const needsTailSampling =\n 'needsTailSampling' in sampler &&\n typeof sampler.needsTailSampling === 'function'\n ? sampler.needsTailSampling()\n : false;\n\n if (!shouldSample && !needsTailSampling) {\n const fn = fnFactory(createDummyCtx());\n return await fn.call(this, ...args);\n }\n\n const startTime = performance.now();\n const isRootSpan =\n options.startNewRoot || otelTrace.getActiveSpan() === undefined;\n const shouldAutoFlush =\n options.flushOnRootSpanEnd ?? getInitConfig()?.flushOnRootSpanEnd ?? true;\n const shouldAutoFlushSpans = getInitConfig()?.forceFlushOnShutdown ?? false;\n\n const flushIfNeeded = async () => {\n if (!shouldAutoFlush || !isRootSpan) return;\n\n try {\n // Flush events queue\n const queue = getEventQueue();\n if (queue && queue.size() > 0) {\n await queue.flush();\n }\n\n // Flush OpenTelemetry spans if enabled\n if (shouldAutoFlushSpans) {\n const sdk = getSdk();\n if (sdk) {\n try {\n // Type assertion needed as getTracerProvider is not in the public NodeSDK interface\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const sdkAny = sdk as any;\n if (typeof sdkAny.getTracerProvider === 'function') {\n const tracerProvider = sdkAny.getTracerProvider();\n if (\n tracerProvider &&\n typeof tracerProvider.forceFlush === 'function'\n ) {\n await tracerProvider.forceFlush();\n }\n }\n } catch {\n // Ignore errors when accessing tracer provider (may not be available in test mocks)\n }\n }\n }\n } catch (error) {\n const initConfig = getInitConfig();\n const logger = initConfig?.logger;\n if (logger?.error) {\n logger.error(\n {\n err: error instanceof Error ? error : undefined,\n },\n `[autotel] Auto-flush failed${error instanceof Error ? '' : `: ${String(error)}`}`,\n );\n }\n }\n };\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 async (span) => {\n // Run within operation context so events can auto-capture operation.name\n return runInOperationContext(spanName, async () => {\n let shouldKeepSpan = true;\n\n setSpanName(span, spanName);\n\n // Initialize context storage with the active context BEFORE creating trace context\n const initialContext = context.active();\n const contextStorage = getContextStorage();\n if (!contextStorage.getStore()) {\n enterOrRun(contextStorage, initialContext);\n }\n\n const ctxValue = createTraceContext(span);\n const fn = fnFactory(ctxValue);\n const argsAttributes = {\n ...captureInputAttrs(args, options.captureInput),\n ...(options.attributesFromArgs\n ? options.attributesFromArgs(args)\n : {}),\n };\n\n const handleTailSampling = (\n success: boolean,\n duration: number,\n error?: unknown,\n ) => {\n if (\n needsTailSampling &&\n 'shouldKeepTrace' in sampler &&\n typeof sampler.shouldKeepTrace === 'function'\n ) {\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 = async (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 await flushIfNeeded();\n return result;\n };\n\n const onError = async (error: unknown): Promise<never> => {\n const duration = performance.now() - startTime;\n\n callCounter?.add(1, {\n operation: spanName,\n status: 'error',\n });\n\n durationHistogram?.record(duration, {\n operation: spanName,\n status: 'error',\n });\n\n const errorMessage =\n error instanceof Error ? error.message : 'Unknown error';\n const truncatedMessage = truncateErrorMessage(errorMessage);\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: truncatedMessage,\n });\n\n span.setAttributes({\n ...argsAttributes,\n 'operation.name': spanName,\n 'code.function': spanName,\n 'operation.duration': duration,\n 'operation.success': false,\n error: true,\n 'exception.type':\n error instanceof Error ? error.constructor.name : 'Error',\n 'exception.message': truncatedMessage,\n });\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\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n\n handleTailSampling(false, duration, error);\n\n span.end();\n await flushIfNeeded();\n throw error;\n };\n\n try {\n callCounter?.add(1, {\n operation: spanName,\n status: 'started',\n });\n\n // Execute the user's function with the updated context\n // This ensures ctx.setBaggage() changes are visible to OpenTelemetry operations\n // (like BaggageSpanProcessor, child spans, etc.)\n // We use getActiveContextWithBaggage() which checks the stored context,\n // so if baggage is set during execution, it will be picked up\n const executeWithContext = async () => {\n // Get the current context (may have been updated by ctx.setBaggage())\n const currentContext = getActiveContextWithBaggage();\n // Establish the context in OpenTelemetry's context manager\n return context.with(currentContext, async () => {\n return fn.call(this, ...args);\n });\n };\n const result = await executeWithContext();\n\n return await onSuccess(result);\n } catch (error) {\n await onError(error);\n throw error;\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: tempFn.name || 'trace',\n configurable: true,\n });\n\n return wrappedFunction;\n}\n\n/**\n * Core tracing wrapper for sync functions (internal implementation)\n */\nfunction wrapWithTracingSync<TArgs extends unknown[], TReturn>(\n fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,\n options: TracingOptions<TArgs, TReturn>,\n variableName?: string,\n): (...args: 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 const sampler = options.sampler || new AlwaysSampler();\n\n // We need to get a reference function name for span naming\n // Create a minimal dummy context just for extracting the function name\n // This won't affect actual tracing - we use the real context inside the span\n const tempFn = fnFactory(createDummyCtx());\n const spanName = getSpanName(options, tempFn, variableName);\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 const needsTailSampling =\n 'needsTailSampling' in sampler &&\n typeof sampler.needsTailSampling === 'function'\n ? sampler.needsTailSampling()\n : 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 // Track if this is a root span for auto-flush\n const isRootSpan =\n options.startNewRoot || otelTrace.getActiveSpan() === undefined;\n const shouldAutoFlush =\n options.flushOnRootSpanEnd ?? getInitConfig()?.flushOnRootSpanEnd ?? true;\n const shouldAutoFlushSpans = getInitConfig()?.forceFlushOnShutdown ?? false;\n\n // Note: This is intentionally fire-and-forget (void) for synchronous functions.\n // Synchronous functions cannot await flush completion without blocking execution.\n // The forceFlushOnShutdown guarantee only applies to async functions.\n const flushIfNeeded = () => {\n if (!shouldAutoFlush || !isRootSpan) return;\n\n // Flush events queue\n const queue = getEventQueue();\n if (queue && queue.size() > 0) {\n void queue.flush().catch((error) => {\n const initConfig = getInitConfig();\n const logger = initConfig?.logger;\n if (logger?.error) {\n logger.error(\n {\n err: error instanceof Error ? error : undefined,\n },\n `[autotel] Auto-flush failed${error instanceof Error ? '' : `: ${String(error)}`}`,\n );\n }\n });\n }\n\n // Flush OpenTelemetry spans if enabled\n if (shouldAutoFlushSpans) {\n const sdk = getSdk();\n if (sdk) {\n try {\n // Type assertion needed as getTracerProvider is not in the public NodeSDK interface\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const sdkAny = sdk as any;\n if (typeof sdkAny.getTracerProvider === 'function') {\n const tracerProvider = sdkAny.getTracerProvider();\n if (\n tracerProvider &&\n typeof tracerProvider.forceFlush === 'function'\n ) {\n void tracerProvider.forceFlush().catch((error: unknown) => {\n const initConfig = getInitConfig();\n const logger = initConfig?.logger;\n if (logger?.error) {\n logger.error(\n {\n err: error instanceof Error ? error : undefined,\n },\n `[autotel] Span flush failed${error instanceof Error ? '' : `: ${String(error)}`}`,\n );\n }\n });\n }\n }\n } catch {\n // Ignore errors when accessing tracer provider (may not be available in test mocks)\n }\n }\n }\n };\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 let shouldKeepSpan = true;\n\n // Store span name for trace context helpers\n setSpanName(span, spanName);\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 = {\n ...captureInputAttrs(args, options.captureInput),\n ...(options.attributesFromArgs\n ? options.attributesFromArgs(args)\n : {}),\n };\n\n const handleTailSampling = (\n success: boolean,\n duration: number,\n error?: unknown,\n ) => {\n if (\n needsTailSampling &&\n 'shouldKeepTrace' in sampler &&\n typeof sampler.shouldKeepTrace === 'function'\n ) {\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 void flushIfNeeded();\n return result;\n };\n\n const onError = (error: unknown): never => {\n const duration = performance.now() - startTime;\n\n callCounter?.add(1, {\n operation: spanName,\n status: 'error',\n });\n\n durationHistogram?.record(duration, {\n operation: spanName,\n status: 'error',\n });\n\n const errorMessage =\n error instanceof Error ? error.message : 'Unknown error';\n const truncatedMessage = truncateErrorMessage(errorMessage);\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: truncatedMessage,\n });\n\n span.setAttributes({\n ...argsAttributes,\n 'operation.name': spanName,\n 'code.function': spanName,\n 'operation.duration': duration,\n 'operation.success': false,\n error: true,\n 'exception.type':\n error instanceof Error ? error.constructor.name : 'Error',\n 'exception.message': truncatedMessage,\n });\n\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n\n handleTailSampling(false, duration, error);\n\n span.end();\n void flushIfNeeded();\n throw error;\n };\n\n try {\n callCounter?.add(1, {\n oper