autotel-cloudflare 2.1.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +432 -0
  3. package/dist/actors.d.ts +248 -0
  4. package/dist/actors.js +1030 -0
  5. package/dist/actors.js.map +1 -0
  6. package/dist/agents.d.ts +219 -0
  7. package/dist/agents.js +276 -0
  8. package/dist/agents.js.map +1 -0
  9. package/dist/bindings.d.ts +40 -0
  10. package/dist/bindings.js +4 -0
  11. package/dist/bindings.js.map +1 -0
  12. package/dist/chunk-JDPN3HND.js +520 -0
  13. package/dist/chunk-JDPN3HND.js.map +1 -0
  14. package/dist/chunk-QXFYTHQF.js +298 -0
  15. package/dist/chunk-QXFYTHQF.js.map +1 -0
  16. package/dist/chunk-SKKRPS5K.js +50 -0
  17. package/dist/chunk-SKKRPS5K.js.map +1 -0
  18. package/dist/events.d.ts +1 -0
  19. package/dist/events.js +3 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/handlers.d.ts +121 -0
  22. package/dist/handlers.js +4 -0
  23. package/dist/handlers.js.map +1 -0
  24. package/dist/index.d.ts +144 -0
  25. package/dist/index.js +576 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/logger.d.ts +1 -0
  28. package/dist/logger.js +3 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/sampling.d.ts +4 -0
  31. package/dist/sampling.js +3 -0
  32. package/dist/sampling.js.map +1 -0
  33. package/dist/testing.d.ts +1 -0
  34. package/dist/testing.js +3 -0
  35. package/dist/testing.js.map +1 -0
  36. package/package.json +107 -0
  37. package/src/actors/alarms.ts +225 -0
  38. package/src/actors/index.ts +36 -0
  39. package/src/actors/instrument-actor.test.ts +179 -0
  40. package/src/actors/instrument-actor.ts +574 -0
  41. package/src/actors/sockets.ts +217 -0
  42. package/src/actors/storage.ts +263 -0
  43. package/src/actors/traced-handler.ts +300 -0
  44. package/src/actors/types.ts +98 -0
  45. package/src/actors.ts +50 -0
  46. package/src/agents/index.ts +42 -0
  47. package/src/agents/otel-observability.test.ts +329 -0
  48. package/src/agents/otel-observability.ts +465 -0
  49. package/src/agents/types.ts +167 -0
  50. package/src/agents.ts +76 -0
  51. package/src/bindings/bindings.ts +621 -0
  52. package/src/bindings/common.ts +75 -0
  53. package/src/bindings/index.ts +12 -0
  54. package/src/bindings.ts +6 -0
  55. package/src/events.ts +6 -0
  56. package/src/global/cache.test.ts +292 -0
  57. package/src/global/cache.ts +164 -0
  58. package/src/global/fetch.test.ts +344 -0
  59. package/src/global/fetch.ts +134 -0
  60. package/src/global/index.ts +7 -0
  61. package/src/handlers/durable-objects.test.ts +524 -0
  62. package/src/handlers/durable-objects.ts +250 -0
  63. package/src/handlers/index.ts +6 -0
  64. package/src/handlers/workflows.ts +318 -0
  65. package/src/handlers.ts +6 -0
  66. package/src/index.ts +57 -0
  67. package/src/logger.ts +6 -0
  68. package/src/sampling.ts +6 -0
  69. package/src/testing.ts +6 -0
  70. package/src/wrappers/index.ts +8 -0
  71. package/src/wrappers/instrument.integration.test.ts +468 -0
  72. package/src/wrappers/instrument.ts +643 -0
  73. package/src/wrappers/wrap-do.ts +34 -0
  74. package/src/wrappers/wrap-module.ts +37 -0
@@ -0,0 +1,621 @@
1
+ /**
2
+ * Auto-instrumentation for Cloudflare Workers bindings
3
+ *
4
+ * Note: This file uses Cloudflare Workers types (KVNamespace, R2Bucket, D1Database, Fetcher, etc.)
5
+ * which are globally available via @cloudflare/workers-types when listed in tsconfig.json.
6
+ * These types are devDependencies only - they're not runtime dependencies.
7
+ * At runtime, Cloudflare Workers runtime provides the actual implementations.
8
+ *
9
+ * This module provides automatic tracing for Cloudflare bindings:
10
+ * - KV (key-value operations)
11
+ * - R2 (object storage operations)
12
+ * - D1 (database operations)
13
+ * - Service Bindings
14
+ * - Events Engine
15
+ * - Workers AI
16
+ * - Vectorize
17
+ * - Hyperdrive
18
+ */
19
+
20
+ import {
21
+ trace,
22
+ SpanKind,
23
+ SpanStatusCode,
24
+ } from '@opentelemetry/api';
25
+ import { WorkerTracer } from 'autotel-edge';
26
+ import { wrap } from './common';
27
+
28
+ /**
29
+ * Instrument KV namespace
30
+ */
31
+ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: string): K {
32
+ const name = namespaceName || 'kv';
33
+
34
+ const kvHandler: ProxyHandler<K> = {
35
+ get(target, prop) {
36
+ const value = Reflect.get(target, prop);
37
+
38
+ if (prop === 'get' && typeof value === 'function') {
39
+ return new Proxy(value, {
40
+ apply: (fnTarget, thisArg, args) => {
41
+ const [key, options] = args as [string, KVNamespaceGetOptions<unknown> | undefined];
42
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
43
+
44
+ return tracer.startActiveSpan(
45
+ `KV ${name}: get`,
46
+ {
47
+ kind: SpanKind.CLIENT,
48
+ attributes: {
49
+ 'db.system': 'cloudflare-kv',
50
+ 'db.operation': 'get',
51
+ 'db.namespace': name,
52
+ 'db.key': key,
53
+ 'db.cache_hit': options?.cacheTtl !== undefined,
54
+ },
55
+ },
56
+ async (span) => {
57
+ try {
58
+ const result = await Reflect.apply(fnTarget, thisArg, args);
59
+ span.setAttribute('db.result.type', result === null ? 'null' : typeof result);
60
+ span.setStatus({ code: SpanStatusCode.OK });
61
+ return result;
62
+ } catch (error) {
63
+ span.recordException(error as Error);
64
+ span.setStatus({
65
+ code: SpanStatusCode.ERROR,
66
+ message: error instanceof Error ? error.message : String(error),
67
+ });
68
+ throw error;
69
+ } finally {
70
+ span.end();
71
+ }
72
+ },
73
+ );
74
+ },
75
+ });
76
+ }
77
+
78
+ if (prop === 'put' && typeof value === 'function') {
79
+ return new Proxy(value, {
80
+ apply: (fnTarget, thisArg, args) => {
81
+ const [key] = args as [string, unknown, KVNamespacePutOptions | undefined];
82
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
83
+
84
+ return tracer.startActiveSpan(
85
+ `KV ${name}: put`,
86
+ {
87
+ kind: SpanKind.CLIENT,
88
+ attributes: {
89
+ 'db.system': 'cloudflare-kv',
90
+ 'db.operation': 'put',
91
+ 'db.namespace': name,
92
+ 'db.key': key,
93
+ },
94
+ },
95
+ async (span) => {
96
+ try {
97
+ const result = await Reflect.apply(fnTarget, thisArg, args);
98
+ span.setStatus({ code: SpanStatusCode.OK });
99
+ return result;
100
+ } catch (error) {
101
+ span.recordException(error as Error);
102
+ span.setStatus({
103
+ code: SpanStatusCode.ERROR,
104
+ message: error instanceof Error ? error.message : String(error),
105
+ });
106
+ throw error;
107
+ } finally {
108
+ span.end();
109
+ }
110
+ },
111
+ );
112
+ },
113
+ });
114
+ }
115
+
116
+ if (prop === 'delete' && typeof value === 'function') {
117
+ return new Proxy(value, {
118
+ apply: (fnTarget, thisArg, args) => {
119
+ const [key] = args as [string];
120
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
121
+
122
+ return tracer.startActiveSpan(
123
+ `KV ${name}: delete`,
124
+ {
125
+ kind: SpanKind.CLIENT,
126
+ attributes: {
127
+ 'db.system': 'cloudflare-kv',
128
+ 'db.operation': 'delete',
129
+ 'db.namespace': name,
130
+ 'db.key': key,
131
+ },
132
+ },
133
+ async (span) => {
134
+ try {
135
+ const result = await Reflect.apply(fnTarget, thisArg, args);
136
+ span.setStatus({ code: SpanStatusCode.OK });
137
+ return result;
138
+ } catch (error) {
139
+ span.recordException(error as Error);
140
+ span.setStatus({
141
+ code: SpanStatusCode.ERROR,
142
+ message: error instanceof Error ? error.message : String(error),
143
+ });
144
+ throw error;
145
+ } finally {
146
+ span.end();
147
+ }
148
+ },
149
+ );
150
+ },
151
+ });
152
+ }
153
+
154
+ if (prop === 'list' && typeof value === 'function') {
155
+ return new Proxy(value, {
156
+ apply: (fnTarget, thisArg, args) => {
157
+ const [options] = args as [KVNamespaceListOptions | undefined];
158
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
159
+
160
+ return tracer.startActiveSpan(
161
+ `KV ${name}: list`,
162
+ {
163
+ kind: SpanKind.CLIENT,
164
+ attributes: {
165
+ 'db.system': 'cloudflare-kv',
166
+ 'db.operation': 'list',
167
+ 'db.namespace': name,
168
+ 'db.prefix': options?.prefix || undefined,
169
+ 'db.limit': options?.limit || undefined,
170
+ },
171
+ },
172
+ async (span) => {
173
+ try {
174
+ const result = await Reflect.apply(fnTarget, thisArg, args);
175
+ span.setAttribute('db.result.keys_count', result.keys.length);
176
+ span.setStatus({ code: SpanStatusCode.OK });
177
+ return result;
178
+ } catch (error) {
179
+ span.recordException(error as Error);
180
+ span.setStatus({
181
+ code: SpanStatusCode.ERROR,
182
+ message: error instanceof Error ? error.message : String(error),
183
+ });
184
+ throw error;
185
+ } finally {
186
+ span.end();
187
+ }
188
+ },
189
+ );
190
+ },
191
+ });
192
+ }
193
+
194
+ return value;
195
+ },
196
+ };
197
+
198
+ return wrap(kv, kvHandler);
199
+ }
200
+
201
+ /**
202
+ * Instrument R2 bucket
203
+ */
204
+ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R {
205
+ const name = bucketName || 'r2';
206
+
207
+ const r2Handler: ProxyHandler<R> = {
208
+ get(target, prop) {
209
+ const value = Reflect.get(target, prop);
210
+
211
+ if (prop === 'get' && typeof value === 'function') {
212
+ return new Proxy(value, {
213
+ apply: (fnTarget, thisArg, args) => {
214
+ const [key] = args as [string, R2GetOptions | undefined];
215
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
216
+
217
+ return tracer.startActiveSpan(
218
+ `R2 ${name}: get`,
219
+ {
220
+ kind: SpanKind.CLIENT,
221
+ attributes: {
222
+ 'db.system': 'cloudflare-r2',
223
+ 'db.operation': 'get',
224
+ 'db.bucket': name,
225
+ 'db.key': key,
226
+ },
227
+ },
228
+ async (span) => {
229
+ try {
230
+ const result = await Reflect.apply(fnTarget, thisArg, args);
231
+ if (result) {
232
+ span.setAttribute('db.result.size', result.size);
233
+ span.setAttribute('db.result.etag', result.etag);
234
+ span.setAttribute('db.result.content_type', result.httpMetadata?.contentType);
235
+ } else {
236
+ span.setAttribute('db.result.exists', false);
237
+ }
238
+ span.setStatus({ code: SpanStatusCode.OK });
239
+ return result;
240
+ } catch (error) {
241
+ span.recordException(error as Error);
242
+ span.setStatus({
243
+ code: SpanStatusCode.ERROR,
244
+ message: error instanceof Error ? error.message : String(error),
245
+ });
246
+ throw error;
247
+ } finally {
248
+ span.end();
249
+ }
250
+ },
251
+ );
252
+ },
253
+ });
254
+ }
255
+
256
+ if (prop === 'put' && typeof value === 'function') {
257
+ return new Proxy(value, {
258
+ apply: (fnTarget, thisArg, args) => {
259
+ const [key] = args as [string, ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, R2PutOptions | undefined];
260
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
261
+
262
+ return tracer.startActiveSpan(
263
+ `R2 ${name}: put`,
264
+ {
265
+ kind: SpanKind.CLIENT,
266
+ attributes: {
267
+ 'db.system': 'cloudflare-r2',
268
+ 'db.operation': 'put',
269
+ 'db.bucket': name,
270
+ 'db.key': key,
271
+ },
272
+ },
273
+ async (span) => {
274
+ try {
275
+ const result = await Reflect.apply(fnTarget, thisArg, args);
276
+ span.setAttribute('db.result.etag', result.etag);
277
+ span.setAttribute('db.result.uploaded', result.uploaded);
278
+ span.setStatus({ code: SpanStatusCode.OK });
279
+ return result;
280
+ } catch (error) {
281
+ span.recordException(error as Error);
282
+ span.setStatus({
283
+ code: SpanStatusCode.ERROR,
284
+ message: error instanceof Error ? error.message : String(error),
285
+ });
286
+ throw error;
287
+ } finally {
288
+ span.end();
289
+ }
290
+ },
291
+ );
292
+ },
293
+ });
294
+ }
295
+
296
+ if (prop === 'delete' && typeof value === 'function') {
297
+ return new Proxy(value, {
298
+ apply: (fnTarget, thisArg, args) => {
299
+ const keys = args as string[];
300
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
301
+
302
+ return tracer.startActiveSpan(
303
+ `R2 ${name}: delete`,
304
+ {
305
+ kind: SpanKind.CLIENT,
306
+ attributes: {
307
+ 'db.system': 'cloudflare-r2',
308
+ 'db.operation': 'delete',
309
+ 'db.bucket': name,
310
+ 'db.keys_count': keys.length,
311
+ },
312
+ },
313
+ async (span) => {
314
+ try {
315
+ const result = await Reflect.apply(fnTarget, thisArg, args);
316
+ span.setStatus({ code: SpanStatusCode.OK });
317
+ return result;
318
+ } catch (error) {
319
+ span.recordException(error as Error);
320
+ span.setStatus({
321
+ code: SpanStatusCode.ERROR,
322
+ message: error instanceof Error ? error.message : String(error),
323
+ });
324
+ throw error;
325
+ } finally {
326
+ span.end();
327
+ }
328
+ },
329
+ );
330
+ },
331
+ });
332
+ }
333
+
334
+ if (prop === 'list' && typeof value === 'function') {
335
+ return new Proxy(value, {
336
+ apply: (fnTarget, thisArg, args) => {
337
+ const [options] = args as [R2ListOptions | undefined];
338
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
339
+
340
+ return tracer.startActiveSpan(
341
+ `R2 ${name}: list`,
342
+ {
343
+ kind: SpanKind.CLIENT,
344
+ attributes: {
345
+ 'db.system': 'cloudflare-r2',
346
+ 'db.operation': 'list',
347
+ 'db.bucket': name,
348
+ 'db.prefix': options?.prefix || undefined,
349
+ 'db.limit': options?.limit || undefined,
350
+ },
351
+ },
352
+ async (span) => {
353
+ try {
354
+ const result = await Reflect.apply(fnTarget, thisArg, args);
355
+ span.setAttribute('db.result.objects_count', result.objects.length);
356
+ span.setAttribute('db.result.truncated', result.truncated);
357
+ span.setStatus({ code: SpanStatusCode.OK });
358
+ return result;
359
+ } catch (error) {
360
+ span.recordException(error as Error);
361
+ span.setStatus({
362
+ code: SpanStatusCode.ERROR,
363
+ message: error instanceof Error ? error.message : String(error),
364
+ });
365
+ throw error;
366
+ } finally {
367
+ span.end();
368
+ }
369
+ },
370
+ );
371
+ },
372
+ });
373
+ }
374
+
375
+ return value;
376
+ },
377
+ };
378
+
379
+ return wrap(r2, r2Handler);
380
+ }
381
+
382
+ /**
383
+ * Instrument D1 database
384
+ */
385
+ export function instrumentD1<D extends D1Database>(d1: D, databaseName?: string): D {
386
+ const name = databaseName || 'd1';
387
+
388
+ const d1Handler: ProxyHandler<D> = {
389
+ get(target, prop) {
390
+ const value = Reflect.get(target, prop);
391
+
392
+ if (prop === 'prepare' && typeof value === 'function') {
393
+ return new Proxy(value, {
394
+ apply: (fnTarget, thisArg, args) => {
395
+ const [query] = args as [string];
396
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
397
+
398
+ const prepared = Reflect.apply(fnTarget, thisArg, args);
399
+
400
+ // Instrument the prepared statement
401
+ const preparedHandler: ProxyHandler<typeof prepared> = {
402
+ get(target, prop) {
403
+ const value = Reflect.get(target, prop);
404
+
405
+ if (prop === 'first' || prop === 'run' || prop === 'all' || prop === 'raw') {
406
+ return new Proxy(value, {
407
+ apply: (fnTarget, thisArg, args) => {
408
+ return tracer.startActiveSpan(
409
+ `D1 ${name}: ${prop}`,
410
+ {
411
+ kind: SpanKind.CLIENT,
412
+ attributes: {
413
+ 'db.system': 'cloudflare-d1',
414
+ 'db.operation': prop,
415
+ 'db.name': name,
416
+ 'db.statement': query,
417
+ },
418
+ },
419
+ async (span) => {
420
+ try {
421
+ const result = await Reflect.apply(fnTarget, thisArg, args);
422
+ if (prop === 'all' && Array.isArray(result)) {
423
+ span.setAttribute('db.result.rows_count', result.length);
424
+ } else if (prop === 'first' && result) {
425
+ span.setAttribute('db.result.exists', true);
426
+ }
427
+ span.setStatus({ code: SpanStatusCode.OK });
428
+ return result;
429
+ } catch (error) {
430
+ span.recordException(error as Error);
431
+ span.setStatus({
432
+ code: SpanStatusCode.ERROR,
433
+ message: error instanceof Error ? error.message : String(error),
434
+ });
435
+ throw error;
436
+ } finally {
437
+ span.end();
438
+ }
439
+ },
440
+ );
441
+ },
442
+ });
443
+ }
444
+
445
+ return value;
446
+ },
447
+ };
448
+
449
+ return wrap(prepared, preparedHandler);
450
+ },
451
+ });
452
+ }
453
+
454
+ if (prop === 'exec' && typeof value === 'function') {
455
+ return new Proxy(value, {
456
+ apply: (fnTarget, thisArg, args) => {
457
+ const [query] = args as [string];
458
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
459
+
460
+ return tracer.startActiveSpan(
461
+ `D1 ${name}: exec`,
462
+ {
463
+ kind: SpanKind.CLIENT,
464
+ attributes: {
465
+ 'db.system': 'cloudflare-d1',
466
+ 'db.operation': 'exec',
467
+ 'db.name': name,
468
+ 'db.statement': query,
469
+ },
470
+ },
471
+ async (span) => {
472
+ try {
473
+ const result = await Reflect.apply(fnTarget, thisArg, args);
474
+ span.setAttribute('db.result.count', result.count);
475
+ span.setStatus({ code: SpanStatusCode.OK });
476
+ return result;
477
+ } catch (error) {
478
+ span.recordException(error as Error);
479
+ span.setStatus({
480
+ code: SpanStatusCode.ERROR,
481
+ message: error instanceof Error ? error.message : String(error),
482
+ });
483
+ throw error;
484
+ } finally {
485
+ span.end();
486
+ }
487
+ },
488
+ );
489
+ },
490
+ });
491
+ }
492
+
493
+ return value;
494
+ },
495
+ };
496
+
497
+ return wrap(d1, d1Handler);
498
+ }
499
+
500
+ /**
501
+ * Instrument service binding (Fetcher)
502
+ */
503
+ export function instrumentServiceBinding<F extends Fetcher>(fetcher: F, serviceName?: string): F {
504
+ const name = serviceName || 'service';
505
+
506
+ const fetcherHandler: ProxyHandler<F> = {
507
+ get(target, prop) {
508
+ const value = Reflect.get(target, prop);
509
+
510
+ if (prop === 'fetch' && typeof value === 'function') {
511
+ return new Proxy(value, {
512
+ apply: (fnTarget, thisArg, args) => {
513
+ const [input, init] = args as [RequestInfo | URL, RequestInit | undefined];
514
+ const request = new Request(input, init);
515
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
516
+
517
+ return tracer.startActiveSpan(
518
+ `Service ${name}: ${request.method}`,
519
+ {
520
+ kind: SpanKind.CLIENT,
521
+ attributes: {
522
+ 'rpc.system': 'cloudflare-service-binding',
523
+ 'rpc.service': name,
524
+ 'http.request.method': request.method,
525
+ 'url.full': request.url,
526
+ },
527
+ },
528
+ async (span) => {
529
+ try {
530
+ const response = await Reflect.apply(fnTarget, thisArg, args);
531
+ span.setAttribute('http.response.status_code', response.status);
532
+ span.setStatus({ code: SpanStatusCode.OK });
533
+ return response;
534
+ } catch (error) {
535
+ span.recordException(error as Error);
536
+ span.setStatus({
537
+ code: SpanStatusCode.ERROR,
538
+ message: error instanceof Error ? error.message : String(error),
539
+ });
540
+ throw error;
541
+ } finally {
542
+ span.end();
543
+ }
544
+ },
545
+ );
546
+ },
547
+ });
548
+ }
549
+
550
+ return value;
551
+ },
552
+ };
553
+
554
+ return wrap(fetcher, fetcherHandler);
555
+ }
556
+
557
+ /**
558
+ * Auto-instrument all Cloudflare bindings in the environment
559
+ */
560
+ export function instrumentBindings(env: Record<string, any>): Record<string, any> {
561
+ const instrumented: Record<string, any> = {};
562
+
563
+ for (const [key, value] of Object.entries(env)) {
564
+ if (!value || typeof value !== 'object') {
565
+ instrumented[key] = value;
566
+ continue;
567
+ }
568
+
569
+ // Check for KV namespace
570
+ if ('get' in value && 'put' in value && 'delete' in value && 'list' in value) {
571
+ // Likely KV namespace
572
+ try {
573
+ instrumented[key] = instrumentKV(value as KVNamespace, key);
574
+ continue;
575
+ } catch {
576
+ // Not KV, continue checking
577
+ }
578
+ }
579
+
580
+ // Check for R2 bucket
581
+ if ('get' in value && 'put' in value && 'delete' in value && 'list' in value && 'head' in value) {
582
+ // Likely R2 bucket
583
+ try {
584
+ instrumented[key] = instrumentR2(value as R2Bucket, key);
585
+ continue;
586
+ } catch {
587
+ // Not R2, continue checking
588
+ }
589
+ }
590
+
591
+ // Check for D1 database
592
+ if ('prepare' in value && 'exec' in value && typeof value.prepare === 'function') {
593
+ // Likely D1 database
594
+ try {
595
+ instrumented[key] = instrumentD1(value as D1Database, key);
596
+ continue;
597
+ } catch {
598
+ // Not D1, continue checking
599
+ }
600
+ }
601
+
602
+ // Check for Service Binding (Fetcher)
603
+ if ('fetch' in value && typeof value.fetch === 'function') {
604
+ // Likely service binding
605
+ try {
606
+ instrumented[key] = instrumentServiceBinding(value as Fetcher, key);
607
+ continue;
608
+ } catch {
609
+ // Not a service binding, continue checking
610
+ }
611
+ }
612
+
613
+ // For other bindings (Events Engine, Workers AI, Vectorize, Hyperdrive),
614
+ // they don't have standard interfaces we can detect, so we pass them through
615
+ // Users can manually instrument them if needed
616
+ instrumented[key] = value;
617
+ }
618
+
619
+ return instrumented;
620
+ }
621
+
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Common instrumentation utilities
3
+ */
4
+
5
+ /**
6
+ * Promise tracker for waitUntil
7
+ */
8
+ export class PromiseTracker {
9
+ private promises: Promise<unknown>[] = [];
10
+
11
+ track(promise: Promise<unknown>): void {
12
+ this.promises.push(promise);
13
+ }
14
+
15
+ async wait(): Promise<void> {
16
+ await Promise.allSettled(this.promises);
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Proxy ExecutionContext to track waitUntil promises
22
+ */
23
+ export function proxyExecutionContext(ctx: ExecutionContext): {
24
+ ctx: ExecutionContext;
25
+ tracker: PromiseTracker;
26
+ } {
27
+ const tracker = new PromiseTracker();
28
+
29
+ const proxied = new Proxy(ctx, {
30
+ get(target, prop) {
31
+ if (prop === 'waitUntil') {
32
+ return (promise: Promise<unknown>) => {
33
+ tracker.track(promise);
34
+ return target.waitUntil(promise);
35
+ };
36
+ }
37
+ return Reflect.get(target, prop);
38
+ },
39
+ });
40
+
41
+ return { ctx: proxied, tracker };
42
+ }
43
+
44
+ /**
45
+ * Helper to wrap/unwrap proxied objects
46
+ */
47
+ const unwrapSymbol = Symbol('unwrap');
48
+
49
+ type Wrapped<T> = { [unwrapSymbol]: T } & T;
50
+
51
+ export function isWrapped<T>(item: T): item is Wrapped<T> {
52
+ return item && !!(item as Wrapped<T>)[unwrapSymbol];
53
+ }
54
+
55
+ export function unwrap<T extends object>(item: T): T {
56
+ if (item && isWrapped(item)) {
57
+ return item[unwrapSymbol];
58
+ } else {
59
+ return item;
60
+ }
61
+ }
62
+
63
+ export function wrap<T extends object>(
64
+ item: T,
65
+ handler: ProxyHandler<T>,
66
+ ): Wrapped<T> {
67
+ const proxy = new Proxy(item, handler) as Wrapped<T>;
68
+ Object.defineProperty(proxy, unwrapSymbol, {
69
+ value: item,
70
+ writable: false,
71
+ enumerable: false,
72
+ configurable: false,
73
+ });
74
+ return proxy;
75
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Bindings instrumentation for Cloudflare Workers
3
+ * Auto-instrument KV, R2, D1, Service Bindings, and more
4
+ */
5
+
6
+ export {
7
+ instrumentKV,
8
+ instrumentR2,
9
+ instrumentD1,
10
+ instrumentServiceBinding,
11
+ instrumentBindings,
12
+ } from './bindings';