UNPKG

autotel-cloudflare

Version:

The #1 OpenTelemetry package for Cloudflare Workers - complete bindings coverage, native CF OTel integration, advanced sampling

737 lines (729 loc) 26.3 kB
import { _ as trapArgs, a as asRecord, c as asString, f as member, g as readProperty, i as asNumber, n as asBoolean, r as asFunction, t as applyTrap } from "./values-BC6rdpGG.js"; import { a as getWorkflowLogger, i as getRequestLogger, n as getActorLogger, r as getQueueLogger, t as createWorkersLogger } from "./execution-logger-6IZS5fEz.js"; import { n as isNativeTracingAvailable, t as getNativeTracerFromCtx } from "./native-tracing-D0EyAtsF.js"; import { a as unwrap, o as wrap, r as proxyExecutionContext, t as toException } from "./exception-D3xOCdBW.js"; import { t as workerTracer } from "./tracer-z8zwRBVS.js"; import { a as instrumentKV, c as instrumentImages, d as instrumentHyperdrive, f as instrumentVectorize, i as instrumentD1, l as instrumentAnalyticsEngine, n as instrumentRateLimiter, o as instrumentR2, p as instrumentAI, r as instrumentBindings, s as instrumentServiceBinding, t as instrumentBrowserRendering, u as instrumentQueueProducer } from "./bindings-BzekkOXx.js"; import { n as instrumentDO, t as instrumentWorkflow } from "./handlers-avm65CVU.js"; import { WorkerTracer, WorkerTracerProvider, createInitialiser, ensureGlobalContextManager, getActiveConfig, getServiceForPath, setConfig, shouldInstrumentPath, withNativeTracer } from "autotel-edge"; import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api"; import { resourceFromAttributes } from "@opentelemetry/resources"; export * from "autotel-edge" //#region src/global/fetch.ts /** * Global fetch() instrumentation for autotel-edge * * Automatically traces all outgoing fetch() calls with: * - HTTP method, URL, status code * - Request/response headers * - Automatic context propagation * - Error tracking */ /** * Gather HTTP request attributes following OpenTelemetry semantic conventions */ function gatherRequestAttributes(request) { const url = new URL(request.url); const redactQuery = getActiveConfig()?.dataSafety?.redactQueryParams === true; return { "http.request.method": request.method.toUpperCase(), "url.full": redactQuery ? `${url.origin}${url.pathname}` : request.url, "url.scheme": url.protocol.replace(":", ""), "server.address": url.host, "url.path": url.pathname, "url.query": redactQuery ? url.search ? "[REDACTED]" : "" : url.search, "network.protocol.name": "http", "user_agent.original": request.headers.get("user-agent") || void 0 }; } /** * Gather HTTP response attributes */ function gatherResponseAttributes(response) { return { "http.response.status_code": response.status, "http.response.body.size": response.headers.get("content-length") || void 0 }; } /** * Instrument the global fetch function * * This wraps globalThis.fetch to automatically create spans for all outgoing HTTP requests. * * **Note:** This is called automatically when the library is initialized with * `instrumentation.instrumentGlobalFetch: true` (default). */ function instrumentGlobalFetch() { const originalFetch = globalThis.fetch; globalThis.fetch = function fetch(input, init) { const request = new Request(input, init); if (!request.url.startsWith("http")) return originalFetch(input, init); const config = getActiveConfig(); if (!config) return originalFetch(input, init); const tracer = workerTracer("autotel-edge"); const url = new URL(request.url); const spanName = `${request.method} ${url.host}`; return tracer.startActiveSpan(spanName, { kind: SpanKind.CLIENT, attributes: gatherRequestAttributes(request) }, async (span) => { try { if (typeof config.fetch?.includeTraceContext === "function" ? config.fetch.includeTraceContext(request) : config.fetch?.includeTraceContext ?? true) propagation.inject(context.active(), request.headers, { set: (headers, key, value) => { if (typeof value === "string") headers.set(key, value); } }); const response = await originalFetch(request); span.setAttributes(gatherResponseAttributes(response)); if (response.ok) span.setStatus({ code: SpanStatusCode.OK }); else span.setStatus({ code: SpanStatusCode.ERROR }); return response; } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } }); }; } //#endregion //#region src/global/cache.ts /** * Global Cache API instrumentation for Cloudflare Workers * * Automatically traces cache operations: * - cache.match() - Read from cache * - cache.put() - Write to cache * - cache.delete() - Delete from cache */ /** * Sanitize URL for span attributes (remove query params that might contain sensitive data) */ function sanitizeURL(url) { const u = new URL(url); return `${u.protocol}//${u.host}${u.pathname}`; } /** * Instrument a cache method (match, put, delete) */ function instrumentCacheMethod(fn, cacheName, operation) { return wrap(fn, { async apply(target, thisArg, argArray) { const tracer = workerTracer("autotel-edge"); const firstArg = argArray[0]; const url = firstArg instanceof Request ? firstArg.url : typeof firstArg === "string" ? firstArg : void 0; const spanName = `Cache ${cacheName}.${operation}`; return tracer.startActiveSpan(spanName, { kind: SpanKind.CLIENT, attributes: { "cache.name": cacheName, "cache.operation": operation, "cache.key": url ? sanitizeURL(url) : void 0 } }, async (span) => { try { const result = await target.apply(thisArg, argArray); if (operation === "match") span.setAttribute("cache.hit", !!result); span.setStatus({ code: SpanStatusCode.OK }); return result; } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); throw error; } finally { span.end(); } }); } }); } /** * Instrument a Cache instance */ function instrumentCache(cache, cacheName) { return wrap(cache, { get(target, prop) { const value = member(target, prop); const method = asFunction(value); if ((prop === "match" || prop === "put" || prop === "delete") && method !== void 0) return instrumentCacheMethod(method.bind(target), cacheName, prop); if (method !== void 0) return method.bind(target); return value; } }); } /** * Instrument caches.open() */ function instrumentCachesOpen(openFn) { return wrap(openFn, { async apply(target, thisArg, argArray) { const cacheName = argArray[0]; return instrumentCache(await applyTrap(target, thisArg, argArray), cacheName); } }); } /** * Instrument the global caches API * * This wraps globalThis.caches to automatically create spans for all cache operations. * * **Note:** This is called automatically when the library is initialized with * `instrumentation.instrumentGlobalCache: true` (default). */ function instrumentGlobalCache() { globalThis.caches = wrap(caches, { get(target, prop) { if (prop === "default") return instrumentCache(target.default, "default"); else if (prop === "open") { const openFn = member(target, prop); if (typeof openFn === "function") return instrumentCachesOpen(openFn.bind(target)); } return member(target, prop); } }); } //#endregion //#region src/wrappers/instrument.ts /** * Handler instrumentation for Cloudflare Workers * * Note: This file uses Cloudflare Workers types (ExportedHandler, Request, Response, etc.) * which are globally available via @cloudflare/workers-types when listed in tsconfig.json. * These types are devDependencies only - they're not runtime dependencies. * At runtime, Cloudflare Workers runtime provides the actual implementations. * * Provides automatic OpenTelemetry tracing for: * - HTTP handlers (fetch) * - Scheduled/cron handlers * - Queue handlers (with message tracking) * - Email handlers * - Auto-instrumentation of Cloudflare bindings (KV, R2, D1, Service Bindings) * - Global fetch and cache instrumentation * - Post-processor support for span customization * - Tail sampling support * - Cold start tracking */ const headersGetter = { get: (carrier, key) => carrier.get(key) ?? void 0, keys: (carrier) => [...carrier.keys()] }; /** * Create fetch handler instrumentation with config support for postProcess */ /** * Extract Cloudflare-specific attributes from a request */ function extractCfAttributes(request) { const cf = asRecord(member(request, "cf")); if (!cf) return {}; const attrs = {}; const set = (key, value) => { const scalar = asString(value) ?? asNumber(value) ?? asBoolean(value); if (scalar !== void 0) attrs[key] = scalar; }; set("cloudflare.colo", cf.colo); const ray = request.headers.get("cf-ray"); if (ray) attrs["cloudflare.ray_id"] = ray; set("cloudflare.country", cf.country); set("cloudflare.city", cf.city); set("cloudflare.region", cf.region); set("cloudflare.continent", cf.continent); set("cloudflare.timezone", cf.timezone); set("cloudflare.latitude", cf.latitude); set("cloudflare.longitude", cf.longitude); set("cloudflare.asn", cf.asn); set("cloudflare.as_organization", cf.asOrganization); set("cloudflare.http_protocol", cf.httpProtocol); set("cloudflare.tls_version", cf.tlsVersion); set("cloudflare.client_tcp_rtt", cf.clientTcpRtt); return attrs; } function createFetchInstrumentation(config) { return { getInitialSpanInfo: (request) => { const url = new URL(request.url); const routeService = getServiceForPath(url.pathname, config.handlers.fetch.routes); const cfAttrs = readProperty(config, "extractCfAttributes") === false ? {} : extractCfAttributes(request); return { name: `${request.method} ${url.pathname}`, options: { kind: SpanKind.SERVER, attributes: { "http.request.method": request.method, "url.full": request.url, ...routeService ? { "service.name": routeService, "autotel.route.service": routeService } : {}, ...cfAttrs } }, context: propagation.extract(context.active(), request.headers, headersGetter) }; }, getAttributesFromResult: (response) => ({ "http.response.status_code": response.status }), executionSucces: (span, trigger, result) => { if (result.status >= 500) span.setStatus({ code: SpanStatusCode.ERROR }); if (config.handlers.fetch.postProcess) { const readableSpan = span; config.handlers.fetch.postProcess(span, { request: trigger, response: result, readable: readableSpan }); } } }; } /** * Scheduled handler instrumentation */ const scheduledInstrumentation = { getInitialSpanInfo: (event) => { return { name: `scheduledHandler ${event.cron || "unknown"}`, options: { kind: SpanKind.INTERNAL, attributes: { "faas.trigger": "timer", "faas.cron": event.cron || "unknown", "faas.scheduled_time": new Date(event.scheduledTime).toISOString() } } }; } }; /** * Tracks message status counts for queue processing */ var MessageStatusCount = class { succeeded = 0; failed = 0; implicitly_acked = 0; implicitly_retried = 0; total; constructor(total) { this.total = total; } ack() { this.succeeded = this.succeeded + 1; } ackRemaining() { this.implicitly_acked = this.total - this.succeeded - this.failed; this.succeeded = this.total - this.failed; } retry() { this.failed = this.failed + 1; } retryRemaining() { this.implicitly_retried = this.total - this.succeeded - this.failed; this.failed = this.total - this.succeeded; } toAttributes() { return { "queue.messages_count": this.total, "queue.messages_success": this.succeeded, "queue.messages_failed": this.failed, "queue.batch_success": this.succeeded === this.total, "queue.implicitly_acked": this.implicitly_acked, "queue.implicitly_retried": this.implicitly_retried }; } }; /** * Add event to active span */ function addQueueEvent(name, msg, delaySeconds) { const attrs = {}; if (msg) { attrs["queue.message_id"] = msg.id; attrs["queue.message_timestamp"] = msg.timestamp.toISOString(); if ("attempts" in msg && typeof msg.attempts === "number") attrs["queue.message_attempts"] = msg.attempts; } if (delaySeconds !== void 0) attrs["queue.retry_delay_seconds"] = delaySeconds; trace.getActiveSpan()?.addEvent(name, attrs); } /** * Proxy a queue message to track ack/retry operations */ function proxyQueueMessage(msg, count) { return wrap(msg, { get: (target, prop) => { const messageFn = asFunction(member(target, prop)); if (prop === "ack" && messageFn) return new Proxy(messageFn, { apply: (fnTarget) => { addQueueEvent("messageAck", msg); count.ack(); fnTarget.apply(msg, []); } }); else if (prop === "retry" && messageFn) return new Proxy(messageFn, { apply: (fnTarget, _thisArg, args) => { const retryOptions = trapArgs(args)[0]; const delaySeconds = retryOptions?.delaySeconds; addQueueEvent("messageRetry", msg, delaySeconds); if (retryOptions?.contentType) { const span = trace.getActiveSpan(); if (span) span.setAttribute("queue.message.content_type", retryOptions.contentType); } count.retry(); return applyTrap(fnTarget, msg, args); } }); else return member(target, prop); } }); } /** * Proxy MessageBatch to track ackAll/retryAll operations */ function proxyMessageBatch(batch, count) { return wrap(batch, { get: (target, prop) => { if (prop === "messages") return wrap(target.messages, { get: (target, prop) => { const index = asString(prop); if (index !== void 0 && !Number.isNaN(Number.parseInt(index))) return proxyQueueMessage(member(target, prop), count); return member(target, prop); } }); const batchFn = asFunction(member(target, prop)); if (prop === "ackAll" && batchFn) return new Proxy(batchFn, { apply: (fnTarget) => { addQueueEvent("ackAll"); count.ackRemaining(); fnTarget.apply(batch, []); } }); else if (prop === "retryAll" && batchFn) return new Proxy(batchFn, { apply: (fnTarget, _thisArg, args) => { const delaySeconds = trapArgs(args)[0]?.delaySeconds; addQueueEvent("retryAll", void 0, delaySeconds); count.retryRemaining(); applyTrap(fnTarget, batch, args); } }); return member(target, prop); } }); } /** * Queue handler instrumentation with message tracking */ var QueueInstrumentation = class { count; getInitialSpanInfo(batch) { return { name: `queueHandler ${batch.queue || "unknown"}`, options: { kind: SpanKind.CONSUMER, attributes: { "faas.trigger": "pubsub", "queue.name": batch.queue || "unknown" } } }; } instrumentTrigger(batch) { this.count = new MessageStatusCount(batch.messages.length); return proxyMessageBatch(batch, this.count); } executionSucces(span, _trigger, _result) { if (this.count) { this.count.ackRemaining(); span.setAttributes(this.count.toAttributes()); } } executionFailed(span, _trigger, _error) { if (this.count) { this.count.retryRemaining(); span.setAttributes(this.count.toAttributes()); } } }; /** * Converts email headers into OpenTelemetry attributes. * When dataSafety.emailHeaderAllowlist is configured, only allowed headers are captured. */ function headerAttributes(message) { const attrs = {}; if (message.headers instanceof Headers) { const allowlist = getActiveConfig()?.dataSafety?.emailHeaderAllowlist; for (const [key, value] of message.headers.entries()) { if (allowlist && !allowlist.includes(key.toLowerCase())) continue; attrs[`email.header.${key}`] = value; } } return attrs; } /** * Email handler instrumentation */ const emailInstrumentation = { getInitialSpanInfo: (message) => { const attributes = { "faas.trigger": "other", "messaging.destination.name": message.to || "unknown" }; if ("headers" in message && message.headers instanceof Headers) { const messageId = message.headers.get("Message-Id"); if (messageId) attributes["rpc.message.id"] = messageId; Object.assign(attributes, headerAttributes(message)); } return { name: `emailHandler ${message.to || "unknown"}`, options: { kind: SpanKind.CONSUMER, attributes } }; } }; /** * Export spans after request completes */ async function exportSpans(traceId, tracker, ctx) { const tracer = trace.getTracer("autotel-edge"); if (tracer instanceof WorkerTracer) try { const ctxWithScheduler = ctx; if (ctxWithScheduler.scheduler) await ctxWithScheduler.scheduler.wait(1); await tracker?.wait(); await tracer.forceFlush(traceId); } catch (error) { console.error("[autotel-edge] Failed to export spans:", error); } } /** * Create handler flow with instrumentation */ function createHandlerFlow(instrumentation) { return (handlerFn, [trigger, env, context$1]) => { const { ctx: proxiedCtx, tracker } = proxyExecutionContext(context$1); const tracer = workerTracer("autotel-edge"); const { name, options, context: spanContext } = instrumentation.getInitialSpanInfo(trigger); if (options.attributes) options.attributes["faas.coldstart"] = coldStart; else options.attributes = { "faas.coldstart": coldStart }; coldStart = false; const parentContext = spanContext || context.active(); const instrumentedTrigger = instrumentation.instrumentTrigger ? instrumentation.instrumentTrigger(trigger) : trigger; return tracer.startActiveSpan(name, options, parentContext, async (span) => { try { const result = await handlerFn(instrumentedTrigger, env, proxiedCtx); if (instrumentation.getAttributesFromResult) { const attributes = instrumentation.getAttributesFromResult(result); span.setAttributes(attributes); } span.setStatus({ code: SpanStatusCode.OK }); if (instrumentation.executionSucces) instrumentation.executionSucces(span, trigger, result); return result; } catch (error) { span.recordException(toException(error)); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); if (instrumentation.executionFailed) instrumentation.executionFailed(span, trigger, error); throw error; } finally { span.end(); context$1.waitUntil(exportSpans(span.spanContext().traceId, tracker, context$1)); } }); }; } let warnedMissingNative = false; /** * Resolve whether this invocation should use Cloudflare's native tracer. * * - `nativeTracing: 'off'` → never use native (always autotel's OTLP pipeline). * - `nativeTracing: 'on'` → require native; warn once and fall back if absent. * - `nativeTracing: 'auto'` (default) → use native when `ctx.tracing` exists. */ function resolveNativeTracer(config, ctx, trigger) { const mode = config.nativeTracing ?? "auto"; if (mode === "off") return null; const nativeTracer = getNativeTracerFromCtx(ctx, (trigger instanceof Request ? trigger.headers.get("cf-ray") : null) ?? crypto.randomUUID()); if (!nativeTracer && mode === "on" && !warnedMissingNative) { warnedMissingNative = true; console.warn("[autotel-cloudflare] nativeTracing is 'on' but Cloudflare native tracing was not detected. Enable observability.traces in your Wrangler config (and use a recent compatibility_date). Falling back to the autotel OTLP exporter."); } return nativeTracer; } /** * Run a handler in native-tracing mode. * * Cloudflare already creates the root handler span and instruments platform * operations (fetch / KV / R2 / D1 / DO), so autotel defers entirely: no * binding proxies (avoids duplicate spans), no provider/exporter, no manual * flush. We install the native tracer into the active context so that the * user's `span()` / `trace()` / `enterSpan()` calls nest inside Cloudflare's * native waterfall and are exported by the platform. */ function runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer) { ensureGlobalContextManager(); const nativeContext = withNativeTracer(nativeTracer, setConfig(config)); return context.with(nativeContext, () => handlerFn(trigger, env, ctx)); } /** * Create handler proxy */ function createHandlerProxy(_handler, handlerFn, initialiser, instrumentation) { return (trigger, env, ctx) => { const config = initialiser(env, trigger); if (config.instrumentation.disabled) return handlerFn(trigger, env, ctx); const nativeTracer = resolveNativeTracer(config, ctx, trigger); if (nativeTracer) return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer); const instrumentedEnv = instrumentBindings(env); const configContext = setConfig(config); initProvider(config); const flowFn = createHandlerFlow(instrumentation); return context.with(configContext, () => { return flowFn(handlerFn, [ trigger, instrumentedEnv, ctx ]); }); }; } /** * Create handler proxy with dynamic instrumentation (for fetch with postProcess) */ function createHandlerProxyWithConfig(_handler, handlerFn, initialiser, createInstrumentation) { return (trigger, env, ctx) => { const config = initialiser(env, trigger); if (config.instrumentation.disabled) return handlerFn(trigger, env, ctx); if (trigger instanceof Request) { const pathname = new URL(trigger.url).pathname; const fetchCfg = config.handlers.fetch; if (!shouldInstrumentPath(pathname, { include: fetchCfg.include, exclude: fetchCfg.exclude })) return handlerFn(trigger, env, ctx); } const nativeTracer = resolveNativeTracer(config, ctx, trigger); if (nativeTracer) return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer); const instrumentedEnv = instrumentBindings(env); const configContext = setConfig(config); initProvider(config); const flowFn = createHandlerFlow(createInstrumentation(config)); return context.with(configContext, () => { return flowFn(handlerFn, [ trigger, instrumentedEnv, ctx ]); }); }; } let providerInitialized = false; let coldStart = true; /** * Initialize the tracer provider */ function initProvider(config) { if (providerInitialized) return; if (config.instrumentation.instrumentGlobalFetch) instrumentGlobalFetch(); if (config.instrumentation.instrumentGlobalCache) instrumentGlobalCache(); propagation.setGlobalPropagator(config.propagator); const resource = resourceFromAttributes({ "service.name": config.service.name, "service.version": config.service.version, "service.namespace": config.service.namespace, "cloud.provider": "cloudflare", "cloud.platform": "cloudflare.workers", "telemetry.sdk.name": "autotel-edge", "telemetry.sdk.language": "js" }); new WorkerTracerProvider(config.spanProcessors, resource).register(); workerTracer("autotel-edge").setHeadSampler(config.sampling.headSampler); providerInitialized = true; } /** * Instrument a Cloudflare Workers handler * * @example * ```typescript * import { instrument } from 'autotel-edge' * * const handler = { * async fetch(request, env, ctx) { * return new Response('Hello World') * } * } * * export default instrument(handler, { * exporter: { * url: env.OTLP_ENDPOINT, * headers: { 'x-api-key': env.API_KEY } * }, * service: { name: 'my-worker' } * }) * ``` */ function instrument(handler, config) { const initialiser = createInitialiser(config); if (handler.fetch) handler.fetch = createHandlerProxyWithConfig(handler, unwrap(handler.fetch), initialiser, createFetchInstrumentation); if (handler.scheduled) handler.scheduled = createHandlerProxy(handler, unwrap(handler.scheduled), initialiser, scheduledInstrumentation); if (handler.queue) handler.queue = createHandlerProxy(handler, unwrap(handler.queue), initialiser, new QueueInstrumentation()); if (handler.email) handler.email = createHandlerProxy(handler, unwrap(handler.email), initialiser, emailInstrumentation); return handler; } //#endregion //#region src/wrappers/wrap-module.ts /** * workers-honeycomb-logger style wrapper API * * @example * ```typescript * import { wrapModule } from 'autotel-cloudflare' * * const handler = { * async fetch(req, env, ctx) { * return new Response('Hello') * } * } * * export default wrapModule( * { service: { name: 'my-worker' } }, * handler * ) * ``` */ /** * Wrap a Cloudflare Workers module-style handler * Alternative API style inspired by workers-honeycomb-logger * * @param config Configuration (can be static object or function) * @param handler The worker handler to wrap * @returns Instrumented handler */ function wrapModule(config, handler) { return instrument(handler, config); } //#endregion //#region src/wrappers/wrap-do.ts /** * Durable Object wrapper * * @example * ```typescript * import { wrapDurableObject } from 'autotel-cloudflare' * * class Counter implements DurableObject { * async fetch(request: Request) { * return new Response('count') * } * } * * export default wrapDurableObject({ service: { name: 'counter-do' } }, Counter) * ``` */ /** * Wrap a Durable Object class with instrumentation * Alternative API style inspired by workers-honeycomb-logger * * @param config Configuration (can be static object or function) * @param doClass The Durable Object class to wrap * @returns Instrumented Durable Object class */ function wrapDurableObject(config, doClass) { return instrumentDO(doClass, config); } //#endregion //#region src/wrappers/define-worker-fetch.ts /** * Wrap a Workers fetch handler so: * - the handler is instrumented (spans, propagation, waitUntil export flush) * - the handler receives a request-scoped logger as its fourth argument */ function defineWorkerFetch(config, handler, loggerOptions = {}) { const wrapped = instrument({ fetch(request, env, ctx) { return handler(request, env, ctx, createWorkersLogger(request, loggerOptions)); } }, config); return { fetch(request, env, ctx) { return Promise.resolve(wrapped.fetch(request, env, ctx)); } }; } //#endregion export { createWorkersLogger, defineWorkerFetch, getActorLogger, getNativeTracerFromCtx, getQueueLogger, getRequestLogger, getWorkflowLogger, instrument, instrumentAI, instrumentAnalyticsEngine, instrumentBindings, instrumentBrowserRendering, instrumentD1, instrumentDO, instrumentGlobalCache, instrumentGlobalFetch, instrumentHyperdrive, instrumentImages, instrumentKV, instrumentQueueProducer, instrumentR2, instrumentRateLimiter, instrumentServiceBinding, instrumentVectorize, instrumentWorkflow, isNativeTracingAvailable, wrapDurableObject, wrapModule };