@heystack/otel 0.14.0 → 0.16.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.
@@ -1,15 +1,15 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // Cloudflare binding instrumentation for @heystack/otel/workers.
3
3
  //
4
- // Wraps D1, KV, R2, Vectorize, Workers AI, Queue producers, and Service
5
- // bindings with OTel child spans so that every binding operation is visible
6
- // as a child of the active request span.
4
+ // Wraps supported Cloudflare data, AI, messaging, Durable Object, Workflow,
5
+ // and Service binding operations with OTel child spans.
7
6
  //
8
- // WinterCG-safe: no `node:*` imports. Span factory is injected so the logic
7
+ // WinterCG-safe: no Node-builtin imports. Span factory is injected so the logic
9
8
  // is pure and unit-testable without a global provider.
10
9
  // ---------------------------------------------------------------------------
11
10
  import { context, SpanKind, SpanStatusCode } from "@opentelemetry/api";
12
11
  import { isTracingSuppressed } from "@opentelemetry/core";
12
+ import { makeTraceparent } from "./traceparent.js";
13
13
  // ---------------------------------------------------------------------------
14
14
  // Duck-type detectors — conservative; require the distinctive method set
15
15
  // exactly as documented in the task brief.
@@ -45,6 +45,17 @@ function isQueueLike(b) {
45
45
  return (typeof b?.send === "function" &&
46
46
  typeof b?.sendBatch === "function");
47
47
  }
48
+ function isWorkflowLike(b) {
49
+ return (typeof b?.get === "function" &&
50
+ typeof b?.create === "function" &&
51
+ typeof b?.createBatch === "function");
52
+ }
53
+ function isRateLimitLike(b) {
54
+ return typeof b?.limit === "function";
55
+ }
56
+ function isAnalyticsEngineLike(b) {
57
+ return typeof b?.writeDataPoint === "function";
58
+ }
48
59
  /** Service binding / Fetcher: last branch — anything with `.fetch`. Prior branches
49
60
  * exclude D1/R2/KV/Vectorize/AI/Queue, so order alone is sufficient. */
50
61
  function isServiceLike(b) {
@@ -53,7 +64,12 @@ function isServiceLike(b) {
53
64
  // ---------------------------------------------------------------------------
54
65
  // Span lifecycle helper
55
66
  // ---------------------------------------------------------------------------
56
- async function runWithSpan(span, fn) {
67
+ /**
68
+ * Run `fn`, ending `span` in `finally` and recording exceptions + ERROR status
69
+ * on failure (re-throwing). Internal helper shared with the class-level
70
+ * wrappers in workers.ts.
71
+ */
72
+ export async function runWithSpan(span, fn) {
57
73
  try {
58
74
  return await fn();
59
75
  }
@@ -69,12 +85,100 @@ async function runWithSpan(span, fn) {
69
85
  span.end();
70
86
  }
71
87
  }
88
+ function runSyncWithSpan(span, fn) {
89
+ try {
90
+ return fn();
91
+ }
92
+ catch (err) {
93
+ span.recordException(err instanceof Error ? err : new Error(String(err)));
94
+ span.setStatus({
95
+ code: SpanStatusCode.ERROR,
96
+ message: err instanceof Error ? err.message : String(err),
97
+ });
98
+ throw err;
99
+ }
100
+ finally {
101
+ span.end();
102
+ }
103
+ }
104
+ function isPromiseLike(value) {
105
+ return (((typeof value === "object" && value !== null) || typeof value === "function") &&
106
+ typeof value.then === "function");
107
+ }
108
+ function recordSpanError(span, error) {
109
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
110
+ span.setStatus({
111
+ code: SpanStatusCode.ERROR,
112
+ message: error instanceof Error ? error.message : String(error),
113
+ });
114
+ }
115
+ /** Trace a call without replacing synchronous, Promise, or custom thenable results. */
116
+ function runWithSpanPreservingReturn(span, fn) {
117
+ try {
118
+ const result = fn();
119
+ if (isPromiseLike(result)) {
120
+ let settled = false;
121
+ const finish = (error, failed = false) => {
122
+ if (settled)
123
+ return;
124
+ settled = true;
125
+ if (failed) {
126
+ try {
127
+ recordSpanError(span, error);
128
+ }
129
+ catch {
130
+ // Instrumentation must not alter the observed thenable's behaviour.
131
+ }
132
+ }
133
+ try {
134
+ span.end();
135
+ }
136
+ catch {
137
+ // Instrumentation must not alter the observed thenable's behaviour.
138
+ }
139
+ };
140
+ try {
141
+ const observer = result.then(() => finish(), (error) => finish(error, true));
142
+ // A non-compliant custom thenable may reject the observer despite the
143
+ // rejection callback above. Consume that rejection without replacing
144
+ // or awaiting the original RPC/Promise object returned to the caller.
145
+ if (isPromiseLike(observer)) {
146
+ try {
147
+ observer.then(undefined, () => undefined);
148
+ }
149
+ catch {
150
+ // Calling then() is best-effort for exotic host thenables.
151
+ }
152
+ }
153
+ }
154
+ catch (error) {
155
+ finish(error, true);
156
+ }
157
+ return result;
158
+ }
159
+ span.end();
160
+ return result;
161
+ }
162
+ catch (error) {
163
+ recordSpanError(span, error);
164
+ span.end();
165
+ throw error;
166
+ }
167
+ }
168
+ /** Transform either a synchronous result or a promise without changing its category. */
169
+ function mapReturn(result, map) {
170
+ return isPromiseLike(result) ? Promise.resolve(result).then(map) : map(result);
171
+ }
72
172
  // ---------------------------------------------------------------------------
73
173
  // Proxy helper — intercepts listed handlers, falls through to prototype for rest
74
174
  // ---------------------------------------------------------------------------
75
- function makeProxy(target, handlers) {
175
+ const rawTarget = Symbol("heystack.rawBindingTarget");
176
+ /** Internal proxy builder shared with the class-level wrappers in workers.ts. */
177
+ export function makeProxy(target, handlers) {
76
178
  return new Proxy(target, {
77
179
  get(t, prop, receiver) {
180
+ if (prop === rawTarget)
181
+ return t;
78
182
  if (typeof prop === "string" && prop in handlers)
79
183
  return handlers[prop];
80
184
  const val = Reflect.get(t, prop, receiver);
@@ -82,6 +186,9 @@ function makeProxy(target, handlers) {
82
186
  },
83
187
  });
84
188
  }
189
+ function unwrapTarget(value) {
190
+ return (value?.[rawTarget] ?? value);
191
+ }
85
192
  // ---------------------------------------------------------------------------
86
193
  // D1 wrappers
87
194
  // ---------------------------------------------------------------------------
@@ -112,6 +219,47 @@ function wrapD1(binding, opts) {
112
219
  prepare(sql) {
113
220
  return wrapD1Statement(binding.prepare(sql), sql, opts);
114
221
  },
222
+ batch(statements) {
223
+ if (isTracingSuppressed(context.active())) {
224
+ return binding.batch(statements.map(unwrapTarget));
225
+ }
226
+ const span = opts.startSpan("D1 batch", {
227
+ "db.system": "d1",
228
+ "db.operation.name": "batch",
229
+ "db.operation.batch.size": statements.length,
230
+ });
231
+ return runWithSpan(span, () => binding.batch(statements.map(unwrapTarget)));
232
+ },
233
+ exec(query) {
234
+ if (isTracingSuppressed(context.active()))
235
+ return binding.exec(query);
236
+ const span = opts.startSpan("D1 exec", {
237
+ "db.system": "d1",
238
+ "db.operation.name": "exec",
239
+ });
240
+ return runWithSpan(span, () => binding.exec(query));
241
+ },
242
+ withSession(...args) {
243
+ return wrapD1Session(binding.withSession(...args), opts);
244
+ },
245
+ });
246
+ }
247
+ function wrapD1Session(session, opts) {
248
+ return makeProxy(session, {
249
+ prepare(sql) {
250
+ return wrapD1Statement(session.prepare(sql), sql, opts);
251
+ },
252
+ batch(statements) {
253
+ if (isTracingSuppressed(context.active())) {
254
+ return session.batch(statements.map(unwrapTarget));
255
+ }
256
+ const span = opts.startSpan("D1 session batch", {
257
+ "db.system": "d1",
258
+ "db.operation.name": "batch",
259
+ "db.operation.batch.size": statements.length,
260
+ });
261
+ return runWithSpan(span, () => session.batch(statements.map(unwrapTarget)));
262
+ },
115
263
  });
116
264
  }
117
265
  // ---------------------------------------------------------------------------
@@ -131,6 +279,7 @@ function wrapKV(binding, opts, namespace) {
131
279
  };
132
280
  return makeProxy(binding, {
133
281
  get: wrapOp("get", (key) => key),
282
+ getWithMetadata: wrapOp("getWithMetadata", (key) => key),
134
283
  put: wrapOp("put", (key) => key),
135
284
  list: wrapOp("list"),
136
285
  delete: wrapOp("delete", (key) => key),
@@ -151,11 +300,56 @@ function wrapR2(binding, opts, bucket) {
151
300
  const span = opts.startSpan(`R2 ${op}`, attrs);
152
301
  return runWithSpan(span, () => binding[op](...args));
153
302
  };
303
+ const wrapMultipart = (upload, key) => makeProxy(upload, {
304
+ uploadPart: async (partNumber, ...args) => {
305
+ if (isTracingSuppressed(context.active())) {
306
+ return upload.uploadPart(partNumber, ...args);
307
+ }
308
+ const span = opts.startSpan("R2 uploadPart", {
309
+ "r2.bucket": bucket,
310
+ "r2.key": key,
311
+ "r2.multipart.part_number": partNumber,
312
+ });
313
+ return runWithSpan(span, () => upload.uploadPart(partNumber, ...args));
314
+ },
315
+ abort: wrapMultipartOp("abort", upload, key),
316
+ complete: wrapMultipartOp("complete", upload, key, (parts) => ({
317
+ "r2.multipart.part_count": parts.length,
318
+ })),
319
+ });
320
+ function wrapMultipartOp(op, upload, key, extraAttrs) {
321
+ return async (...args) => {
322
+ if (isTracingSuppressed(context.active()))
323
+ return upload[op](...args);
324
+ const span = opts.startSpan(`R2 ${op}`, {
325
+ "r2.bucket": bucket,
326
+ "r2.key": key,
327
+ ...extraAttrs?.(...args),
328
+ });
329
+ return runWithSpan(span, () => upload[op](...args));
330
+ };
331
+ }
154
332
  return makeProxy(binding, {
155
333
  get: wrapOp("get", (key) => key),
156
334
  put: wrapOp("put", (key) => key),
157
335
  head: wrapOp("head", (key) => key),
158
336
  delete: wrapOp("delete", (key) => key),
337
+ list: wrapOp("list"),
338
+ async createMultipartUpload(key, ...args) {
339
+ if (isTracingSuppressed(context.active())) {
340
+ const upload = await binding.createMultipartUpload(key, ...args);
341
+ return wrapMultipart(upload, key);
342
+ }
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));
348
+ return wrapMultipart(upload, key);
349
+ },
350
+ resumeMultipartUpload(key, uploadId) {
351
+ return wrapMultipart(binding.resumeMultipartUpload(key, uploadId), key);
352
+ },
159
353
  });
160
354
  }
161
355
  // ---------------------------------------------------------------------------
@@ -261,6 +455,66 @@ function wrapQueue(binding, opts, name) {
261
455
  const span = opts.startSpan("Queue sendBatch", batchAttrs, SpanKind.PRODUCER);
262
456
  return runWithSpan(span, () => binding.sendBatch(...args));
263
457
  },
458
+ metrics: async (...args) => {
459
+ if (isTracingSuppressed(context.active()))
460
+ return binding.metrics(...args);
461
+ const span = opts.startSpan("Queue metrics", {
462
+ ...baseAttrs,
463
+ "messaging.operation": "metrics",
464
+ });
465
+ return runWithSpan(span, () => binding.metrics(...args));
466
+ },
467
+ });
468
+ }
469
+ // ---------------------------------------------------------------------------
470
+ // Workflow, rate-limit, and Analytics Engine bindings
471
+ // ---------------------------------------------------------------------------
472
+ function wrapWorkflow(binding, opts, name) {
473
+ const wrapOp = (op, kind, getCount) => async (...args) => {
474
+ if (isTracingSuppressed(context.active()))
475
+ return binding[op](...args);
476
+ const attrs = {
477
+ "cloudflare.binding.type": "workflow",
478
+ "cloudflare.binding.name": name,
479
+ "cloudflare.workflow.operation": op,
480
+ };
481
+ const count = getCount?.(...args);
482
+ if (count !== undefined)
483
+ attrs["cloudflare.workflow.batch.size"] = count;
484
+ const span = opts.startSpan(`Workflow ${op}`, attrs, kind);
485
+ return runWithSpan(span, () => binding[op](...args));
486
+ };
487
+ return makeProxy(binding, {
488
+ get: wrapOp("get", SpanKind.CLIENT),
489
+ create: wrapOp("create", SpanKind.PRODUCER),
490
+ createBatch: wrapOp("createBatch", SpanKind.PRODUCER, (batch) => batch.length),
491
+ deleteBatch: wrapOp("deleteBatch", SpanKind.CLIENT, (ids) => ids.length),
492
+ });
493
+ }
494
+ function wrapRateLimit(binding, opts, name) {
495
+ return makeProxy(binding, {
496
+ limit: async (...args) => {
497
+ if (isTracingSuppressed(context.active()))
498
+ return binding.limit(...args);
499
+ const span = opts.startSpan("RateLimit limit", {
500
+ "cloudflare.binding.type": "rate_limit",
501
+ "cloudflare.binding.name": name,
502
+ });
503
+ return runWithSpan(span, () => binding.limit(...args));
504
+ },
505
+ });
506
+ }
507
+ function wrapAnalyticsEngine(binding, opts, name) {
508
+ return makeProxy(binding, {
509
+ writeDataPoint(...args) {
510
+ if (isTracingSuppressed(context.active()))
511
+ return binding.writeDataPoint(...args);
512
+ const span = opts.startSpan("AnalyticsEngine writeDataPoint", {
513
+ "cloudflare.binding.type": "analytics_engine",
514
+ "cloudflare.binding.name": name,
515
+ });
516
+ return runSyncWithSpan(span, () => binding.writeDataPoint(...args));
517
+ },
264
518
  });
265
519
  }
266
520
  // ---------------------------------------------------------------------------
@@ -276,6 +530,14 @@ function wrapQueue(binding, opts, name) {
276
530
  export function injectTraceparentArgs(args, traceparent) {
277
531
  const [input, ...rest] = args;
278
532
  if (input instanceof Request) {
533
+ const originalInit = rest[0];
534
+ if (originalInit?.headers !== undefined) {
535
+ const init = { ...originalInit };
536
+ const headers = new Headers(originalInit.headers);
537
+ headers.set("traceparent", traceparent);
538
+ init.headers = headers;
539
+ return [input, init, ...rest.slice(1)];
540
+ }
279
541
  const headers = new Headers(input.headers);
280
542
  headers.set("traceparent", traceparent);
281
543
  return [new Request(input, { headers }), ...rest];
@@ -295,13 +557,202 @@ function wrapService(binding, opts, name) {
295
557
  }
296
558
  const span = opts.startSpan(`Service ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
297
559
  const sc = span.spanContext();
298
- const traceparent = `00-${sc.traceId}-${sc.spanId}-01`;
560
+ const traceparent = makeTraceparent(sc);
299
561
  const injectedArgs = injectTraceparentArgs(args, traceparent);
300
562
  return runWithSpan(span, () => binding.fetch(...injectedArgs));
301
563
  },
302
564
  });
303
565
  }
304
566
  // ---------------------------------------------------------------------------
567
+ // Durable Object namespace binding
568
+ // ---------------------------------------------------------------------------
569
+ /** DO namespace detector using the real synchronous Cloudflare API surface. */
570
+ function isDurableObjectNamespaceLike(b) {
571
+ return (typeof b?.idFromName === "function" &&
572
+ typeof b?.get === "function" &&
573
+ typeof b?.newUniqueId === "function");
574
+ }
575
+ /** Fetcher/DO members that are platform operations or must retain host semantics. */
576
+ const DO_STUB_RPC_EXCLUDED = new Set([
577
+ "fetch",
578
+ "connect",
579
+ "alarm",
580
+ "webSocketMessage",
581
+ "webSocketClose",
582
+ "webSocketError",
583
+ "id",
584
+ "name",
585
+ "dup",
586
+ "constructor",
587
+ "then",
588
+ ]);
589
+ /** Wrap a DO stub: CLIENT spans on fetch (+ traceparent injection) and RPC calls. */
590
+ function wrapDurableObjectStub(stub, opts, name) {
591
+ const fetch = (...args) => {
592
+ if (isTracingSuppressed(context.active()))
593
+ return stub.fetch(...args);
594
+ const span = opts.startSpan(`DO ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
595
+ const sc = span.spanContext();
596
+ // Propagate into the DO so its own spans join this trace.
597
+ const injectedArgs = injectTraceparentArgs(args, makeTraceparent(sc));
598
+ return runWithSpanPreservingReturn(span, () => stub.fetch(...injectedArgs));
599
+ };
600
+ // workerd exposes RPC methods through a wildcard property hook, so method
601
+ // names are not enumerable. Resolve and wrap them lazily on first access.
602
+ const rpcMethods = new Map();
603
+ return new Proxy(stub, {
604
+ get(target, property) {
605
+ if (property === "fetch")
606
+ return fetch;
607
+ const value = Reflect.get(target, property, target);
608
+ if (typeof property !== "string" ||
609
+ typeof value !== "function" ||
610
+ DO_STUB_RPC_EXCLUDED.has(property) ||
611
+ property in Object.prototype) {
612
+ return typeof value === "function" ? value.bind(target) : value;
613
+ }
614
+ const cached = rpcMethods.get(property);
615
+ if (cached)
616
+ return cached;
617
+ // Capture the host callable returned for this property. Calling it with
618
+ // the original stub as receiver preserves both ordinary and workerd RPC
619
+ // method semantics; the wrapper itself remains stable across reads.
620
+ const method = value;
621
+ const wrapped = (...args) => {
622
+ if (isTracingSuppressed(context.active())) {
623
+ return Reflect.apply(method, target, args);
624
+ }
625
+ const span = opts.startSpan(`DO ${name} ${property}`, { "peer.service": name, "rpc.method": property }, SpanKind.CLIENT);
626
+ return runWithSpanPreservingReturn(span, () => Reflect.apply(method, target, args));
627
+ };
628
+ rpcMethods.set(property, wrapped);
629
+ return wrapped;
630
+ },
631
+ });
632
+ }
633
+ function wrapDurableObjectNamespace(ns, opts, name) {
634
+ const handlers = {
635
+ // Namespace lookup is local and synchronous. Trace the returned stub's
636
+ // actual fetch/RPC calls, not get() itself.
637
+ get: (...args) => wrapDurableObjectStub(ns.get(...args), opts, name),
638
+ };
639
+ if (typeof ns.getByName === "function") {
640
+ handlers.getByName = (...args) => wrapDurableObjectStub(ns.getByName(...args), opts, name);
641
+ }
642
+ if (typeof ns.jurisdiction === "function") {
643
+ handlers.jurisdiction = (...args) => wrapDurableObjectNamespace(ns.jurisdiction(...args), opts, name);
644
+ }
645
+ // idFromName/idFromString/newUniqueId are synchronous local operations and
646
+ // fall through untouched.
647
+ return makeProxy(ns, handlers);
648
+ }
649
+ // ---------------------------------------------------------------------------
650
+ // Workflows binding wrapper
651
+ // ---------------------------------------------------------------------------
652
+ /** Workflow binding: exposes create/createBatch/get and is not a DO namespace. */
653
+ function isWorkflowBindingLike(b) {
654
+ return (typeof b?.create === "function" &&
655
+ typeof b?.createBatch === "function" &&
656
+ typeof b?.get === "function" &&
657
+ !isDurableObjectNamespaceLike(b));
658
+ }
659
+ const WORKFLOW_INSTANCE_METHODS = [
660
+ "pause",
661
+ "resume",
662
+ "terminate",
663
+ "restart",
664
+ "status",
665
+ "sendEvent",
666
+ ];
667
+ function workflowId(value) {
668
+ if (typeof value === "string" && value.length > 0)
669
+ return value;
670
+ if (value && typeof value === "object") {
671
+ const id = value.id;
672
+ if (typeof id === "string" && id.length > 0)
673
+ return id;
674
+ }
675
+ return undefined;
676
+ }
677
+ function workflowBatchIds(value) {
678
+ if (!Array.isArray(value))
679
+ return [];
680
+ return value.map(workflowId).filter((id) => id !== undefined);
681
+ }
682
+ function wrapWorkflowInstance(instance, opts, name) {
683
+ if (!instance || typeof instance !== "object")
684
+ return instance;
685
+ const target = instance;
686
+ const handlers = {};
687
+ const instanceId = workflowId(instance);
688
+ for (const method of WORKFLOW_INSTANCE_METHODS) {
689
+ if (typeof target[method] !== "function")
690
+ continue;
691
+ handlers[method] = (...args) => {
692
+ const underlying = target[method];
693
+ if (isTracingSuppressed(context.active())) {
694
+ return underlying.apply(instance, args);
695
+ }
696
+ const attrs = {
697
+ "cloudflare.workflow.namespace": name,
698
+ };
699
+ if (instanceId !== undefined) {
700
+ attrs["cloudflare.workflow.instance_id"] = instanceId;
701
+ }
702
+ const span = opts.startSpan(`Workflow ${method}`, attrs, SpanKind.CLIENT);
703
+ return runWithSpanPreservingReturn(span, () => underlying.apply(instance, args));
704
+ };
705
+ }
706
+ return makeProxy(instance, handlers);
707
+ }
708
+ function wrapWorkflowBindingResult(value, op, opts, name, span) {
709
+ if (op === "deleteBatch")
710
+ return value;
711
+ if (op === "createBatch") {
712
+ if (!Array.isArray(value))
713
+ return value;
714
+ const returnedIds = workflowBatchIds(value);
715
+ if (span && returnedIds.length > 0) {
716
+ span.setAttribute("cloudflare.workflow.instance_ids", returnedIds);
717
+ }
718
+ return value.map((item) => wrapWorkflowInstance(item, opts, name));
719
+ }
720
+ const returnedId = workflowId(value);
721
+ if (span && returnedId !== undefined) {
722
+ span.setAttribute("cloudflare.workflow.instance_id", returnedId);
723
+ }
724
+ return wrapWorkflowInstance(value, opts, name);
725
+ }
726
+ function wrapWorkflowBinding(binding, opts, name) {
727
+ const baseAttrs = {
728
+ "cloudflare.workflow.namespace": name,
729
+ "cloudflare.binding.type": "workflow",
730
+ "cloudflare.binding.name": name,
731
+ };
732
+ const ops = ["create", "createBatch", "get", "deleteBatch"];
733
+ const handlers = {};
734
+ for (const op of ops) {
735
+ handlers[op] = (...args) => {
736
+ if (isTracingSuppressed(context.active())) {
737
+ return mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name));
738
+ }
739
+ const attrs = { ...baseAttrs };
740
+ const instanceId = workflowId(args[0]);
741
+ const batchIds = op === "createBatch" || op === "deleteBatch" ? workflowBatchIds(args[0]) : [];
742
+ if (instanceId !== undefined) {
743
+ attrs["cloudflare.workflow.instance_id"] = instanceId;
744
+ }
745
+ if (batchIds.length > 0) {
746
+ attrs["cloudflare.workflow.instance_ids"] = batchIds;
747
+ attrs["cloudflare.workflow.batch.size"] = batchIds.length;
748
+ }
749
+ const span = opts.startSpan(`Workflow ${op}`, attrs, SpanKind.CLIENT);
750
+ return runWithSpanPreservingReturn(span, () => mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name, span)));
751
+ };
752
+ }
753
+ return makeProxy(binding, handlers);
754
+ }
755
+ // ---------------------------------------------------------------------------
305
756
  // Main export
306
757
  // ---------------------------------------------------------------------------
307
758
  /**
@@ -315,6 +766,11 @@ function wrapService(binding, opts, name) {
315
766
  * - Vectorize: `query`+`upsert`
316
767
  * - Workers AI: `run` (without `prepare`)
317
768
  * - Queue producer: `send`+`sendBatch`
769
+ * - Workflow: `get`+`create`+`createBatch`
770
+ * - Rate limit: `limit`
771
+ * - Analytics Engine: `writeDataPoint`
772
+ * - Durable Object namespace: `idFromName`+`get`+`newUniqueId` (stub fetch/RPC spans)
773
+ * - Workflow binding: `create`+`createBatch`+`get` and not a DO namespace
318
774
  * - Service binding / Fetcher: `fetch` (last branch; most generic)
319
775
  *
320
776
  * Unrecognised bindings are passed through unchanged.
@@ -356,6 +812,21 @@ export function instrumentEnv(env, opts) {
356
812
  else if (isQueueLike(binding)) {
357
813
  result[key] = wrapQueue(binding, opts, key);
358
814
  }
815
+ else if (isDurableObjectNamespaceLike(binding)) {
816
+ result[key] = wrapDurableObjectNamespace(binding, opts, key);
817
+ }
818
+ else if (isWorkflowBindingLike(binding)) {
819
+ result[key] = wrapWorkflowBinding(binding, opts, key);
820
+ }
821
+ else if (isWorkflowLike(binding)) {
822
+ result[key] = wrapWorkflow(binding, opts, key);
823
+ }
824
+ else if (isRateLimitLike(binding)) {
825
+ result[key] = wrapRateLimit(binding, opts, key);
826
+ }
827
+ else if (isAnalyticsEngineLike(binding)) {
828
+ result[key] = wrapAnalyticsEngine(binding, opts, key);
829
+ }
359
830
  else if (isServiceLike(binding)) {
360
831
  result[key] = wrapService(binding, opts, key);
361
832
  }