@decocms/start 4.3.0 → 4.5.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.
package/src/sdk/otel.ts CHANGED
@@ -2,47 +2,66 @@
2
2
  * Single observability entry point for `@decocms/start` on Cloudflare Workers.
3
3
  *
4
4
  * `instrumentWorker(handler, options)` wraps a Worker handler with:
5
- * - structured JSON logger (stdout → Cloudflare Logs / Logpush) — always
6
- * - OTLP/HTTP logs exporter (HyperDX) — when `OTEL_EXPORTER_OTLP_ENDPOINT` is set
7
- * - OTLP/HTTP metrics exporter (HyperDX) — same condition
5
+ * - structured JSON logger (stdout → Cloudflare Workers Logs) — always
8
6
  * - Workers Analytics Engine metrics — when `env.DECO_METRICS` binding exists
9
- * - OTel traces via `@microlabs/otel-cf-workers` — when OTLP endpoint is set,
10
- * or always-on for the framework's internal `withTracing()` calls.
11
- * - URL-based head sampler from `OTEL_SAMPLING_CONFIG`
12
- * - OTel `Resource` with service.* / cloud.* / deployment.environment /
13
- * deco.runtime.version attributes
7
+ * - OTLP/HTTP metrics exporter (HyperDX) — when `OTEL_EXPORTER_OTLP_ENDPOINT`
8
+ * is set (CF doesn't support OTLP metrics export yet, so this stays app-side)
9
+ * - Per-request `ctx.waitUntil(forceFlush)` for any registered OTel batch
10
+ * processors so log/metric batches don't die with the isolate
11
+ * - Bridges framework-internal `withTracing()` calls onto the global
12
+ * `@opentelemetry/api` tracer, stamping `deco.*` attributes on every span
13
+ * so they survive Cloudflare's platform-managed trace export
14
14
  *
15
- * Removing HyperDX = unsetting `OTEL_EXPORTER_OTLP_ENDPOINT`. The console-JSON
16
- * logger and AE metrics keep flowing — this is the "no vendor lock-in" guarantee.
15
+ * **Logs and traces export to HyperDX is now handled by Cloudflare** via the
16
+ * `observability.{logs,traces}.destinations` block in `wrangler.jsonc`. CF
17
+ * captures `console.*` output and `@opentelemetry/api` global tracer spans
18
+ * out-of-band and ships them OTLP-encoded to whatever destination is
19
+ * configured. This eliminates the in-Worker exporter SDK, the per-request
20
+ * subrequest cost of pushing OTLP, and the entire class of bug PR #153 fixed
21
+ * (batch processors that never flush before isolate recycling).
17
22
  *
18
23
  * @example
19
24
  * ```ts
25
+ * // worker-entry.ts
20
26
  * import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
21
27
  * import { instrumentWorker } from "@decocms/start/sdk/otel";
22
28
  *
23
29
  * const handler = createDecoWorkerEntry(serverEntry, options);
24
- *
25
30
  * export default instrumentWorker(handler, { serviceName: "my-store" });
26
31
  * ```
27
32
  *
28
- * Wrangler bindings to add when enabling OTLP + AE:
33
+ * Companion `wrangler.jsonc` block (run `scripts/migrate-to-cf-observability.ts`
34
+ * to inject this automatically):
29
35
  * ```jsonc
30
- * "version_metadata": { "binding": "CF_VERSION_METADATA" },
31
- * "analytics_engine_datasets": [{ "binding": "DECO_METRICS", "dataset": "deco_metrics_my_site" }]
36
+ * "observability": {
37
+ * "logs": { "enabled": true, "destinations": ["hyperdx-logs"],
38
+ * "head_sampling_rate": 1.0, "persist": false },
39
+ * "traces": { "enabled": true, "destinations": ["hyperdx-traces"],
40
+ * "head_sampling_rate": 0.1, "persist": false }
41
+ * },
42
+ * "version_metadata": { "binding": "CF_VERSION_METADATA" },
43
+ * "analytics_engine_datasets": [{ "binding": "DECO_METRICS",
44
+ * "dataset": "deco_metrics_my_site" }]
32
45
  * ```
33
46
  *
34
- * Required Worker secrets when OTLP is enabled:
35
- * wrangler secret put OTEL_EXPORTER_OTLP_ENDPOINT # https://in-otel.hyperdx.io
36
- * wrangler secret put OTEL_EXPORTER_OTLP_HEADERS # authorization=<token>
47
+ * **Back-compat seam.** Sites that need to keep app-side OTLP log export
48
+ * (custom destination not covered by CF, custom batching, etc.) can opt back
49
+ * in with `enableAppSideOtlpLogs: true` and the existing `OTEL_EXPORTER_OTLP_*`
50
+ * secrets. Slated for removal in 5.0.0.
37
51
  */
38
52
 
39
- import { instrument, type ResolveConfigFn } from "@microlabs/otel-cf-workers";
40
53
  import { trace } from "@opentelemetry/api";
41
54
  import { type Resource, resourceFromAttributes } from "@opentelemetry/resources";
42
55
 
43
56
  import { configureMeter, configureTracer } from "../middleware/observability";
44
57
  import { createCompositeLogger, createCompositeMeter } from "./composite";
45
- import { configureLogger, defaultLoggerAdapter, type LogLevel, logger } from "./logger";
58
+ import {
59
+ configureLogger,
60
+ defaultLoggerAdapter,
61
+ type LogLevel,
62
+ logger,
63
+ setLoggerAttributeFloor,
64
+ } from "./logger";
46
65
  import {
47
66
  createAnalyticsEngineMeterAdapter,
48
67
  createOtelLoggerAdapter,
@@ -51,7 +70,6 @@ import {
51
70
  setRuntimeEnv,
52
71
  } from "./otelAdapters";
53
72
  import { RequestContext } from "./requestContext";
54
- import { createUrlBasedHeadSampler, decodeSamplingConfig } from "./sampler";
55
73
 
56
74
  const VALID_LOG_LEVELS: readonly LogLevel[] = ["debug", "info", "warn", "error"];
57
75
 
@@ -79,20 +97,38 @@ export interface OtelOptions {
79
97
  /** Push interval for OTLP metrics, in ms. Defaults to env.OTEL_EXPORT_INTERVAL or 60_000. */
80
98
  metricsExportIntervalMillis?: number;
81
99
  /**
82
- * Minimum severity to forward to OTLP logs (HyperDX). Below the floor
83
- * the framework still writes a structured JSON line to `console.*`
84
- * (Cloudflare Workers Logs), so nothing is silently lost.
100
+ * Minimum severity to forward to the **app-side OTLP** logger (only
101
+ * relevant when `enableAppSideOtlpLogs: true`). The default `console.*`
102
+ * adapter is unaffected and continues to capture every level for
103
+ * Cloudflare Workers Logs / CF-side OTLP export.
85
104
  *
86
105
  * Defaults to `"warn"`. Falls back to env `OTEL_LOG_MIN_SEVERITY` when
87
106
  * unset. Set to `"debug"` to forward everything.
88
107
  */
89
108
  otlpMinSeverity?: LogLevel;
90
109
  /**
91
- * Version of `@decocms/start` to advertise as `deco.runtime.version`.
110
+ * Opt-in: also wire an in-Worker OTLP logger that pushes log records to
111
+ * `OTEL_EXPORTER_OTLP_ENDPOINT`. Defaults to `false` — sites should
112
+ * prefer the platform-managed CF-side path
113
+ * (`observability.logs.destinations` in `wrangler.jsonc`), which is
114
+ * cheaper, has no flush-bug class, and consumes zero subrequest budget.
115
+ *
116
+ * Use this only when CF's OTLP logs export doesn't meet a specific need
117
+ * (e.g. shipping to a destination CF doesn't support, custom batching,
118
+ * staging-only debugging). Requires `OTEL_EXPORTER_OTLP_ENDPOINT` and
119
+ * `OTEL_EXPORTER_OTLP_HEADERS` to be set.
120
+ *
121
+ * Slated for removal in 5.0.0.
122
+ */
123
+ enableAppSideOtlpLogs?: boolean;
124
+ /**
125
+ * Version of `@decocms/start` to advertise as `deco.runtime.version`
126
+ * on every span (CF doesn't preserve it as a resource attribute since
127
+ * we no longer ship our own resource — we stamp it per-span instead).
92
128
  * Falls back to a build-time constant; override only for tests.
93
129
  */
94
130
  decoRuntimeVersion?: string;
95
- /** Optional `@decocms/apps` version, advertised as `deco.apps.version`. */
131
+ /** Optional `@decocms/apps` version, stamped as `deco.apps.version` on every span. */
96
132
  decoAppsVersion?: string;
97
133
  }
98
134
 
@@ -124,25 +160,59 @@ interface BootState {
124
160
 
125
161
  let bootState: BootState | null = null;
126
162
 
163
+ /**
164
+ * Per-span attribute floor — stamped on every span we create via
165
+ * `configureTracer().startSpan(...)`. These match what the legacy resource
166
+ * attributes used to carry (when `@microlabs/otel-cf-workers` shipped its
167
+ * own OTel `Resource`); we now stamp them on each span so HyperDX panels
168
+ * filtering on `deco.runtime.version`, `deco.apps.version`, or
169
+ * `deployment.environment` keep working with CF-managed export, which only
170
+ * preserves CF's own resource attribute set (`service.name`, `faas.name`,
171
+ * `cloudflare.script_version.id`, etc.).
172
+ *
173
+ * Populated by `bootObservability` before any span is created. Stays an
174
+ * empty object until then so early span creation is a no-op stamp.
175
+ */
176
+ let spanAttributeFloor: Record<string, string> = {};
177
+
127
178
  // ---------------------------------------------------------------------------
128
179
  // instrumentWorker
129
180
  // ---------------------------------------------------------------------------
130
181
 
131
182
  /**
132
- * Wraps a Cloudflare Worker handler with the full @decocms/start
133
- * observability stack. Idempotent — calling twice on the same handler
134
- * is a no-op (returns the already-instrumented handler).
183
+ * Wraps a Cloudflare Worker handler with the @decocms/start observability
184
+ * stack:
185
+ * - structured JSON logger (always)
186
+ * - AE meter (when `DECO_METRICS` binding present)
187
+ * - optional app-side OTLP meter (when `OTEL_EXPORTER_OTLP_ENDPOINT` set)
188
+ * - optional app-side OTLP logger (when `enableAppSideOtlpLogs: true`)
189
+ * - per-request `ctx.waitUntil(forceFlush)` for any registered batch processors
190
+ * - bridge from framework-internal `withTracing()` to `@opentelemetry/api`
191
+ * global tracer, with `deco.*` attributes stamped on every span
192
+ *
193
+ * Logs and traces export to HyperDX (or any OTLP destination) is handled
194
+ * by Cloudflare via `observability.{logs,traces}.destinations` in
195
+ * `wrangler.jsonc`. This wrapper does NOT call `@microlabs/otel-cf-workers`
196
+ * `instrument()` — CF's platform-managed export captures `console.*` output
197
+ * and global-tracer spans out-of-band.
135
198
  */
136
199
  export function instrumentWorker(
137
200
  handler: WorkerHandler,
138
201
  options: OtelOptions | ((env: Record<string, unknown>) => OtelOptions) = {},
139
202
  ): WorkerHandler {
140
- // Bridge our pluggable TracerAdapter onto @opentelemetry/api so
141
- // framework-internal `withTracing()` calls produce real OTel spans
142
- // for whatever exporter is configured (OTLP, console, etc.).
203
+ // Bridge our pluggable TracerAdapter onto @opentelemetry/api. Framework
204
+ // code calls `withTracing("name", fn, { attr: val })`; that delegates here
205
+ // and lands on `trace.getTracer("@decocms/start").startSpan(...)`.
206
+ //
207
+ // CF Workers Tracing (when `observability.traces.enabled = true` in
208
+ // wrangler) installs its own TracerProvider into the @opentelemetry/api
209
+ // global, so these spans flow through to whatever OTLP destination is
210
+ // configured. Without CF tracing the global tracer is a no-op proxy and
211
+ // the spans simply drop — same outcome as before, no error.
143
212
  configureTracer({
144
213
  startSpan: (name, attrs) => {
145
- const span = trace.getTracer("@decocms/start").startSpan(name, { attributes: attrs });
214
+ const merged = { ...spanAttributeFloor, ...(attrs ?? {}) };
215
+ const span = trace.getTracer("@decocms/start").startSpan(name, { attributes: merged });
146
216
  return {
147
217
  end: () => span.end(),
148
218
  setError: (error) => {
@@ -153,64 +223,32 @@ export function instrumentWorker(
153
223
  },
154
224
  });
155
225
 
156
- const resolveConfig: ResolveConfigFn = (env, _trigger) => {
157
- const opts = typeof options === "function" ? options(env as Record<string, unknown>) : options;
158
- bootObservability(opts, env as Record<string, unknown>);
159
-
160
- const state = bootState!;
161
-
162
- // Sampling — base64 JSON via OTEL_SAMPLING_CONFIG, see sdk/sampler.ts
163
- const samplingConfig = decodeSamplingConfig(env.OTEL_SAMPLING_CONFIG as string | undefined);
164
- const headSampler = createUrlBasedHeadSampler(samplingConfig);
165
-
166
- // microlabs requires an exporter even when we only want internal
167
- // tracing. When OTLP isn't configured, we still set up a no-op
168
- // collector — the URL we'd never reach so spans simply drop.
169
- const exporterUrl = state.otlpEndpoint ?? "http://127.0.0.1:0/v1/traces";
170
-
171
- return {
172
- exporter: {
173
- url: joinPath(exporterUrl, "/v1/traces"),
174
- headers: state.otlpHeaders,
175
- },
176
- service: {
177
- name: state.serviceName,
178
- version: (env.CF_VERSION_METADATA as { id?: string } | undefined)?.id,
179
- },
180
- sampling: { headSampler },
181
- // microlabs auto-instruments globalThis.fetch + KV + waitUntil.
182
- instrumentation: {
183
- instrumentGlobalFetch: true,
184
- instrumentGlobalCache: true,
185
- },
186
- };
187
- };
188
-
189
- const innerHandler: WorkerHandler = {
226
+ return {
190
227
  async fetch(request, env, ctx) {
191
- // Stash env so request-scoped adapters (AE) can resolve their bindings.
192
- // Done inside RequestContext.run wrapping in workerEntry.ts as well, but
193
- // for instrumentWorker we re-stash in case this handler is wrapped over
194
- // the top of a Worker that doesn't go through createDecoWorkerEntry.
228
+ const opts =
229
+ typeof options === "function" ? options(env as Record<string, unknown>) : options;
230
+ bootObservability(opts, env as Record<string, unknown>);
231
+
232
+ // Stash env so request-scoped adapters (AE) can resolve their
233
+ // bindings. Done inside RequestContext.run wrapping in workerEntry.ts
234
+ // too, but we re-stash here in case `instrumentWorker` is wrapped
235
+ // over a handler that doesn't go through `createDecoWorkerEntry`.
195
236
  const wrap = async () => {
196
237
  setRuntimeEnv(env);
197
238
  return handler.fetch(request, env, ctx);
198
239
  };
199
240
 
200
241
  try {
201
- // RequestContext may already be active (createDecoWorkerEntry sets it
202
- // up). If so, run inline; otherwise wrap. Cheap to detect via current.
203
242
  if (RequestContext.current) {
204
243
  return await wrap();
205
244
  }
206
245
  return await RequestContext.run(request, wrap);
207
246
  } finally {
208
- // Drain OTLP logger + meter batches inside the post-response window
209
- // the platform guarantees via `waitUntil`. Without this hook,
210
- // BatchLogRecordProcessor (5s flush) and PeriodicExportingMetricReader
211
- // (60s flush) batches usually die with the isolate before the timer
212
- // fires and never reach HyperDX. `flushOtelProviders` is a no-op when
213
- // OTLP isn't configured, so this is safe in every code path.
247
+ // Drain OTLP meter (and OTLP logger, if `enableAppSideOtlpLogs`)
248
+ // batches inside the post-response window `waitUntil` guarantees.
249
+ // Without this hook, `PeriodicExportingMetricReader` (60s flush)
250
+ // batches usually die with the isolate before the timer fires.
251
+ // No-op when no batch processors are registered.
214
252
  try {
215
253
  ctx.waitUntil(flushOtelProviders());
216
254
  } catch {
@@ -220,9 +258,6 @@ export function instrumentWorker(
220
258
  }
221
259
  },
222
260
  };
223
-
224
- // deno-lint-ignore no-explicit-any
225
- return instrument(innerHandler as any, resolveConfig) as unknown as WorkerHandler;
226
261
  }
227
262
 
228
263
  // ---------------------------------------------------------------------------
@@ -239,21 +274,57 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
239
274
  const otlpHeaders =
240
275
  opts.headers ?? parseHeaders(env.OTEL_EXPORTER_OTLP_HEADERS as string | undefined);
241
276
 
277
+ const decoRuntimeVersion = opts.decoRuntimeVersion ?? DECO_RUNTIME_VERSION;
278
+ const deploymentEnvironment = (env.DECO_ENV_NAME as string | undefined) ?? "production";
279
+
242
280
  const resource = buildResource({
243
281
  serviceName,
244
282
  serviceVersion: (env.CF_VERSION_METADATA as { id?: string } | undefined)?.id,
245
283
  serviceInstanceId: cryptoRandomId(),
246
- deploymentEnvironment: (env.DECO_ENV_NAME as string | undefined) ?? "production",
247
- decoRuntimeVersion: opts.decoRuntimeVersion ?? DECO_RUNTIME_VERSION,
284
+ deploymentEnvironment,
285
+ decoRuntimeVersion,
248
286
  decoAppsVersion: opts.decoAppsVersion,
249
287
  });
250
288
 
289
+ // Stamp deco.* attributes on every span we create. CF-managed trace
290
+ // export emits its own resource attribute set (service.name=Worker name,
291
+ // faas.name, cloudflare.script_version.id, faas.version, etc.) so the
292
+ // legacy resource attrs from `buildResource` don't survive on the
293
+ // CF-side path. Stamping them per-span preserves the dimensions
294
+ // existing HyperDX dashboards filter on.
295
+ spanAttributeFloor = {
296
+ "deco.runtime.version": decoRuntimeVersion,
297
+ "deployment.environment": deploymentEnvironment,
298
+ ...(opts.decoAppsVersion ? { "deco.apps.version": opts.decoAppsVersion } : {}),
299
+ };
300
+
301
+ // Same set, stamped on every log record. CF Workers Logs ships the JSON
302
+ // body verbatim (resource attrs from `buildResource` are NOT applied to
303
+ // logs in default mode), so HyperDX panels grouping by these dimensions
304
+ // would otherwise return empty. Caller-supplied `attrs` still win on key
305
+ // collision.
306
+ setLoggerAttributeFloor({
307
+ "deco.runtime.version": decoRuntimeVersion,
308
+ "deployment.environment": deploymentEnvironment,
309
+ ...(opts.decoAppsVersion ? { "deco.apps.version": opts.decoAppsVersion } : {}),
310
+ });
311
+
251
312
  // ---- Logger ----------------------------------------------------------
313
+ // Default mode: console JSON only. Cloudflare Workers Logs captures the
314
+ // output and ships it via `observability.logs.destinations` to whichever
315
+ // OTLP destination is configured in `wrangler.jsonc`. This is the
316
+ // recommended path: zero in-Worker exporter, no flush bug, no subrequest
317
+ // cost per emit.
318
+ //
319
+ // Opt-in mode (`enableAppSideOtlpLogs: true`): also wire the OTLP logger
320
+ // adapter for sites with destinations CF doesn't support. Requires
321
+ // `OTEL_EXPORTER_OTLP_ENDPOINT` to be set.
252
322
  const otlpMinSeverity =
253
323
  opts.otlpMinSeverity ?? parseLogLevel(env.OTEL_LOG_MIN_SEVERITY) ?? "warn";
254
324
 
325
+ const wantAppSideLogs = opts.enableAppSideOtlpLogs === true;
255
326
  const otelLogger =
256
- otlpEndpoint != null
327
+ wantAppSideLogs && otlpEndpoint != null
257
328
  ? createOtelLoggerAdapter({
258
329
  endpoint: otlpEndpoint,
259
330
  headers: otlpHeaders,
@@ -266,6 +337,9 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
266
337
  configureLogger(createCompositeLogger([defaultLoggerAdapter, otelLogger]));
267
338
 
268
339
  // ---- Meter -----------------------------------------------------------
340
+ // OTLP meter stays default-on when an endpoint is configured: CF doesn't
341
+ // support OTLP metrics export yet, so this is the only path to
342
+ // HyperDX-compatible metrics. Drop this branch when CF ships metrics.
269
343
  const otelMeter =
270
344
  otlpEndpoint != null
271
345
  ? createOtelMeterAdapter({
@@ -295,17 +369,32 @@ function bootObservability(opts: OtelOptions, env: Record<string, unknown>): voi
295
369
  booted = true;
296
370
 
297
371
  // Single boot-time breadcrumb so operators can confirm the wiring at a
298
- // glance from CF Logs without enabling debug.
372
+ // glance from CF Logs without enabling debug. Surfaces which export
373
+ // mode is active (CF-native vs app-side) so misconfigured sites are
374
+ // obvious from the first request.
299
375
  logger.info("observability booted", {
300
376
  service: serviceName,
301
- otlp: Boolean(otlpEndpoint),
302
- otlpMinSeverity: otlpEndpoint != null ? otlpMinSeverity : null,
377
+ mode: wantAppSideLogs ? "hybrid (app-side OTLP logs + CF traces)" : "cf-native",
378
+ otlpMeter: Boolean(otlpEndpoint),
379
+ otlpLogger: wantAppSideLogs && otlpEndpoint != null,
380
+ otlpMinSeverity: wantAppSideLogs && otlpEndpoint != null ? otlpMinSeverity : null,
303
381
  analyticsEngine: aeEnabled,
304
- sampling: Boolean(env.OTEL_SAMPLING_CONFIG),
305
- runtimeVersion: opts.decoRuntimeVersion ?? DECO_RUNTIME_VERSION,
382
+ runtimeVersion: decoRuntimeVersion,
383
+ deploymentEnvironment,
306
384
  });
307
385
  }
308
386
 
387
+ /**
388
+ * Test-only: clear boot state so successive tests can re-boot
389
+ * `instrumentWorker` with different options. Do not call from app code.
390
+ */
391
+ export function _resetBootStateForTests(): void {
392
+ booted = false;
393
+ bootState = null;
394
+ spanAttributeFloor = {};
395
+ setLoggerAttributeFloor({});
396
+ }
397
+
309
398
  // ---------------------------------------------------------------------------
310
399
  // Resource attributes
311
400
  // ---------------------------------------------------------------------------
@@ -345,7 +434,7 @@ function buildResource(input: ResourceInput): Resource {
345
434
  * Drift is acceptable — this attribute is for operator triage, not for
346
435
  * billing / SLOs.
347
436
  */
348
- const DECO_RUNTIME_VERSION = "2.28.2";
437
+ const DECO_RUNTIME_VERSION = "4.4.0";
349
438
 
350
439
  function parseHeaders(str?: string): Record<string, string> {
351
440
  if (!str) return {};
@@ -365,14 +454,6 @@ function numericEnv(value: unknown, fallback: number): number {
365
454
  return Number.isFinite(n) && n > 0 ? n : fallback;
366
455
  }
367
456
 
368
- function joinPath(base: string, path: string): string {
369
- if (!base) return path;
370
- if (base.endsWith("/")) base = base.slice(0, -1);
371
- if (!path.startsWith("/")) path = "/" + path;
372
- if (base.toLowerCase().endsWith(path.toLowerCase())) return base;
373
- return base + path;
374
- }
375
-
376
457
  function cryptoRandomId(): string {
377
458
  // crypto.randomUUID is universally available in CF Workers + Node 19+.
378
459
  try {
@@ -1,12 +1,19 @@
1
1
  /**
2
2
  * URL-based head sampler — port of `deco-cx/deco/observability/otel/samplers/urlBased.ts`.
3
3
  *
4
- * Lets ops dial sampling rates per URL pattern without redeploying. Reads
5
- * `OTEL_SAMPLING_CONFIG` (base64-encoded JSON) at boot and decides each
6
- * trace's sample rate based on the matching pattern.
4
+ * **No longer wired into `instrumentWorker` by default.** As of 4.4.0 the
5
+ * recommended path for trace sampling is Cloudflare's wrangler-level
6
+ * `observability.traces.head_sampling_rate`, which is one global rate per
7
+ * Worker. This module stays as an opt-in escape hatch for sites that need
8
+ * URL-pattern-aware sampling (e.g. always trace `/checkout`, sample
9
+ * homepages at 1%) — those sites must wire OTel themselves outside the
10
+ * default `instrumentWorker` flow.
11
+ *
12
+ * The sampler reads `OTEL_SAMPLING_CONFIG` (base64-encoded JSON) and
13
+ * decides each trace's sample rate based on the matching pattern.
7
14
  *
8
15
  * Wrapped in `ParentBasedSampler` so a span inherits its parent's sampling
9
- * decision when one exists (i.e. distributed traces are kept consistent end
16
+ * decision when one exists (i.e. distributed traces stay consistent end
10
17
  * to end).
11
18
  *
12
19
  * **Default ratio.** When no `default` is provided in the config (or the env
@@ -28,6 +35,9 @@
28
35
  * ]
29
36
  * }
30
37
  * ```
38
+ *
39
+ * @deprecated Slated for removal in 5.0.0 unless a site declares an active
40
+ * need. Use Cloudflare's `head_sampling_rate` first.
31
41
  */
32
42
 
33
43
  import { type Attributes, type Context, type Link, type SpanKind, trace } from "@opentelemetry/api";
@@ -189,7 +199,9 @@ export function decodeSamplingConfig(raw: string | undefined): SamplingConfig |
189
199
 
190
200
  /**
191
201
  * Build a `ParentBasedSampler` rooted at our URL-based sampler.
192
- * Use as the `headSampler` for `@microlabs/otel-cf-workers`.
202
+ * Wire as the `headSampler` for any custom OTel SDK setup (e.g. a site
203
+ * that opts back into `@microlabs/otel-cf-workers` outside the default
204
+ * `instrumentWorker` flow).
193
205
  */
194
206
  export function createUrlBasedHeadSampler(config: SamplingConfig | null): Sampler {
195
207
  const root = new URLBasedSampler(config ?? {});
@@ -949,10 +949,13 @@ export function createDecoWorkerEntry(
949
949
  // via getRuntimeEnv() in sdk/otelAdapters.ts.
950
950
  setRuntimeEnv(env);
951
951
 
952
- // Wrap inner handler in a single root span. `@microlabs/otel-cf-workers`
953
- // already creates an outer span via its `instrument()` wrapper; this
954
- // adds a nested span carrying our normalized path/status attributes
955
- // that microlabs doesn't capture (it uses url.path verbatim).
952
+ // Wrap inner handler in a single root span carrying our normalized
953
+ // path/method attributes. With Cloudflare-managed trace export
954
+ // (`observability.traces.destinations` in wrangler.jsonc), this
955
+ // span and any `withTracing` spans nested below it flow to
956
+ // HyperDX via CF's platform-managed OTLP push, since the bridge
957
+ // in `instrumentWorker` configures the `@opentelemetry/api`
958
+ // global tracer for us.
956
959
  return withTracing(
957
960
  "deco.http.request",
958
961
  async () => {