@heystack/otel 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ export const NATIVELY_COVERED = Object.freeze({
2
+ d1: true,
3
+ kv: true,
4
+ r2: true,
5
+ durable_object: true,
6
+ queue: true,
7
+ ratelimit: true,
8
+ fetch: true,
9
+ // Cloudflare propagates Worker-to-Worker calls automatically and emits `jsrpc.*` spans.
10
+ service: true,
11
+ ai: false,
12
+ vectorize: false,
13
+ workflow: false,
14
+ hyperdrive: false,
15
+ analytics_engine: false,
16
+ });
17
+ /** Native tracing does not propagate trace context to external (non-Cloudflare) services. */
18
+ export const NATIVE_PROPAGATES_EXTERNAL = false;
19
+ /** `enrich?.[kind] ?? !table[kind]` — `table` is a test seam. */
20
+ export function shouldEnrichNatively(kind, enrich, table = NATIVELY_COVERED) {
21
+ return enrich?.[kind] ?? !table[kind];
22
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Cloudflare-native custom span support (WinterCG-safe: only `@opentelemetry/api`
3
+ * and relative modules — no `node:*`).
4
+ *
5
+ * Cloudflare's `ctx.tracing` custom span API exposes NO trace/span ids, so
6
+ * `NativeOtelSpan.spanContext()` always returns INVALID_SPAN_CONTEXT — ids are
7
+ * never fabricated.
8
+ */
9
+ import { type Span, type SpanAttributes, type SpanAttributeValue, type SpanContext, type SpanStatus as ApiSpanStatus, type Link, type Exception, type TimeInput } from "@opentelemetry/api";
10
+ type Attr = string | number | boolean | undefined;
11
+ /** Minimal public surface exposed by Cloudflare's Workers custom-spans API. */
12
+ export interface CloudflareSpan {
13
+ readonly isTraced?: boolean;
14
+ setAttribute(key: string, value: Attr): unknown;
15
+ setAttributes?(attributes: Record<string, Attr>): unknown;
16
+ recordException?(exception: unknown): void;
17
+ end?(): void;
18
+ }
19
+ /** `ExecutionContext.tracing` without importing the `cloudflare:workers` module. */
20
+ export interface CloudflareTracing {
21
+ enterSpan<T, A extends unknown[]>(name: string, callback: (span: CloudflareSpan, ...args: A) => T, ...args: A): T;
22
+ startActiveSpan?<T>(name: string, callback: (span: CloudflareSpan) => T): T;
23
+ startSpan?(name: string): CloudflareSpan;
24
+ getActiveSpan?(): CloudflareSpan | undefined;
25
+ }
26
+ /** @deprecated Use {@link CloudflareSpan}. */
27
+ export type CloudflareNativeSpan = CloudflareSpan;
28
+ /** @deprecated Use {@link CloudflareTracing}. */
29
+ export type CloudflareNativeTracing = CloudflareTracing;
30
+ /**
31
+ * Context key under which `runNativeSpan` / `withSpan` publish the request's
32
+ * Cloudflare native tracer, so nested helpers (and the fetch wrapper) can join
33
+ * the platform trace tree instead of creating OTel-provider spans.
34
+ */
35
+ export declare const NATIVE_TRACING_CONTEXT_KEY: symbol;
36
+ export declare function nativeTracingFromContext(): CloudflareTracing | undefined;
37
+ /** workers.ts registers its global ContextManager bootstrap here (avoids an import cycle). */
38
+ export declare function setNativeContextInit(fn: () => void): void;
39
+ /**
40
+ * Adapts Cloudflare's deliberately-small Span API to the OTel API shape used by
41
+ * `withSpan`. Unsupported OTel operations degrade to attributes instead of
42
+ * creating a second exporter/provider.
43
+ */
44
+ export declare class NativeOtelSpan implements Span {
45
+ private readonly nativeSpan;
46
+ private ended;
47
+ /** True once `recordException` stamped a specific `error.type` (the exception class). */
48
+ private errorTyped;
49
+ constructor(nativeSpan: CloudflareSpan);
50
+ spanContext(): SpanContext;
51
+ setAttribute(key: string, value: SpanAttributeValue): this;
52
+ setAttributes(attributes: SpanAttributes): this;
53
+ addEvent(name: string, attributesOrStartTime?: SpanAttributes | TimeInput): this;
54
+ addLink(_link: Link): this;
55
+ addLinks(_links: Link[]): this;
56
+ setStatus(status: ApiSpanStatus): this;
57
+ updateName(_name: string): this;
58
+ /** Calls the native `end` exactly once. */
59
+ end(_endTime?: TimeInput): void;
60
+ isRecording(): boolean;
61
+ recordException(exception: Exception, _time?: TimeInput): void;
62
+ }
63
+ /** Resolve the native tracer: explicit config override first, then `ctx.tracing`. */
64
+ export declare function getNativeTracing(ctx: unknown, override?: CloudflareTracing): CloudflareTracing | null;
65
+ export declare function runNativeSpan<T>(tracing: CloudflareTracing | null, name: string, attrs: Record<string, Attr>, fn: (span: NativeOtelSpan) => T | Promise<T>): Promise<T>;
66
+ export {};
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Cloudflare-native custom span support (WinterCG-safe: only `@opentelemetry/api`
3
+ * and relative modules — no `node:*`).
4
+ *
5
+ * Cloudflare's `ctx.tracing` custom span API exposes NO trace/span ids, so
6
+ * `NativeOtelSpan.spanContext()` always returns INVALID_SPAN_CONTEXT — ids are
7
+ * never fabricated.
8
+ */
9
+ import { context, createContextKey, INVALID_SPAN_CONTEXT, trace, SpanStatusCode, } from "@opentelemetry/api";
10
+ /**
11
+ * Context key under which `runNativeSpan` / `withSpan` publish the request's
12
+ * Cloudflare native tracer, so nested helpers (and the fetch wrapper) can join
13
+ * the platform trace tree instead of creating OTel-provider spans.
14
+ */
15
+ export const NATIVE_TRACING_CONTEXT_KEY = createContextKey("heystack.cloudflare.native-tracing");
16
+ export function nativeTracingFromContext() {
17
+ return context.active().getValue(NATIVE_TRACING_CONTEXT_KEY);
18
+ }
19
+ let contextInit;
20
+ /** workers.ts registers its global ContextManager bootstrap here (avoids an import cycle). */
21
+ export function setNativeContextInit(fn) {
22
+ contextInit = fn;
23
+ }
24
+ /**
25
+ * Adapts Cloudflare's deliberately-small Span API to the OTel API shape used by
26
+ * `withSpan`. Unsupported OTel operations degrade to attributes instead of
27
+ * creating a second exporter/provider.
28
+ */
29
+ export class NativeOtelSpan {
30
+ nativeSpan;
31
+ ended = false;
32
+ /** True once `recordException` stamped a specific `error.type` (the exception class). */
33
+ errorTyped = false;
34
+ constructor(nativeSpan) {
35
+ this.nativeSpan = nativeSpan;
36
+ }
37
+ spanContext() { return INVALID_SPAN_CONTEXT; }
38
+ setAttribute(key, value) {
39
+ try {
40
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
41
+ this.nativeSpan.setAttribute(key, value);
42
+ }
43
+ else if (Array.isArray(value)) {
44
+ this.nativeSpan.setAttribute(key, JSON.stringify(value));
45
+ }
46
+ }
47
+ catch {
48
+ // Telemetry must never surface as an application error; drop the attribute.
49
+ }
50
+ return this;
51
+ }
52
+ setAttributes(attributes) {
53
+ for (const [key, value] of Object.entries(attributes)) {
54
+ if (value !== undefined)
55
+ this.setAttribute(key, value);
56
+ }
57
+ return this;
58
+ }
59
+ addEvent(name, attributesOrStartTime) {
60
+ const prefix = `event.${name}`;
61
+ this.setAttribute(prefix, true);
62
+ if (attributesOrStartTime && !Array.isArray(attributesOrStartTime) && typeof attributesOrStartTime === "object") {
63
+ for (const [key, value] of Object.entries(attributesOrStartTime)) {
64
+ if (value !== undefined)
65
+ this.setAttribute(`${prefix}.${key}`, value);
66
+ }
67
+ }
68
+ return this;
69
+ }
70
+ addLink(_link) { return this; }
71
+ addLinks(_links) { return this; }
72
+ setStatus(status) {
73
+ if (status.code === SpanStatusCode.ERROR) {
74
+ // Don't clobber the more specific class name a preceding recordException set.
75
+ if (!this.errorTyped)
76
+ this.setAttribute("error.type", "error");
77
+ if (status.message)
78
+ this.setAttribute("error.message", status.message);
79
+ this.setAttribute("otel.status_code", "ERROR");
80
+ }
81
+ return this;
82
+ }
83
+ updateName(_name) { return this; }
84
+ /** Calls the native `end` exactly once. */
85
+ end(_endTime) {
86
+ if (this.ended)
87
+ return;
88
+ this.ended = true;
89
+ try {
90
+ this.nativeSpan.end?.();
91
+ }
92
+ catch { /* never surface */ }
93
+ }
94
+ isRecording() { return this.nativeSpan.isTraced !== false; }
95
+ recordException(exception, _time) {
96
+ const error = exception instanceof Error ? exception : new Error(String(exception));
97
+ this.setAttribute("error.type", error.name || "Error");
98
+ this.errorTyped = true;
99
+ this.setAttribute("error.message", error.message);
100
+ if (error.stack)
101
+ this.setAttribute("exception.stacktrace", error.stack);
102
+ try {
103
+ this.nativeSpan.recordException?.(error);
104
+ }
105
+ catch { /* never surface */ }
106
+ }
107
+ }
108
+ /** Resolve the native tracer: explicit config override first, then `ctx.tracing`. */
109
+ export function getNativeTracing(ctx, override) {
110
+ if (override && typeof override.enterSpan === "function")
111
+ return override;
112
+ const candidate = ctx?.tracing;
113
+ return candidate && typeof candidate.enterSpan === "function" ? candidate : null;
114
+ }
115
+ const NOOP_NATIVE_SPAN = { isTraced: false, setAttribute: () => undefined };
116
+ export async function runNativeSpan(tracing, name, attrs, fn) {
117
+ contextInit?.();
118
+ const body = async (nativeSpan, ownsEnd) => {
119
+ const adapter = new NativeOtelSpan(nativeSpan);
120
+ for (const [key, value] of Object.entries(attrs)) {
121
+ if (value !== undefined)
122
+ adapter.setAttribute(key, value);
123
+ }
124
+ const base = tracing ? context.active().setValue(NATIVE_TRACING_CONTEXT_KEY, tracing) : context.active();
125
+ const nativeContext = trace.setSpan(base, adapter);
126
+ try {
127
+ return await context.with(nativeContext, () => fn(adapter));
128
+ }
129
+ catch (error) {
130
+ const err = error instanceof Error ? error : new Error(String(error));
131
+ adapter.recordException(err);
132
+ adapter.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
133
+ throw error;
134
+ }
135
+ finally {
136
+ if (ownsEnd)
137
+ adapter.end();
138
+ }
139
+ };
140
+ if (!tracing)
141
+ return body(NOOP_NATIVE_SPAN, false);
142
+ if (typeof tracing.startActiveSpan === "function") {
143
+ return tracing.startActiveSpan(name, (s) => body(s, true));
144
+ }
145
+ // enterSpan ends the span when the callback settles.
146
+ return tracing.enterSpan(name, (s) => body(s, false));
147
+ }
@@ -0,0 +1,24 @@
1
+ export interface NativePropagation {
2
+ traceId: string;
3
+ spanId: string;
4
+ }
5
+ export declare const NATIVE_PROPAGATION_CONTEXT_KEY: symbol;
6
+ /** One synthetic trace/span pair standing for the whole invocation. */
7
+ export declare function createNativePropagation(random?: (n: number) => Uint8Array): NativePropagation;
8
+ export declare function propagationTraceparent(p: NativePropagation): string;
9
+ export declare function isAiGatewayHost(url: URL): boolean;
10
+ /** Hostname allow-check: undefined = all hosts, [] = none; string = exact host, RegExp tests the host. */
11
+ export declare function propagationTargetAllowed(hostname: string, targets: ReadonlyArray<string | RegExp> | undefined): boolean;
12
+ /**
13
+ * Return `[input, init]` with `traceparent` (and `cf-aig-otel-*` for the AI
14
+ * Gateway) added. Never overwrites an existing header, never mutates the
15
+ * caller's objects, and never consumes a Request body (the Request itself is
16
+ * passed through; only `init.headers` carries the merged set).
17
+ */
18
+ export declare function injectOutboundHeaders(input: RequestInfo | URL, init: RequestInit | undefined, opts: {
19
+ traceparent: string;
20
+ aiGateway?: {
21
+ traceId: string;
22
+ parentSpanId: string;
23
+ };
24
+ }): [RequestInfo | URL, RequestInit];
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Native-mode trace propagation (WinterCG-safe: no node:* imports).
3
+ *
4
+ * Cloudflare's native spans expose no trace/span ids, so the SDK mints ONE
5
+ * synthetic pair per invocation. It is emitted on outbound headers and recorded
6
+ * on the root span as `heystack.propagation.*` link attributes; ingest turns
7
+ * those into span links. The synthetic ids are NOT Cloudflare's native ids.
8
+ */
9
+ import { createContextKey } from "@opentelemetry/api";
10
+ import { detectLLMProvider } from "./llm-enrich.js";
11
+ export const NATIVE_PROPAGATION_CONTEXT_KEY = createContextKey("heystack.native-propagation");
12
+ function randomHex(bytes, random) {
13
+ for (let attempt = 0; attempt < 4; attempt++) {
14
+ const buf = random(bytes);
15
+ let hex = "";
16
+ let nonZero = false;
17
+ for (const b of buf) {
18
+ if (b !== 0)
19
+ nonZero = true;
20
+ hex += b.toString(16).padStart(2, "0");
21
+ }
22
+ if (nonZero)
23
+ return hex;
24
+ }
25
+ return "1".padStart(bytes * 2, "0");
26
+ }
27
+ const defaultRandom = (n) => crypto.getRandomValues(new Uint8Array(n));
28
+ /** One synthetic trace/span pair standing for the whole invocation. */
29
+ export function createNativePropagation(random = defaultRandom) {
30
+ return { traceId: randomHex(16, random), spanId: randomHex(8, random) };
31
+ }
32
+ export function propagationTraceparent(p) {
33
+ return `00-${p.traceId}-${p.spanId}-01`;
34
+ }
35
+ export function isAiGatewayHost(url) {
36
+ return detectLLMProvider(url.hostname) === "cloudflare";
37
+ }
38
+ /** Hostname allow-check: undefined = all hosts, [] = none; string = exact host, RegExp tests the host. */
39
+ export function propagationTargetAllowed(hostname, targets) {
40
+ if (!targets)
41
+ return true;
42
+ const host = hostname.toLowerCase();
43
+ for (const t of targets) {
44
+ try {
45
+ if (typeof t === "string" ? t.toLowerCase() === host : t.test(host))
46
+ return true;
47
+ }
48
+ catch {
49
+ // bad pattern: skip
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+ /**
55
+ * Return `[input, init]` with `traceparent` (and `cf-aig-otel-*` for the AI
56
+ * Gateway) added. Never overwrites an existing header, never mutates the
57
+ * caller's objects, and never consumes a Request body (the Request itself is
58
+ * passed through; only `init.headers` carries the merged set).
59
+ */
60
+ export function injectOutboundHeaders(input, init, opts) {
61
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
62
+ if (!headers.has("traceparent"))
63
+ headers.set("traceparent", opts.traceparent);
64
+ if (opts.aiGateway) {
65
+ if (!headers.has("cf-aig-otel-trace-id"))
66
+ headers.set("cf-aig-otel-trace-id", opts.aiGateway.traceId);
67
+ if (!headers.has("cf-aig-otel-parent-span-id")) {
68
+ headers.set("cf-aig-otel-parent-span-id", opts.aiGateway.parentSpanId);
69
+ }
70
+ }
71
+ return [input, { ...(init ?? {}), headers }];
72
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Route templating for Workers. `routes` patterns look like `/users/:id` or
3
+ * `/files/*`; the matched PATTERN (never the raw path) becomes `http.route`.
4
+ */
5
+ /** First matching pattern wins; `:name` matches one segment, trailing `*` matches the rest. */
6
+ export declare function matchRoute(pathname: string, routes: readonly string[]): string | undefined;
7
+ /** Resolve `http.route`: `routeTemplate(req)` first, then `routes`. Never throws. */
8
+ export declare function resolveRoute(req: Request, opts: {
9
+ routes?: readonly string[];
10
+ routeTemplate?: (req: Request) => string | undefined;
11
+ }): string | undefined;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Route templating for Workers. `routes` patterns look like `/users/:id` or
3
+ * `/files/*`; the matched PATTERN (never the raw path) becomes `http.route`.
4
+ */
5
+ const MAX_ROUTE_LEN = 256;
6
+ function segs(p) {
7
+ return p.split("/").filter((s) => s !== "");
8
+ }
9
+ /** First matching pattern wins; `:name` matches one segment, trailing `*` matches the rest. */
10
+ export function matchRoute(pathname, routes) {
11
+ const path = segs(pathname);
12
+ for (const pattern of routes) {
13
+ if (typeof pattern !== "string" || pattern === "")
14
+ continue;
15
+ const pat = segs(pattern);
16
+ const star = pat[pat.length - 1] === "*";
17
+ const fixed = star ? pat.slice(0, -1) : pat;
18
+ if (star ? path.length < fixed.length : path.length !== fixed.length)
19
+ continue;
20
+ const ok = fixed.every((p, i) => p[0] === ":" || p === path[i]);
21
+ if (ok)
22
+ return pattern;
23
+ }
24
+ return undefined;
25
+ }
26
+ /** Resolve `http.route`: `routeTemplate(req)` first, then `routes`. Never throws. */
27
+ export function resolveRoute(req, opts) {
28
+ try {
29
+ const t = opts.routeTemplate?.(req);
30
+ if (typeof t === "string" && t !== "" && t.length <= MAX_ROUTE_LEN)
31
+ return t;
32
+ }
33
+ catch {
34
+ // a faulty user callback must never affect the request
35
+ }
36
+ if (opts.routes && opts.routes.length > 0) {
37
+ try {
38
+ return matchRoute(new URL(req.url).pathname, opts.routes);
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ }
44
+ return undefined;
45
+ }
@@ -29,7 +29,16 @@ export declare function loadRemoteSamplingRate(opts: {
29
29
  apiKey: string;
30
30
  fetchImpl: typeof fetch;
31
31
  }): Promise<void>;
32
+ export interface SamplerOptions {
33
+ /**
34
+ * Honour a REMOTE parent's sampled flag (an inbound `traceparent` `-01`/`-00`).
35
+ * Default `true`. When `false`, a remote parent gets the same ratio decision as
36
+ * a fresh root (over the continued trace id), so an untrusted caller can neither
37
+ * force recording nor suppress it. Local parents are always honoured.
38
+ */
39
+ trustRemoteParent?: boolean;
40
+ }
32
41
  export declare function makeSampler(sampling?: {
33
42
  rate?: number;
34
43
  remote?: boolean;
35
- }): Sampler;
44
+ }, opts?: SamplerOptions): Sampler;
@@ -74,16 +74,20 @@ export async function loadRemoteSamplingRate(opts) {
74
74
  /* fail open: leave rate at keep-all */
75
75
  }
76
76
  }
77
- export function makeSampler(sampling) {
77
+ export function makeSampler(sampling, opts = {}) {
78
+ const parentBased = (root) => opts.trustRemoteParent === false
79
+ ? new ParentBasedSampler({ root, remoteParentSampled: root, remoteParentNotSampled: root })
80
+ : new ParentBasedSampler({ root });
78
81
  if (sampling?.remote) {
79
82
  // Dynamic rate: reads from the per-isolate ref that loadRemoteSamplingRate sets.
80
83
  // Starts at 1 (keep-all) until the config fetch resolves on the first request.
81
- return new ParentBasedSampler({ root: new HeystackRatioSampler(() => _remoteRate.value) });
84
+ return parentBased(new HeystackRatioSampler(() => _remoteRate.value));
82
85
  }
83
86
  const rate = sampling?.rate;
84
87
  if (rate === undefined || rate >= 1)
85
88
  return new AlwaysOnSampler();
86
89
  // ParentBased: a sampled/dropped PARENT (local or remote via traceparent) wins,
87
90
  // so a distributed trace samples consistently; only the root uses the ratio.
88
- return new ParentBasedSampler({ root: new HeystackRatioSampler(rate) });
91
+ // With trustRemoteParent=false a remote parent is re-decided by the ratio.
92
+ return parentBased(new HeystackRatioSampler(rate));
89
93
  }
package/dist/workers.d.ts CHANGED
@@ -3,6 +3,10 @@ import { type Context, type ContextManager } from "@opentelemetry/api";
3
3
  import { BasicTracerProvider, type ReadableSpan, type SpanExporter } from "@opentelemetry/sdk-trace-base";
4
4
  import { type HeystackOptions } from "./core.js";
5
5
  import { type LLMGatewayConfig } from "./llm-enrich.js";
6
+ import { wrapSqlClient } from "./workers-bindings.js";
7
+ import { NATIVELY_COVERED, NATIVE_PROPAGATES_EXTERNAL, shouldEnrichNatively, type BindingKind } from "./workers-coverage.js";
8
+ import { type CloudflareSpan, type CloudflareTracing } from "./workers-native.js";
9
+ import { type SpanPrivacyOptions } from "./redact.js";
6
10
  declare const ExportResultCode: {
7
11
  readonly SUCCESS: 0;
8
12
  readonly FAILED: 1;
@@ -54,13 +58,15 @@ interface OtlpTracesPayload {
54
58
  }[];
55
59
  }[];
56
60
  }
61
+ /** Per-span attribute transform applied at serialization (privacy pass). */
62
+ type AttributeTransform = (attrs: Record<string, unknown>, span: ReadableSpan) => Record<string, unknown>;
57
63
  /**
58
64
  * Serialize a batch of ended spans into a single OTLP/JSON traces payload.
59
65
  *
60
66
  * All spans in a SimpleSpanProcessor batch share the same per-request provider,
61
67
  * hence the same resource, so we emit a single resourceSpans entry.
62
68
  */
63
- export declare function serializeSpans(spans: ReadableSpan[]): OtlpTracesPayload;
69
+ export declare function serializeSpans(spans: ReadableSpan[], transform?: AttributeTransform): OtlpTracesPayload;
64
70
  /** Parse a W3C `traceparent`. Returns null for malformed or all-zero ids. */
65
71
  export declare function parseTraceparent(header: string | null): {
66
72
  traceId: string;
@@ -112,7 +118,13 @@ export declare class HeystackSpanExporter implements SpanExporter {
112
118
  * the `pending` set + manual `flushHeystack()` when absent/unavailable.
113
119
  */
114
120
  waitUntil?: (p: Promise<unknown>) => void;
115
- constructor(options: HeystackOptions);
121
+ /**
122
+ * Privacy pass on every exported span — redacts URL attributes (`url.full`,
123
+ * `http.url`, `http.target`, `url.query`, …) so spans from host frameworks
124
+ * (Next/OpenNext, manual spans) are covered too, not just Heystack's own.
125
+ */
126
+ private readonly privacy;
127
+ constructor(options: HeystackOptions & SpanPrivacyOptions);
116
128
  export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
117
129
  /**
118
130
  * POST the batch with ONE bounded automatic retry for network errors / 429 /
@@ -143,7 +155,7 @@ export declare class HeystackSpanExporter implements SpanExporter {
143
155
  */
144
156
  forceFlush(): Promise<void>;
145
157
  }
146
- export interface WorkersConfig {
158
+ export interface WorkersConfig extends SpanPrivacyOptions {
147
159
  service: string;
148
160
  /** Defaults to env.HEYSTACK_API_KEY at request time if omitted. */
149
161
  apiKey?: string;
@@ -185,17 +197,33 @@ export interface WorkersConfig {
185
197
  * Defaults to `false` (no binding tracing). Consumed by a later task.
186
198
  */
187
199
  instrumentBindings?: boolean | string[];
200
+ /**
201
+ * Native mode only: per-kind override of which bindings the SDK enriches on
202
+ * top of Cloudflare's own spans. Default: only kinds Cloudflare does not trace
203
+ * (`ai`, `vectorize`, `workflow`, `hyperdrive`, `analytics_engine`). Set
204
+ * `{ ai: false }` to opt out or `{ d1: true }` to force-enrich a covered kind.
205
+ * Ignored when `instrumentBindings` is set explicitly.
206
+ */
207
+ nativeEnrichment?: Partial<Record<BindingKind, boolean>>;
188
208
  /**
189
209
  * Select the Workers tracing plane. `auto` (default) uses Cloudflare's native
190
210
  * tracer when `ctx.tracing` is available and otherwise falls back to direct
191
211
  * OTLP export. `native` prefers the platform tracer but still fails open when
192
212
  * unavailable. `direct` always uses Heystack's fetch exporter.
193
213
  *
194
- * Native mode deliberately disables Heystack root/fetch/binding
195
- * instrumentation: Cloudflare already emits those spans, and running both
196
- * creates duplicate, disconnected trace trees.
214
+ * Native mode creates no OTel provider and does not re-instrument what
215
+ * Cloudflare already spans (fetch, D1, KV, R2, DO, Queues, rate limit,
216
+ * service bindings). It enriches only the uncovered binding kinds (Workers AI,
217
+ * Vectorize, Workflows, Analytics Engine; Hyperdrive via `wrapSqlClient`) as
218
+ * native child spans, gated by `nativeEnrichment` / an explicit
219
+ * `instrumentBindings`.
197
220
  */
198
221
  nativeTracing?: "auto" | "native" | "direct";
222
+ /**
223
+ * Explicit Cloudflare tracer (`ctx.tracing`) for contexts that have none of
224
+ * their own, e.g. Durable Objects. Preferred over `ctx.tracing` when set.
225
+ */
226
+ tracing?: CloudflareTracing;
199
227
  /**
200
228
  * Head-sampling configuration. When omitted (or `rate` >= 1), all traces are
201
229
  * kept. When `rate` < 1, a deterministic FNV-1a hash of the trace ID decides
@@ -204,7 +232,9 @@ export interface WorkersConfig {
204
232
  *
205
233
  * Note: head sampling is parent-respecting, so an incoming request carrying a
206
234
  * sampled `traceparent` is still recorded even at `rate: 0` (it is not an
207
- * absolute kill-switch; it governs only fresh/root traces).
235
+ * absolute kill-switch; it governs only fresh/root traces) — unless
236
+ * `trustInboundSampling: false`, which makes the rate decide for inbound
237
+ * traces too.
208
238
  *
209
239
  * When `remote: true`, the sampling rate is fetched once per isolate from
210
240
  * `{endpoint}/v1/sampling/config` on the first request. Cold-isolate spans
@@ -215,6 +245,25 @@ export interface WorkersConfig {
215
245
  rate?: number;
216
246
  remote?: boolean;
217
247
  };
248
+ /**
249
+ * Whether an inbound W3C `traceparent` sampled flag (`-01` / `-00`) decides
250
+ * keep/drop for the request's trace. Default `true` (parent-based sampling,
251
+ * so a browser→Worker trace samples consistently). Set `false` on a Worker
252
+ * that receives traffic from callers you don't control: the trace id is
253
+ * still continued (correlation is kept) but the keep/drop decision is made
254
+ * by THIS Worker's `sampling` rate over the trace id, so a client can't force
255
+ * recording with `-01` (or hide its requests with `-00`). Only meaningful when
256
+ * `sampling` is set (with no rate every trace is kept anyway).
257
+ */
258
+ trustInboundSampling?: boolean;
259
+ /**
260
+ * Cloudflare geo attributes recorded on the SERVER span from `req.cf`.
261
+ * `"country"` (default) records only `geo.country`; `"full"` also records
262
+ * `geo.region`, `geo.city` and `geo.asn`; `false` records none. The end-user
263
+ * IP (`client.address`) is separate and off by default — see
264
+ * {@link SpanPrivacyOptions.captureClientIp}.
265
+ */
266
+ captureGeo?: "country" | "full" | false;
218
267
  /**
219
268
  * LLM/gen_ai capture for outbound calls to known providers (OpenAI, Anthropic,
220
269
  * Mistral, Google, CF AI Gateway). Metadata (model, tokens, latency,
@@ -243,17 +292,36 @@ export interface WorkersConfig {
243
292
  redact?: (text: string) => string;
244
293
  maxContentChars?: number;
245
294
  gateways?: LLMGatewayConfig[];
295
+ /**
296
+ * The Cloudflare AI Gateway exports its own OTel spans for these calls.
297
+ * When true the SDK's LLM span is marked `heystack.ai.delegated` so the
298
+ * call is counted once (by the gateway). Opt-in: without it the SDK span is
299
+ * the counted one, so an AI call isn't lost if the gateway's own export
300
+ * isn't configured. The `cf-aig-otel-*` headers are sent either way.
301
+ */
302
+ gatewayExport?: boolean;
246
303
  };
304
+ /**
305
+ * Path templates for `http.route`, e.g. `["/users/:id", "/files/*"]` (first
306
+ * match wins; a trailing `*` matches the rest). If neither this nor
307
+ * `routeTemplate` is set, `http.route` is omitted and Heystack templates the
308
+ * path server-side.
309
+ */
310
+ routes?: string[];
311
+ /** Compute the `http.route` template per request; wins over `routes`. */
312
+ routeTemplate?: (req: Request) => string | undefined;
313
+ /**
314
+ * Hostnames (exact string or RegExp) that receive `traceparent` (and AI
315
+ * Gateway headers) on native-mode outbound fetches. Default: every host except
316
+ * the Heystack ingest endpoint. `[]` disables propagation.
317
+ */
318
+ propagateTargets?: Array<string | RegExp>;
247
319
  }
248
- /** Minimal public surface exposed by Cloudflare's Workers custom-spans API. */
249
- export interface CloudflareNativeSpan {
250
- readonly isTraced: boolean;
251
- setAttribute(key: string, value?: string | number | boolean): void;
252
- }
253
- /** `ExecutionContext.tracing` without importing the `cloudflare:workers` module. */
254
- export interface CloudflareNativeTracing {
255
- enterSpan<T, A extends unknown[]>(name: string, callback: (span: CloudflareNativeSpan, ...args: A) => T, ...args: A): T;
256
- }
320
+ export type { CloudflareSpan, CloudflareTracing };
321
+ /** @deprecated Use {@link CloudflareSpan}. */
322
+ export type CloudflareNativeSpan = CloudflareSpan;
323
+ /** @deprecated Use {@link CloudflareTracing}. */
324
+ export type CloudflareNativeTracing = CloudflareTracing;
257
325
  /**
258
326
  * A `BasicTracerProvider` with the underlying `HeystackSpanExporter` attached so
259
327
  * flush paths can drain its in-flight fetches directly (the OTel processor's
@@ -272,10 +340,12 @@ export type HeystackTracerProvider = BasicTracerProvider & {
272
340
  * must drain the exporter — `await provider.heystackExporter.forceFlush()` (or
273
341
  * `flushHeystack()`), not just `provider.forceFlush()`.
274
342
  */
275
- export declare function createTracerProvider(config: HeystackOptions & {
343
+ export declare function createTracerProvider(config: HeystackOptions & SpanPrivacyOptions & {
276
344
  sampling?: {
277
345
  rate?: number;
346
+ remote?: boolean;
278
347
  };
348
+ trustInboundSampling?: boolean;
279
349
  }): HeystackTracerProvider;
280
350
  /**
281
351
  * An AsyncLocalStorage-backed ContextManager for the /workers entry. When the
@@ -483,6 +553,22 @@ export declare function wrapDurableObjectClass<C extends {
483
553
  export declare function wrapWorkflowEntrypointClass<C extends {
484
554
  readonly prototype: object;
485
555
  }>(cls: C, opts?: InstrumentClassesOpts): C;
556
+ /**
557
+ * Decorate a `WorkerEntrypoint` class (the target of a Service binding):
558
+ * - `fetch()` is traced exactly like an `instrument()`ed handler (SERVER span,
559
+ * inbound `traceparent` continued, export drained via `ctx.waitUntil`).
560
+ * - every public RPC method gets a SERVER span `rpc <method>` with
561
+ * `jsrpc.method` / `rpc.method`. Return values (including stubs, RpcTargets
562
+ * and custom thenables), `this` and errors pass through untouched.
563
+ *
564
+ * RPC spans exist in DIRECT mode only. When Cloudflare native tracing is active
565
+ * it already emits `jsrpc` spans, so RPC calls pass straight through and `fetch`
566
+ * takes `instrument()`'s native path. Constructor, `#private`, `_underscore` and
567
+ * Symbol-keyed members are never wrapped.
568
+ *
569
+ * export default wrapWorkerEntrypointClass(class Api extends WorkerEntrypoint { ... }, { service: "api" });
570
+ */
571
+ export declare function wrapWorkerEntrypointClass<T extends new (...a: any[]) => object>(cls: T, cfg: WorkersConfig): T;
486
572
  /**
487
573
  * Run `fn` inside a new child span named `name`. The span is automatically
488
574
  * parented to the currently-active span (via `context.active()`), started
@@ -502,8 +588,8 @@ export declare function withSpan<T>(name: string, attrs: Record<string, string |
502
588
  * without `nodejs_compat`: the explicit context cannot be lost across async
503
589
  * boundaries. It falls back to the regular OTel tracer outside Cloudflare.
504
590
  */
505
- export declare function withCloudflareSpan<T>(ctx: ExecutionContext | CloudflareNativeTracing, name: string, fn: (span: Span) => T | Promise<T>): Promise<T>;
506
- export declare function withCloudflareSpan<T>(ctx: ExecutionContext | CloudflareNativeTracing, name: string, attrs: Record<string, string | number | boolean>, fn: (span: Span) => T | Promise<T>): Promise<T>;
591
+ export declare function withCloudflareSpan<T>(ctx: ExecutionContext | CloudflareTracing, name: string, fn: (span: Span) => T | Promise<T>): Promise<T>;
592
+ export declare function withCloudflareSpan<T>(ctx: ExecutionContext | CloudflareTracing, name: string, attrs: Record<string, string | number | boolean>, fn: (span: Span) => T | Promise<T>): Promise<T>;
507
593
  /**
508
594
  * Add a named event (with optional attributes) to the currently-active span.
509
595
  * No-op when no span is active.
@@ -523,3 +609,6 @@ export { llmRequestAttrs as genAiRequestAttrs } from "./llm-enrich.js";
523
609
  */
524
610
  export { llmResponseAttrs as genAiResponseAttrs } from "./llm-enrich.js";
525
611
  export type { LLMGatewayConfig, LLMProvider } from "./llm-enrich.js";
612
+ export { NATIVELY_COVERED, NATIVE_PROPAGATES_EXTERNAL, shouldEnrichNatively, wrapSqlClient };
613
+ export { matchRoute } from "./workers-routes.js";
614
+ export type { BindingKind };