@heystack/otel 0.18.0 → 0.19.1
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.
- package/README.md +151 -27
- package/dist/core.d.ts +1 -0
- package/dist/core.js +4 -0
- package/dist/hash.d.ts +19 -0
- package/dist/hash.js +47 -0
- package/dist/next.d.ts +2 -1
- package/dist/next.js +5 -0
- package/dist/node.d.ts +14 -2
- package/dist/node.js +49 -4
- package/dist/redact.d.ts +57 -0
- package/dist/redact.js +190 -0
- package/dist/web.d.ts +42 -0
- package/dist/web.js +91 -24
- package/dist/workers-bindings.d.ts +39 -2
- package/dist/workers-bindings.js +388 -78
- package/dist/workers-coverage.d.ts +11 -0
- package/dist/workers-coverage.js +22 -0
- package/dist/workers-native.d.ts +66 -0
- package/dist/workers-native.js +147 -0
- package/dist/workers-propagation.d.ts +24 -0
- package/dist/workers-propagation.js +72 -0
- package/dist/workers-routes.d.ts +11 -0
- package/dist/workers-routes.js +45 -0
- package/dist/workers-sampler.d.ts +10 -1
- package/dist/workers-sampler.js +7 -3
- package/dist/workers.d.ts +108 -19
- package/dist/workers.js +639 -190
- package/package.json +9 -4
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Cloudflare binding kinds Cloudflare's native tracing already spans.
|
|
3
|
+
* In native mode the SDK enriches ONLY the uncovered kinds so a D1/KV/R2/... call
|
|
4
|
+
* is never reported twice. WinterCG-safe: pure data, no imports.
|
|
5
|
+
*/
|
|
6
|
+
export type BindingKind = "d1" | "kv" | "r2" | "durable_object" | "queue" | "ai" | "vectorize" | "workflow" | "hyperdrive" | "analytics_engine" | "ratelimit" | "service" | "fetch";
|
|
7
|
+
export declare const NATIVELY_COVERED: Readonly<Record<BindingKind, boolean>>;
|
|
8
|
+
/** Native tracing does not propagate trace context to external (non-Cloudflare) services. */
|
|
9
|
+
export declare const NATIVE_PROPAGATES_EXTERNAL = false;
|
|
10
|
+
/** `enrich?.[kind] ?? !table[kind]` — `table` is a test seam. */
|
|
11
|
+
export declare function shouldEnrichNatively(kind: BindingKind, enrich?: Partial<Record<BindingKind, boolean>>, table?: Readonly<Record<BindingKind, boolean>>): boolean;
|
|
@@ -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;
|
package/dist/workers-sampler.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
91
|
+
// With trustRemoteParent=false a remote parent is re-decided by the ratio.
|
|
92
|
+
return parentBased(new HeystackRatioSampler(rate));
|
|
89
93
|
}
|