@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.
@@ -7,9 +7,11 @@
7
7
  // WinterCG-safe: no Node-builtin imports. Span factory is injected so the logic
8
8
  // is pure and unit-testable without a global provider.
9
9
  // ---------------------------------------------------------------------------
10
- import { context, SpanKind, SpanStatusCode } from "@opentelemetry/api";
10
+ import { context, INVALID_SPAN_CONTEXT, isSpanContextValid, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
11
11
  import { isTracingSuppressed } from "@opentelemetry/core";
12
+ import { scrubKeysAsync } from "./hash.js";
12
13
  import { makeTraceparent } from "./traceparent.js";
14
+ import { nativeTracingFromContext, runNativeSpan } from "./workers-native.js";
13
15
  // ---------------------------------------------------------------------------
14
16
  // Duck-type detectors — conservative; require the distinctive method set
15
17
  // exactly as documented in the task brief.
@@ -61,6 +63,66 @@ function isAnalyticsEngineLike(b) {
61
63
  function isServiceLike(b) {
62
64
  return typeof b?.fetch === "function";
63
65
  }
66
+ /**
67
+ * Adapt a callback-scoped `withSpan` to the immediate-return `startSpan` shape
68
+ * the legacy wrappers use. The callback stays open (pending promise) until the
69
+ * wrapper calls `end()`; calls made before the callback has run are buffered.
70
+ */
71
+ function startSpanFromWithSpan(withSpan) {
72
+ return (name, attrs, kind) => {
73
+ let real;
74
+ const queue = [];
75
+ let release = () => undefined;
76
+ const done = new Promise((resolve) => { release = resolve; });
77
+ let ended = false;
78
+ const apply = (f) => {
79
+ if (real)
80
+ f(real);
81
+ else
82
+ queue.push(f);
83
+ };
84
+ void withSpan(name, attrs, kind, (s) => {
85
+ real = s;
86
+ for (const f of queue.splice(0))
87
+ f(s);
88
+ return done;
89
+ }).catch(() => undefined);
90
+ const deferred = {
91
+ spanContext: () => real?.spanContext() ?? INVALID_SPAN_CONTEXT,
92
+ setAttribute(k, v) { apply((s) => s.setAttribute(k, v)); return deferred; },
93
+ setAttributes(a) { apply((s) => s.setAttributes(a)); return deferred; },
94
+ addEvent(n, a) { apply((s) => s.addEvent(n, a)); return deferred; },
95
+ addLink() { return deferred; },
96
+ addLinks() { return deferred; },
97
+ setStatus(st) { apply((s) => s.setStatus(st)); return deferred; },
98
+ updateName() { return deferred; },
99
+ end() {
100
+ if (ended)
101
+ return;
102
+ ended = true;
103
+ apply((s) => s.end());
104
+ release();
105
+ },
106
+ isRecording: () => real?.isRecording() ?? true,
107
+ recordException(e) { apply((s) => s.recordException(e)); },
108
+ };
109
+ return deferred;
110
+ };
111
+ }
112
+ /**
113
+ * Run an async operation inside a span: `withSpan` when native, else
114
+ * `startSpan` + {@link runWithSpan}.
115
+ */
116
+ function traced(opts, name, attrs, kind, fn) {
117
+ if (opts.withSpan)
118
+ return opts.withSpan(name, attrs, kind, fn);
119
+ const span = opts.startSpan(name, attrs, kind);
120
+ return runWithSpan(span, () => fn(span));
121
+ }
122
+ /** Native name when `opts.nativeNames`, else the direct-mode name. */
123
+ function nm(opts, direct, native) {
124
+ return opts.nativeNames ? native : direct;
125
+ }
64
126
  // ---------------------------------------------------------------------------
65
127
  // Span lifecycle helper
66
128
  // ---------------------------------------------------------------------------
@@ -85,6 +147,68 @@ export async function runWithSpan(span, fn) {
85
147
  span.end();
86
148
  }
87
149
  }
150
+ /** Plain string keys from a string / string[] argument (anything else yields none). */
151
+ export function keysOf(value) {
152
+ if (typeof value === "string")
153
+ return [value];
154
+ if (Array.isArray(value))
155
+ return value.filter((v) => typeof v === "string");
156
+ return [];
157
+ }
158
+ /** At most this many keys of a multi-key call are hashed and recorded. */
159
+ const MAX_HASHED_KEYS = 10;
160
+ /** Key hashes still in flight; exports drain these first so no span is flushed half-built. */
161
+ const pendingKeyHashes = new Set();
162
+ /** Resolves once every in-flight key hash has attached its attribute and ended its span. */
163
+ export async function settlePendingKeyHashes() {
164
+ while (pendingKeyHashes.size > 0)
165
+ await Promise.allSettled([...pendingKeyHashes]);
166
+ }
167
+ /**
168
+ * Like {@link runWithSpan} for operations that carry user-controlled keys. The
169
+ * hash is computed concurrently with the call and attached under `attr` before
170
+ * the span ends; it can neither delay the call's result nor change its error.
171
+ * If hashing is impossible the attribute is omitted (never the raw key).
172
+ */
173
+ export async function runWithSpanKeyed(span, attr, keys, fn, countAttr) {
174
+ // Bounded work per call: hash only the first MAX_HASHED_KEYS; the count is exact.
175
+ if (countAttr && keys.length > 0)
176
+ span.setAttribute(countAttr, keys.length);
177
+ const hashed = keys.length > 0 ? scrubKeysAsync(keys.slice(0, MAX_HASHED_KEYS)) : undefined;
178
+ try {
179
+ return await fn();
180
+ }
181
+ catch (err) {
182
+ recordSpanError(span, err);
183
+ throw err;
184
+ }
185
+ finally {
186
+ const end = () => {
187
+ try {
188
+ span.end();
189
+ }
190
+ catch {
191
+ // Telemetry must never surface errors.
192
+ }
193
+ };
194
+ if (hashed) {
195
+ const done = hashed.then((h) => {
196
+ try {
197
+ if (h !== undefined)
198
+ span.setAttribute(attr, h);
199
+ }
200
+ catch {
201
+ // ignore
202
+ }
203
+ end();
204
+ }, end).finally(() => pendingKeyHashes.delete(done));
205
+ pendingKeyHashes.add(done);
206
+ }
207
+ else {
208
+ end();
209
+ }
210
+ }
211
+ }
88
212
  function runSyncWithSpan(span, fn) {
89
213
  try {
90
214
  return fn();
@@ -113,7 +237,7 @@ function recordSpanError(span, error) {
113
237
  });
114
238
  }
115
239
  /** Trace a call without replacing synchronous, Promise, or custom thenable results. */
116
- function runWithSpanPreservingReturn(span, fn) {
240
+ export function runWithSpanPreservingReturn(span, fn) {
117
241
  try {
118
242
  const result = fn();
119
243
  if (isPromiseLike(result)) {
@@ -200,6 +324,8 @@ function wrapD1Statement(stmt, sql, opts) {
200
324
  const span = opts.startSpan(`D1 ${op}`, {
201
325
  "db.system": "d1",
202
326
  "db.statement": sql,
327
+ "db.system.name": "cloudflare-d1",
328
+ "db.query.text": sql,
203
329
  });
204
330
  return runWithSpan(span, () => stmt[op](...args));
205
331
  };
@@ -225,6 +351,7 @@ function wrapD1(binding, opts) {
225
351
  }
226
352
  const span = opts.startSpan("D1 batch", {
227
353
  "db.system": "d1",
354
+ "db.system.name": "cloudflare-d1",
228
355
  "db.operation.name": "batch",
229
356
  "db.operation.batch.size": statements.length,
230
357
  });
@@ -235,6 +362,7 @@ function wrapD1(binding, opts) {
235
362
  return binding.exec(query);
236
363
  const span = opts.startSpan("D1 exec", {
237
364
  "db.system": "d1",
365
+ "db.system.name": "cloudflare-d1",
238
366
  "db.operation.name": "exec",
239
367
  });
240
368
  return runWithSpan(span, () => binding.exec(query));
@@ -255,6 +383,7 @@ function wrapD1Session(session, opts) {
255
383
  }
256
384
  const span = opts.startSpan("D1 session batch", {
257
385
  "db.system": "d1",
386
+ "db.system.name": "cloudflare-d1",
258
387
  "db.operation.name": "batch",
259
388
  "db.operation.batch.size": statements.length,
260
389
  });
@@ -266,39 +395,33 @@ function wrapD1Session(session, opts) {
266
395
  // KV wrappers
267
396
  // ---------------------------------------------------------------------------
268
397
  function wrapKV(binding, opts, namespace) {
269
- const wrapOp = (op, getKey) => async (...args) => {
398
+ const wrapOp = (op, keyed = false) => async (...args) => {
270
399
  if (isTracingSuppressed(context.active())) {
271
400
  return binding[op](...args);
272
401
  }
273
- const attrs = { "kv.namespace": namespace };
274
- const key = getKey?.(...args);
275
- if (key !== undefined)
276
- attrs["kv.key"] = key;
277
- const span = opts.startSpan(`KV ${op}`, attrs);
278
- return runWithSpan(span, () => binding[op](...args));
402
+ const span = opts.startSpan(`KV ${op}`, { "kv.namespace": namespace });
403
+ return runWithSpanKeyed(span, "cloudflare.kv.query.keys", keyed ? keysOf(args[0]) : [], () => binding[op](...args), "cloudflare.kv.query.key_count");
279
404
  };
280
405
  return makeProxy(binding, {
281
- get: wrapOp("get", (key) => key),
282
- getWithMetadata: wrapOp("getWithMetadata", (key) => key),
283
- put: wrapOp("put", (key) => key),
406
+ get: wrapOp("get", true),
407
+ getWithMetadata: wrapOp("getWithMetadata", true),
408
+ put: wrapOp("put", true),
284
409
  list: wrapOp("list"),
285
- delete: wrapOp("delete", (key) => key),
410
+ delete: wrapOp("delete", true),
286
411
  });
287
412
  }
288
413
  // ---------------------------------------------------------------------------
289
414
  // R2 wrappers
290
415
  // ---------------------------------------------------------------------------
416
+ /** R2 object keys are user data: only the hashed form is ever emitted. */
417
+ const R2_KEY_ATTR = "cloudflare.r2.request.key";
291
418
  function wrapR2(binding, opts, bucket) {
292
- const wrapOp = (op, getKey) => async (...args) => {
419
+ const wrapOp = (op, keyed = false) => async (...args) => {
293
420
  if (isTracingSuppressed(context.active())) {
294
421
  return binding[op](...args);
295
422
  }
296
- const attrs = { "r2.bucket": bucket };
297
- const key = getKey?.(...args);
298
- if (key !== undefined)
299
- attrs["r2.key"] = key;
300
- const span = opts.startSpan(`R2 ${op}`, attrs);
301
- return runWithSpan(span, () => binding[op](...args));
423
+ const span = opts.startSpan(`R2 ${op}`, { "r2.bucket": bucket });
424
+ return runWithSpanKeyed(span, R2_KEY_ATTR, keyed ? keysOf(args[0]) : [], () => binding[op](...args), "cloudflare.r2.request.key_count");
302
425
  };
303
426
  const wrapMultipart = (upload, key) => makeProxy(upload, {
304
427
  uploadPart: async (partNumber, ...args) => {
@@ -307,10 +430,9 @@ function wrapR2(binding, opts, bucket) {
307
430
  }
308
431
  const span = opts.startSpan("R2 uploadPart", {
309
432
  "r2.bucket": bucket,
310
- "r2.key": key,
311
433
  "r2.multipart.part_number": partNumber,
312
434
  });
313
- return runWithSpan(span, () => upload.uploadPart(partNumber, ...args));
435
+ return runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => upload.uploadPart(partNumber, ...args));
314
436
  },
315
437
  abort: wrapMultipartOp("abort", upload, key),
316
438
  complete: wrapMultipartOp("complete", upload, key, (parts) => ({
@@ -323,28 +445,24 @@ function wrapR2(binding, opts, bucket) {
323
445
  return upload[op](...args);
324
446
  const span = opts.startSpan(`R2 ${op}`, {
325
447
  "r2.bucket": bucket,
326
- "r2.key": key,
327
448
  ...extraAttrs?.(...args),
328
449
  });
329
- return runWithSpan(span, () => upload[op](...args));
450
+ return runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => upload[op](...args));
330
451
  };
331
452
  }
332
453
  return makeProxy(binding, {
333
- get: wrapOp("get", (key) => key),
334
- put: wrapOp("put", (key) => key),
335
- head: wrapOp("head", (key) => key),
336
- delete: wrapOp("delete", (key) => key),
454
+ get: wrapOp("get", true),
455
+ put: wrapOp("put", true),
456
+ head: wrapOp("head", true),
457
+ delete: wrapOp("delete", true),
337
458
  list: wrapOp("list"),
338
459
  async createMultipartUpload(key, ...args) {
339
460
  if (isTracingSuppressed(context.active())) {
340
461
  const upload = await binding.createMultipartUpload(key, ...args);
341
462
  return wrapMultipart(upload, key);
342
463
  }
343
- const span = opts.startSpan("R2 createMultipartUpload", {
344
- "r2.bucket": bucket,
345
- "r2.key": key,
346
- });
347
- const upload = await runWithSpan(span, () => binding.createMultipartUpload(key, ...args));
464
+ const span = opts.startSpan("R2 createMultipartUpload", { "r2.bucket": bucket });
465
+ const upload = await runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => binding.createMultipartUpload(key, ...args));
348
466
  return wrapMultipart(upload, key);
349
467
  },
350
468
  resumeMultipartUpload(key, uploadId) {
@@ -360,18 +478,23 @@ function wrapVectorize(binding, opts, indexName) {
360
478
  if (isTracingSuppressed(context.active())) {
361
479
  return binding[op](...args);
362
480
  }
363
- const span = opts.startSpan(`Vectorize ${op}`, {
364
- "vectorize.index": indexName,
365
- });
366
- return runWithSpan(span, () => binding[op](...args));
481
+ const attrs = { "vectorize.index": indexName };
482
+ if (opts.nativeNames) {
483
+ attrs["cloudflare.binding.type"] = "vectorize";
484
+ attrs["cloudflare.binding.name"] = indexName;
485
+ }
486
+ return traced(opts, nm(opts, `Vectorize ${op}`, `vectorize_${op}`), attrs, undefined, () => binding[op](...args));
367
487
  };
368
- return makeProxy(binding, {
488
+ const handlers = {
369
489
  query: wrapOp("query"),
370
490
  upsert: wrapOp("upsert"),
371
491
  insert: wrapOp("insert"),
372
492
  deleteByIds: wrapOp("deleteByIds"),
373
493
  getByIds: wrapOp("getByIds"),
374
- });
494
+ };
495
+ if (opts.nativeNames && typeof binding.describe === "function")
496
+ handlers.describe = wrapOp("describe");
497
+ return makeProxy(binding, handlers);
375
498
  }
376
499
  // ---------------------------------------------------------------------------
377
500
  // Workers AI wrapper
@@ -385,11 +508,17 @@ function wrapWorkersAI(binding, opts, _name) {
385
508
  const attrs = {
386
509
  "gen_ai.system": "cloudflare.workers_ai",
387
510
  };
511
+ if (opts.nativeNames) {
512
+ attrs["cloudflare.binding.type"] = "ai";
513
+ attrs["cloudflare.binding.name"] = _name;
514
+ attrs["gen_ai.provider.name"] = "cloudflare";
515
+ attrs["gen_ai.operation.name"] = "chat";
516
+ }
388
517
  if (typeof args[0] === "string") {
389
518
  attrs["gen_ai.request.model"] = args[0];
390
519
  }
391
- const span = opts.startSpan("AI run", attrs, SpanKind.CLIENT);
392
- try {
520
+ // PRIVACY: never records prompts/completions — only model + token usage.
521
+ return traced(opts, nm(opts, "AI run", "ai_run"), attrs, SpanKind.CLIENT, async (span) => {
393
522
  const result = await binding.run(...args);
394
523
  // Best-effort usage extraction — never read/await/tee the stream.
395
524
  try {
@@ -410,18 +539,7 @@ function wrapWorkersAI(binding, opts, _name) {
410
539
  // Swallow — a weird shape must never throw.
411
540
  }
412
541
  return result;
413
- }
414
- catch (err) {
415
- span.recordException(err instanceof Error ? err : new Error(String(err)));
416
- span.setStatus({
417
- code: SpanStatusCode.ERROR,
418
- message: err instanceof Error ? err.message : String(err),
419
- });
420
- throw err;
421
- }
422
- finally {
423
- span.end();
424
- }
542
+ });
425
543
  },
426
544
  });
427
545
  }
@@ -432,6 +550,7 @@ function wrapQueue(binding, opts, name) {
432
550
  const baseAttrs = {
433
551
  "messaging.system": "cloudflare_queues",
434
552
  "messaging.destination.name": name,
553
+ "cloudflare.queue.name": name,
435
554
  };
436
555
  return makeProxy(binding, {
437
556
  send: async (...args) => {
@@ -481,8 +600,7 @@ function wrapWorkflow(binding, opts, name) {
481
600
  const count = getCount?.(...args);
482
601
  if (count !== undefined)
483
602
  attrs["cloudflare.workflow.batch.size"] = count;
484
- const span = opts.startSpan(`Workflow ${op}`, attrs, kind);
485
- return runWithSpan(span, () => binding[op](...args));
603
+ return traced(opts, nm(opts, `Workflow ${op}`, `workflow_${op}`), attrs, kind, () => binding[op](...args));
486
604
  };
487
605
  return makeProxy(binding, {
488
606
  get: wrapOp("get", SpanKind.CLIENT),
@@ -497,7 +615,7 @@ function wrapRateLimit(binding, opts, name) {
497
615
  if (isTracingSuppressed(context.active()))
498
616
  return binding.limit(...args);
499
617
  const span = opts.startSpan("RateLimit limit", {
500
- "cloudflare.binding.type": "rate_limit",
618
+ "cloudflare.binding.type": "ratelimit",
501
619
  "cloudflare.binding.name": name,
502
620
  });
503
621
  return runWithSpan(span, () => binding.limit(...args));
@@ -509,7 +627,7 @@ function wrapAnalyticsEngine(binding, opts, name) {
509
627
  writeDataPoint(...args) {
510
628
  if (isTracingSuppressed(context.active()))
511
629
  return binding.writeDataPoint(...args);
512
- const span = opts.startSpan("AnalyticsEngine writeDataPoint", {
630
+ const span = opts.startSpan(nm(opts, "AnalyticsEngine writeDataPoint", "analytics_engine_writeDataPoint"), {
513
631
  "cloudflare.binding.type": "analytics_engine",
514
632
  "cloudflare.binding.name": name,
515
633
  });
@@ -557,8 +675,10 @@ function wrapService(binding, opts, name) {
557
675
  }
558
676
  const span = opts.startSpan(`Service ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
559
677
  const sc = span.spanContext();
560
- const traceparent = makeTraceparent(sc);
561
- const injectedArgs = injectTraceparentArgs(args, traceparent);
678
+ // Native spans expose no ids (INVALID context): never inject an all-zero traceparent.
679
+ const injectedArgs = isSpanContextValid(sc)
680
+ ? injectTraceparentArgs(args, makeTraceparent(sc))
681
+ : args;
562
682
  return runWithSpan(span, () => binding.fetch(...injectedArgs));
563
683
  },
564
684
  });
@@ -594,7 +714,9 @@ function wrapDurableObjectStub(stub, opts, name) {
594
714
  const span = opts.startSpan(`DO ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
595
715
  const sc = span.spanContext();
596
716
  // Propagate into the DO so its own spans join this trace.
597
- const injectedArgs = injectTraceparentArgs(args, makeTraceparent(sc));
717
+ const injectedArgs = isSpanContextValid(sc)
718
+ ? injectTraceparentArgs(args, makeTraceparent(sc))
719
+ : args;
598
720
  return runWithSpanPreservingReturn(span, () => stub.fetch(...injectedArgs));
599
721
  };
600
722
  // workerd exposes RPC methods through a wildcard property hook, so method
@@ -699,7 +821,11 @@ function wrapWorkflowInstance(instance, opts, name) {
699
821
  if (instanceId !== undefined) {
700
822
  attrs["cloudflare.workflow.instance_id"] = instanceId;
701
823
  }
702
- const span = opts.startSpan(`Workflow ${method}`, attrs, SpanKind.CLIENT);
824
+ if (opts.nativeNames) {
825
+ attrs["cloudflare.binding.type"] = "workflow";
826
+ attrs["cloudflare.binding.name"] = name;
827
+ }
828
+ const span = opts.startSpan(nm(opts, `Workflow ${method}`, `workflow_${method}`), attrs, SpanKind.CLIENT);
703
829
  return runWithSpanPreservingReturn(span, () => underlying.apply(instance, args));
704
830
  };
705
831
  }
@@ -746,7 +872,7 @@ function wrapWorkflowBinding(binding, opts, name) {
746
872
  attrs["cloudflare.workflow.instance_ids"] = batchIds;
747
873
  attrs["cloudflare.workflow.batch.size"] = batchIds.length;
748
874
  }
749
- const span = opts.startSpan(`Workflow ${op}`, attrs, SpanKind.CLIENT);
875
+ const span = opts.startSpan(nm(opts, `Workflow ${op}`, `workflow_${op}`), attrs, SpanKind.CLIENT);
750
876
  return runWithSpanPreservingReturn(span, () => mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name, span)));
751
877
  };
752
878
  }
@@ -785,52 +911,201 @@ function wrapWorkflowBinding(binding, opts, name) {
785
911
  * @param opts - `startSpan` factory (now accepts optional `SpanKind`) + `select` filter.
786
912
  * @returns A shallow copy of `env` with selected bindings replaced by proxies.
787
913
  */
788
- export function instrumentEnv(env, opts) {
914
+ export function instrumentEnv(env, rawOpts) {
915
+ if (typeof rawOpts.startSpan !== "function" && typeof rawOpts.withSpan !== "function") {
916
+ throw new TypeError("instrumentEnv requires opts.startSpan or opts.withSpan");
917
+ }
918
+ // withSpan wins: the legacy wrappers get an adapter so startSpan is never called.
919
+ const opts = rawOpts.withSpan
920
+ ? { ...rawOpts, startSpan: startSpanFromWithSpan(rawOpts.withSpan) }
921
+ : rawOpts;
789
922
  const result = { ...env };
790
- const { select } = opts;
923
+ const { select, kindFilter } = opts;
924
+ const allow = (kind) => !kindFilter || kindFilter(kind);
925
+ // Direct mode: stamp binding type/name on every span a binding emits. Native
926
+ // mode (`withSpan`) already sets them where it enriches, and stays unchanged.
927
+ const stamped = (type, key) => {
928
+ if (rawOpts.withSpan)
929
+ return opts;
930
+ const base = opts.startSpan;
931
+ return {
932
+ ...opts,
933
+ startSpan: (name, attrs, kind) => base(name, { "cloudflare.binding.type": type, "cloudflare.binding.name": key, ...attrs }, kind),
934
+ };
935
+ };
791
936
  for (const key of Object.keys(env)) {
792
937
  // Filter: if select is an array, only wrap keys in the list.
793
938
  if (select !== true && !select.includes(key))
794
939
  continue;
795
940
  const binding = env[key];
796
941
  if (isD1Like(binding)) {
797
- result[key] = wrapD1(binding, opts);
942
+ if (allow("d1"))
943
+ result[key] = wrapD1(binding, stamped("d1", key));
798
944
  }
799
945
  else if (isR2Like(binding)) {
800
946
  // R2 before KV — R2 also has `list`, so checking `head` first avoids mis-classifying.
801
- result[key] = wrapR2(binding, opts, key);
947
+ if (allow("r2"))
948
+ result[key] = wrapR2(binding, stamped("r2", key), key);
802
949
  }
803
950
  else if (isKVLike(binding)) {
804
- result[key] = wrapKV(binding, opts, key);
951
+ if (allow("kv"))
952
+ result[key] = wrapKV(binding, stamped("kv", key), key);
805
953
  }
806
954
  else if (isVectorizeLike(binding)) {
807
- result[key] = wrapVectorize(binding, opts, key);
955
+ if (allow("vectorize"))
956
+ result[key] = wrapVectorize(binding, stamped("vectorize", key), key);
808
957
  }
809
958
  else if (isWorkersAILike(binding)) {
810
- result[key] = wrapWorkersAI(binding, opts, key);
959
+ if (allow("ai"))
960
+ result[key] = wrapWorkersAI(binding, stamped("ai", key), key);
811
961
  }
812
962
  else if (isQueueLike(binding)) {
813
- result[key] = wrapQueue(binding, opts, key);
963
+ if (allow("queue"))
964
+ result[key] = wrapQueue(binding, stamped("queue", key), key);
814
965
  }
815
966
  else if (isDurableObjectNamespaceLike(binding)) {
816
- result[key] = wrapDurableObjectNamespace(binding, opts, key);
967
+ if (allow("durable_object"))
968
+ result[key] = wrapDurableObjectNamespace(binding, stamped("durable_object", key), key);
817
969
  }
818
970
  else if (isWorkflowBindingLike(binding)) {
819
- result[key] = wrapWorkflowBinding(binding, opts, key);
971
+ if (allow("workflow"))
972
+ result[key] = wrapWorkflowBinding(binding, stamped("workflow", key), key);
820
973
  }
821
974
  else if (isWorkflowLike(binding)) {
822
- result[key] = wrapWorkflow(binding, opts, key);
975
+ if (allow("workflow"))
976
+ result[key] = wrapWorkflow(binding, stamped("workflow", key), key);
823
977
  }
824
978
  else if (isRateLimitLike(binding)) {
825
- result[key] = wrapRateLimit(binding, opts, key);
979
+ if (allow("ratelimit"))
980
+ result[key] = wrapRateLimit(binding, stamped("ratelimit", key), key);
826
981
  }
827
982
  else if (isAnalyticsEngineLike(binding)) {
828
- result[key] = wrapAnalyticsEngine(binding, opts, key);
983
+ if (allow("analytics_engine"))
984
+ result[key] = wrapAnalyticsEngine(binding, stamped("analytics_engine", key), key);
829
985
  }
830
986
  else if (isServiceLike(binding)) {
831
- result[key] = wrapService(binding, opts, key);
987
+ if (allow("service"))
988
+ result[key] = wrapService(binding, stamped("service", key), key);
832
989
  }
833
990
  // Unrecognised bindings are left as-is.
834
991
  }
835
992
  return result;
836
993
  }
994
+ // ---------------------------------------------------------------------------
995
+ // Hyperdrive: wrap the SQL client the app builds on top of the binding
996
+ // ---------------------------------------------------------------------------
997
+ /**
998
+ * Wrap a Postgres/MySQL client (postgres.js, pg, mysql2, ...) created from a
999
+ * Hyperdrive binding so each `query` / `unsafe` / `execute` call, and each
1000
+ * tagged-template call when the client itself is callable (postgres.js
1001
+ * `sql\`select 1\``), emits a `hyperdrive_query` span. `db.query.text` is
1002
+ * recorded raw (template strings are joined with `?`; ingest sanitises). The
1003
+ * ORIGINAL return value is passed through untouched; the span ends when it
1004
+ * settles. Uses the request's native tracer when present, else the global one.
1005
+ */
1006
+ export function wrapSqlClient(client, opts = {}) {
1007
+ const traceCall = (args, run) => {
1008
+ if (isTracingSuppressed(context.active()))
1009
+ return run();
1010
+ const attrs = { "cloudflare.binding.type": "hyperdrive" };
1011
+ if (opts.name)
1012
+ attrs["cloudflare.binding.name"] = opts.name;
1013
+ if (opts.dbSystem)
1014
+ attrs["db.system.name"] = opts.dbSystem;
1015
+ const first = args[0];
1016
+ if (Array.isArray(first) && "raw" in first)
1017
+ attrs["db.query.text"] = first.join("?");
1018
+ else if (typeof first === "string")
1019
+ attrs["db.query.text"] = first;
1020
+ else if (typeof first?.text === "string") {
1021
+ attrs["db.query.text"] = first.text;
1022
+ }
1023
+ const native = nativeTracingFromContext();
1024
+ const startSpan = () => native
1025
+ ? startSpanFromWithSpan((n, at, _k, fn) => runNativeSpan(native, n, at, fn))("hyperdrive_query", attrs, SpanKind.CLIENT)
1026
+ : trace.getTracer("heystack").startSpan("hyperdrive_query", {
1027
+ attributes: attrs,
1028
+ kind: SpanKind.CLIENT,
1029
+ });
1030
+ let result;
1031
+ let span;
1032
+ try {
1033
+ result = run();
1034
+ }
1035
+ catch (error) {
1036
+ span = startSpan();
1037
+ recordSpanError(span, error);
1038
+ span.end();
1039
+ throw error;
1040
+ }
1041
+ if (!isPromiseLike(result)) {
1042
+ // Synchronous result: nothing lazy to protect.
1043
+ span = startSpan();
1044
+ span.end();
1045
+ return result;
1046
+ }
1047
+ // A plain native Promise is already running and `await` bypasses an own
1048
+ // `then`, so observe it eagerly (safe: observing cannot start anything).
1049
+ if (Object.getPrototypeOf(result) === Promise.prototype) {
1050
+ return runWithSpanPreservingReturn(startSpan(), () => result);
1051
+ }
1052
+ // LAZY thenables (postgres.js `Query`) start executing inside `then()`, so we
1053
+ // must never call `then` ourselves: a fragment nested in another query and
1054
+ // never awaited would otherwise run standalone. Instead shadow `then` on the
1055
+ // instance and start the span on the first consumer-driven call.
1056
+ const origThen = result.then;
1057
+ let started = false;
1058
+ try {
1059
+ Object.defineProperty(result, "then", {
1060
+ configurable: true,
1061
+ writable: true,
1062
+ value: function (onOk, onErr) {
1063
+ if (started)
1064
+ return origThen.call(this, onOk, onErr);
1065
+ started = true;
1066
+ const sp = startSpan();
1067
+ return origThen.call(this, (v) => {
1068
+ try {
1069
+ sp.end();
1070
+ }
1071
+ catch { /* never alter the result */ }
1072
+ return onOk ? onOk(v) : v;
1073
+ }, (e) => {
1074
+ try {
1075
+ recordSpanError(sp, e);
1076
+ sp.end();
1077
+ }
1078
+ catch { /* never alter the result */ }
1079
+ if (onErr)
1080
+ return onErr(e);
1081
+ throw e;
1082
+ });
1083
+ },
1084
+ });
1085
+ }
1086
+ catch {
1087
+ // Frozen / non-extensible: return untraced.
1088
+ }
1089
+ return result;
1090
+ };
1091
+ const methods = new Map();
1092
+ return new Proxy(client, {
1093
+ get(target, prop, receiver) {
1094
+ const value = Reflect.get(target, prop, receiver);
1095
+ if (typeof value !== "function")
1096
+ return value;
1097
+ if (prop === "query" || prop === "unsafe" || prop === "execute") {
1098
+ let wrapped = methods.get(prop);
1099
+ if (!wrapped) {
1100
+ wrapped = (...args) => traceCall(args, () => value.apply(target, args));
1101
+ methods.set(prop, wrapped);
1102
+ }
1103
+ return wrapped;
1104
+ }
1105
+ return value.bind(target);
1106
+ },
1107
+ apply(target, thisArg, args) {
1108
+ return traceCall(args, () => Reflect.apply(target, thisArg, args));
1109
+ },
1110
+ });
1111
+ }
@@ -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;