autotel-cloudflare 3.0.0 → 4.0.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 (73) hide show
  1. package/dist/agents.d.ts +626 -123
  2. package/dist/agents.d.ts.map +1 -1
  3. package/dist/agents.js +260 -171
  4. package/dist/agents.js.map +1 -1
  5. package/package.json +8 -4
  6. package/src/actors/alarms.ts +0 -225
  7. package/src/actors/index.ts +0 -36
  8. package/src/actors/instrument-actor.test.ts +0 -179
  9. package/src/actors/instrument-actor.ts +0 -574
  10. package/src/actors/sockets.ts +0 -217
  11. package/src/actors/storage.ts +0 -263
  12. package/src/actors/traced-handler.ts +0 -300
  13. package/src/actors/types.ts +0 -98
  14. package/src/actors.ts +0 -50
  15. package/src/agents/index.ts +0 -42
  16. package/src/agents/otel-observability.test.ts +0 -336
  17. package/src/agents/otel-observability.ts +0 -474
  18. package/src/agents/types.ts +0 -167
  19. package/src/agents.ts +0 -76
  20. package/src/bindings/ai.test.ts +0 -170
  21. package/src/bindings/ai.ts +0 -73
  22. package/src/bindings/analytics-engine.test.ts +0 -175
  23. package/src/bindings/analytics-engine.ts +0 -78
  24. package/src/bindings/bindings-cache.test.ts +0 -80
  25. package/src/bindings/bindings-detection.test.ts +0 -235
  26. package/src/bindings/bindings-this-binding.test.ts +0 -294
  27. package/src/bindings/bindings.ts +0 -720
  28. package/src/bindings/browser-rendering.test.ts +0 -160
  29. package/src/bindings/browser-rendering.ts +0 -70
  30. package/src/bindings/common.ts +0 -84
  31. package/src/bindings/hyperdrive.test.ts +0 -176
  32. package/src/bindings/hyperdrive.ts +0 -74
  33. package/src/bindings/images.test.ts +0 -262
  34. package/src/bindings/images.ts +0 -182
  35. package/src/bindings/index.ts +0 -20
  36. package/src/bindings/queue-producer.test.ts +0 -224
  37. package/src/bindings/queue-producer.ts +0 -105
  38. package/src/bindings/rate-limiter.test.ts +0 -140
  39. package/src/bindings/rate-limiter.ts +0 -69
  40. package/src/bindings/vectorize.test.ts +0 -362
  41. package/src/bindings/vectorize.ts +0 -86
  42. package/src/bindings.ts +0 -6
  43. package/src/events.ts +0 -6
  44. package/src/execution-logger.test.ts +0 -127
  45. package/src/execution-logger.ts +0 -112
  46. package/src/global/cache.test.ts +0 -292
  47. package/src/global/cache.ts +0 -164
  48. package/src/global/fetch.test.ts +0 -399
  49. package/src/global/fetch.ts +0 -136
  50. package/src/global/index.ts +0 -7
  51. package/src/handlers/durable-objects.test.ts +0 -524
  52. package/src/handlers/durable-objects.ts +0 -250
  53. package/src/handlers/index.ts +0 -6
  54. package/src/handlers/workflows.test.ts +0 -411
  55. package/src/handlers/workflows.ts +0 -345
  56. package/src/handlers.ts +0 -6
  57. package/src/index.ts +0 -91
  58. package/src/logger.ts +0 -15
  59. package/src/native/native-tracing.test.ts +0 -54
  60. package/src/native/native-tracing.ts +0 -83
  61. package/src/native.ts +0 -12
  62. package/src/parse-error.ts +0 -1
  63. package/src/sampling.ts +0 -6
  64. package/src/testing.ts +0 -6
  65. package/src/wrappers/cf-attributes.test.ts +0 -334
  66. package/src/wrappers/define-worker-fetch.test.ts +0 -102
  67. package/src/wrappers/define-worker-fetch.ts +0 -71
  68. package/src/wrappers/index.ts +0 -13
  69. package/src/wrappers/instrument.integration.test.ts +0 -578
  70. package/src/wrappers/instrument.ts +0 -806
  71. package/src/wrappers/native-instrument.test.ts +0 -153
  72. package/src/wrappers/wrap-do.ts +0 -34
  73. package/src/wrappers/wrap-module.ts +0 -37
@@ -1,720 +0,0 @@
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, getActiveConfig } from 'autotel-edge';
26
- import { wrap, isWrapped } from './common';
27
- import { instrumentAI } from './ai';
28
- import { instrumentVectorize } from './vectorize';
29
- import { instrumentHyperdrive } from './hyperdrive';
30
- import { instrumentQueueProducer } from './queue-producer';
31
- import { instrumentAnalyticsEngine } from './analytics-engine';
32
- import { instrumentImages } from './images';
33
-
34
- type DbStatementCapture = 'off' | 'obfuscated' | 'full';
35
-
36
- /**
37
- * Sanitize a SQL statement based on the capture mode.
38
- * - 'full': returns the statement as-is
39
- * - 'obfuscated': replaces string literals and numbers with '?'
40
- * - 'off': returns undefined (attribute not set)
41
- */
42
- function sanitizeStatement(query: string, mode: DbStatementCapture): string | undefined {
43
- if (mode === 'off') return undefined;
44
- if (mode === 'obfuscated') return query.replaceAll(/'[^']*'/g, "'?'").replaceAll(/\b\d+\b/g, '?');
45
- return query;
46
- }
47
-
48
- /**
49
- * Instrument KV namespace
50
- */
51
- export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: string): K {
52
- const name = namespaceName || 'kv';
53
-
54
- const kvHandler: ProxyHandler<K> = {
55
- get(target, prop) {
56
- const value = Reflect.get(target, prop);
57
-
58
- if (prop === 'get' && typeof value === 'function') {
59
- return new Proxy(value, {
60
- apply: (fnTarget, _thisArg, args) => {
61
- const [key, options] = args as [string, KVNamespaceGetOptions<unknown> | undefined];
62
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
63
-
64
- return tracer.startActiveSpan(
65
- `KV ${name}: get`,
66
- {
67
- kind: SpanKind.CLIENT,
68
- attributes: {
69
- 'db.system': 'cloudflare-kv',
70
- 'db.operation': 'get',
71
- 'db.namespace': name,
72
- 'db.key': key,
73
- 'db.cache_hit': options?.cacheTtl !== undefined,
74
- },
75
- },
76
- async (span) => {
77
- try {
78
- const result = await Reflect.apply(fnTarget, target, args);
79
- span.setAttribute('db.result.type', result === null ? 'null' : typeof result);
80
- span.setStatus({ code: SpanStatusCode.OK });
81
- return result;
82
- } catch (error) {
83
- span.recordException(error as Error);
84
- span.setStatus({
85
- code: SpanStatusCode.ERROR,
86
- message: error instanceof Error ? error.message : String(error),
87
- });
88
- throw error;
89
- } finally {
90
- span.end();
91
- }
92
- },
93
- );
94
- },
95
- });
96
- }
97
-
98
- if (prop === 'put' && typeof value === 'function') {
99
- return new Proxy(value, {
100
- apply: (fnTarget, _thisArg, args) => {
101
- const [key] = args as [string, unknown, KVNamespacePutOptions | undefined];
102
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
103
-
104
- return tracer.startActiveSpan(
105
- `KV ${name}: put`,
106
- {
107
- kind: SpanKind.CLIENT,
108
- attributes: {
109
- 'db.system': 'cloudflare-kv',
110
- 'db.operation': 'put',
111
- 'db.namespace': name,
112
- 'db.key': key,
113
- },
114
- },
115
- async (span) => {
116
- try {
117
- const result = await Reflect.apply(fnTarget, target, args);
118
- span.setStatus({ code: SpanStatusCode.OK });
119
- return result;
120
- } catch (error) {
121
- span.recordException(error as Error);
122
- span.setStatus({
123
- code: SpanStatusCode.ERROR,
124
- message: error instanceof Error ? error.message : String(error),
125
- });
126
- throw error;
127
- } finally {
128
- span.end();
129
- }
130
- },
131
- );
132
- },
133
- });
134
- }
135
-
136
- if (prop === 'delete' && typeof value === 'function') {
137
- return new Proxy(value, {
138
- apply: (fnTarget, _thisArg, args) => {
139
- const [key] = args as [string];
140
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
141
-
142
- return tracer.startActiveSpan(
143
- `KV ${name}: delete`,
144
- {
145
- kind: SpanKind.CLIENT,
146
- attributes: {
147
- 'db.system': 'cloudflare-kv',
148
- 'db.operation': 'delete',
149
- 'db.namespace': name,
150
- 'db.key': key,
151
- },
152
- },
153
- async (span) => {
154
- try {
155
- const result = await Reflect.apply(fnTarget, target, args);
156
- span.setStatus({ code: SpanStatusCode.OK });
157
- return result;
158
- } catch (error) {
159
- span.recordException(error as Error);
160
- span.setStatus({
161
- code: SpanStatusCode.ERROR,
162
- message: error instanceof Error ? error.message : String(error),
163
- });
164
- throw error;
165
- } finally {
166
- span.end();
167
- }
168
- },
169
- );
170
- },
171
- });
172
- }
173
-
174
- if (prop === 'list' && typeof value === 'function') {
175
- return new Proxy(value, {
176
- apply: (fnTarget, _thisArg, args) => {
177
- const [options] = args as [KVNamespaceListOptions | undefined];
178
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
179
-
180
- return tracer.startActiveSpan(
181
- `KV ${name}: list`,
182
- {
183
- kind: SpanKind.CLIENT,
184
- attributes: {
185
- 'db.system': 'cloudflare-kv',
186
- 'db.operation': 'list',
187
- 'db.namespace': name,
188
- 'db.prefix': options?.prefix || undefined,
189
- 'db.limit': options?.limit || undefined,
190
- },
191
- },
192
- async (span) => {
193
- try {
194
- const result = await Reflect.apply(fnTarget, target, args);
195
- span.setAttribute('db.result.keys_count', result.keys.length);
196
- span.setStatus({ code: SpanStatusCode.OK });
197
- return result;
198
- } catch (error) {
199
- span.recordException(error as Error);
200
- span.setStatus({
201
- code: SpanStatusCode.ERROR,
202
- message: error instanceof Error ? error.message : String(error),
203
- });
204
- throw error;
205
- } finally {
206
- span.end();
207
- }
208
- },
209
- );
210
- },
211
- });
212
- }
213
-
214
- return value;
215
- },
216
- };
217
-
218
- return wrap(kv, kvHandler);
219
- }
220
-
221
- /**
222
- * Instrument R2 bucket
223
- */
224
- export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R {
225
- const name = bucketName || 'r2';
226
-
227
- const r2Handler: ProxyHandler<R> = {
228
- get(target, prop) {
229
- const value = Reflect.get(target, prop);
230
-
231
- if (prop === 'get' && typeof value === 'function') {
232
- return new Proxy(value, {
233
- apply: (fnTarget, _thisArg, args) => {
234
- const [key] = args as [string, R2GetOptions | undefined];
235
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
236
-
237
- return tracer.startActiveSpan(
238
- `R2 ${name}: get`,
239
- {
240
- kind: SpanKind.CLIENT,
241
- attributes: {
242
- 'db.system': 'cloudflare-r2',
243
- 'db.operation': 'get',
244
- 'db.bucket': name,
245
- 'db.key': key,
246
- },
247
- },
248
- async (span) => {
249
- try {
250
- const result = await Reflect.apply(fnTarget, target, args);
251
- if (result) {
252
- span.setAttribute('db.result.size', result.size);
253
- span.setAttribute('db.result.etag', result.etag);
254
- span.setAttribute('db.result.content_type', result.httpMetadata?.contentType);
255
- } else {
256
- span.setAttribute('db.result.exists', false);
257
- }
258
- span.setStatus({ code: SpanStatusCode.OK });
259
- return result;
260
- } catch (error) {
261
- span.recordException(error as Error);
262
- span.setStatus({
263
- code: SpanStatusCode.ERROR,
264
- message: error instanceof Error ? error.message : String(error),
265
- });
266
- throw error;
267
- } finally {
268
- span.end();
269
- }
270
- },
271
- );
272
- },
273
- });
274
- }
275
-
276
- if (prop === 'put' && typeof value === 'function') {
277
- return new Proxy(value, {
278
- apply: (fnTarget, _thisArg, args) => {
279
- const [key] = args as [string, ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, R2PutOptions | undefined];
280
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
281
-
282
- return tracer.startActiveSpan(
283
- `R2 ${name}: put`,
284
- {
285
- kind: SpanKind.CLIENT,
286
- attributes: {
287
- 'db.system': 'cloudflare-r2',
288
- 'db.operation': 'put',
289
- 'db.bucket': name,
290
- 'db.key': key,
291
- },
292
- },
293
- async (span) => {
294
- try {
295
- const result = await Reflect.apply(fnTarget, target, args);
296
- span.setAttribute('db.result.etag', result.etag);
297
- span.setAttribute('db.result.uploaded', result.uploaded);
298
- span.setStatus({ code: SpanStatusCode.OK });
299
- return result;
300
- } catch (error) {
301
- span.recordException(error as Error);
302
- span.setStatus({
303
- code: SpanStatusCode.ERROR,
304
- message: error instanceof Error ? error.message : String(error),
305
- });
306
- throw error;
307
- } finally {
308
- span.end();
309
- }
310
- },
311
- );
312
- },
313
- });
314
- }
315
-
316
- if (prop === 'delete' && typeof value === 'function') {
317
- return new Proxy(value, {
318
- apply: (fnTarget, _thisArg, args) => {
319
- const keys = args as string[];
320
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
321
-
322
- return tracer.startActiveSpan(
323
- `R2 ${name}: delete`,
324
- {
325
- kind: SpanKind.CLIENT,
326
- attributes: {
327
- 'db.system': 'cloudflare-r2',
328
- 'db.operation': 'delete',
329
- 'db.bucket': name,
330
- 'db.keys_count': keys.length,
331
- },
332
- },
333
- async (span) => {
334
- try {
335
- const result = await Reflect.apply(fnTarget, target, args);
336
- span.setStatus({ code: SpanStatusCode.OK });
337
- return result;
338
- } catch (error) {
339
- span.recordException(error as Error);
340
- span.setStatus({
341
- code: SpanStatusCode.ERROR,
342
- message: error instanceof Error ? error.message : String(error),
343
- });
344
- throw error;
345
- } finally {
346
- span.end();
347
- }
348
- },
349
- );
350
- },
351
- });
352
- }
353
-
354
- if (prop === 'list' && typeof value === 'function') {
355
- return new Proxy(value, {
356
- apply: (fnTarget, _thisArg, args) => {
357
- const [options] = args as [R2ListOptions | undefined];
358
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
359
-
360
- return tracer.startActiveSpan(
361
- `R2 ${name}: list`,
362
- {
363
- kind: SpanKind.CLIENT,
364
- attributes: {
365
- 'db.system': 'cloudflare-r2',
366
- 'db.operation': 'list',
367
- 'db.bucket': name,
368
- 'db.prefix': options?.prefix || undefined,
369
- 'db.limit': options?.limit || undefined,
370
- },
371
- },
372
- async (span) => {
373
- try {
374
- const result = await Reflect.apply(fnTarget, target, args);
375
- span.setAttribute('db.result.objects_count', result.objects.length);
376
- span.setAttribute('db.result.truncated', result.truncated);
377
- span.setStatus({ code: SpanStatusCode.OK });
378
- return result;
379
- } catch (error) {
380
- span.recordException(error as Error);
381
- span.setStatus({
382
- code: SpanStatusCode.ERROR,
383
- message: error instanceof Error ? error.message : String(error),
384
- });
385
- throw error;
386
- } finally {
387
- span.end();
388
- }
389
- },
390
- );
391
- },
392
- });
393
- }
394
-
395
- return value;
396
- },
397
- };
398
-
399
- return wrap(r2, r2Handler);
400
- }
401
-
402
- /**
403
- * Instrument D1 database
404
- */
405
- export function instrumentD1<D extends D1Database>(d1: D, databaseName?: string): D {
406
- const name = databaseName || 'd1';
407
-
408
- const d1Handler: ProxyHandler<D> = {
409
- get(target, prop) {
410
- const value = Reflect.get(target, prop);
411
-
412
- if (prop === 'prepare' && typeof value === 'function') {
413
- return new Proxy(value, {
414
- apply: (fnTarget, _thisArg, args) => {
415
- const [query] = args as [string];
416
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
417
-
418
- const prepared = Reflect.apply(fnTarget, target, args);
419
-
420
- // Instrument the prepared statement
421
- const preparedHandler: ProxyHandler<typeof prepared> = {
422
- get(target, prop) {
423
- const value = Reflect.get(target, prop);
424
-
425
- if (prop === 'first' || prop === 'run' || prop === 'all' || prop === 'raw') {
426
- return new Proxy(value, {
427
- apply: (fnTarget, _thisArg, args) => {
428
- const activeConfig = getActiveConfig();
429
- const captureMode: DbStatementCapture = activeConfig?.dataSafety?.captureDbStatement ?? 'full';
430
- const statement = sanitizeStatement(query, captureMode);
431
- const attributes: Record<string, any> = {
432
- 'db.system': 'cloudflare-d1',
433
- 'db.operation': prop,
434
- 'db.name': name,
435
- };
436
- if (statement !== undefined) {
437
- attributes['db.statement'] = statement;
438
- }
439
- return tracer.startActiveSpan(
440
- `D1 ${name}: ${prop}`,
441
- {
442
- kind: SpanKind.CLIENT,
443
- attributes,
444
- },
445
- async (span) => {
446
- try {
447
- const result = await Reflect.apply(fnTarget, target, args);
448
- if (prop === 'all' && Array.isArray(result)) {
449
- span.setAttribute('db.result.rows_count', result.length);
450
- } else if (prop === 'first' && result) {
451
- span.setAttribute('db.result.exists', true);
452
- }
453
- span.setStatus({ code: SpanStatusCode.OK });
454
- return result;
455
- } catch (error) {
456
- span.recordException(error as Error);
457
- span.setStatus({
458
- code: SpanStatusCode.ERROR,
459
- message: error instanceof Error ? error.message : String(error),
460
- });
461
- throw error;
462
- } finally {
463
- span.end();
464
- }
465
- },
466
- );
467
- },
468
- });
469
- }
470
-
471
- return value;
472
- },
473
- };
474
-
475
- return wrap(prepared, preparedHandler);
476
- },
477
- });
478
- }
479
-
480
- if (prop === 'exec' && typeof value === 'function') {
481
- return new Proxy(value, {
482
- apply: (fnTarget, _thisArg, args) => {
483
- const [query] = args as [string];
484
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
485
- const activeConfig = getActiveConfig();
486
- const captureMode: DbStatementCapture = activeConfig?.dataSafety?.captureDbStatement ?? 'full';
487
- const statement = sanitizeStatement(query, captureMode);
488
- const attributes: Record<string, any> = {
489
- 'db.system': 'cloudflare-d1',
490
- 'db.operation': 'exec',
491
- 'db.name': name,
492
- };
493
- if (statement !== undefined) {
494
- attributes['db.statement'] = statement;
495
- }
496
-
497
- return tracer.startActiveSpan(
498
- `D1 ${name}: exec`,
499
- {
500
- kind: SpanKind.CLIENT,
501
- attributes,
502
- },
503
- async (span) => {
504
- try {
505
- const result = await Reflect.apply(fnTarget, target, args);
506
- span.setAttribute('db.result.count', result.count);
507
- span.setStatus({ code: SpanStatusCode.OK });
508
- return result;
509
- } catch (error) {
510
- span.recordException(error as Error);
511
- span.setStatus({
512
- code: SpanStatusCode.ERROR,
513
- message: error instanceof Error ? error.message : String(error),
514
- });
515
- throw error;
516
- } finally {
517
- span.end();
518
- }
519
- },
520
- );
521
- },
522
- });
523
- }
524
-
525
- return value;
526
- },
527
- };
528
-
529
- return wrap(d1, d1Handler);
530
- }
531
-
532
- /**
533
- * Instrument service binding (Fetcher)
534
- *
535
- * Unlike other bindings, Fetcher objects are native Cloudflare C++ bindings
536
- * whose methods throw "Illegal invocation" when called through a Proxy with
537
- * a different `this` reference. We work around this by calling `target.fetch()`
538
- * directly on the original binding instead of using `Reflect.apply` on a
539
- * detached function reference.
540
- */
541
- export function instrumentServiceBinding<F extends Fetcher>(fetcher: F, serviceName?: string): F {
542
- const name = serviceName || 'service';
543
-
544
- const fetcherHandler: ProxyHandler<F> = {
545
- get(target, prop) {
546
- if (prop === 'fetch' && typeof target.fetch === 'function') {
547
- // Return a plain function wrapper instead of proxying the native method.
548
- // This avoids detaching the native method from its binding, which would
549
- // cause "Illegal invocation" on Cloudflare's native Fetcher objects.
550
- const tracedFetch = (...args: any[]) => {
551
- const [input, init] = args as [RequestInfo | URL, RequestInit | undefined];
552
- const request = new Request(input, init);
553
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
554
-
555
- return tracer.startActiveSpan(
556
- `Service ${name}: ${request.method}`,
557
- {
558
- kind: SpanKind.CLIENT,
559
- attributes: {
560
- 'rpc.system': 'cloudflare-service-binding',
561
- 'rpc.service': name,
562
- 'http.request.method': request.method,
563
- 'url.full': request.url,
564
- },
565
- },
566
- async (span) => {
567
- try {
568
- // Call fetch directly on the original target to preserve
569
- // the native `this` binding that Cloudflare requires
570
- const response = await target.fetch(input, init as RequestInit);
571
- span.setAttribute('http.response.status_code', response.status);
572
- span.setStatus({ code: SpanStatusCode.OK });
573
- return response;
574
- } catch (error) {
575
- span.recordException(error as Error);
576
- span.setStatus({
577
- code: SpanStatusCode.ERROR,
578
- message: error instanceof Error ? error.message : String(error),
579
- });
580
- throw error;
581
- } finally {
582
- span.end();
583
- }
584
- },
585
- );
586
- };
587
- return tracedFetch;
588
- }
589
-
590
- // For non-fetch properties, access the original target directly
591
- // to avoid Proxy-related issues with native bindings
592
- const value = Reflect.get(target, prop);
593
- if (typeof value === 'function') {
594
- // Bind native methods to the original target to prevent
595
- // "Illegal invocation" errors
596
- return value.bind(target);
597
- }
598
- return value;
599
- },
600
- };
601
-
602
- return wrap(fetcher, fetcherHandler);
603
- }
604
-
605
- /**
606
- * Detection helpers
607
- */
608
- const hasMethod = (obj: any, m: string): boolean =>
609
- typeof obj?.[m] === 'function';
610
-
611
- const hasExactMethods = (obj: any, methods: string[]): boolean =>
612
- methods.every(m => hasMethod(obj, m));
613
-
614
- /**
615
- * Auto-instrument all Cloudflare bindings in the environment
616
- *
617
- * Detection order (most specific first):
618
- * 1. R2 — get, put, delete, list, head
619
- * 2. KV — get, put, delete, list (not head)
620
- * 3. D1 — prepare, exec
621
- * 4. Vectorize — query, insert, upsert, describe
622
- * 5. AI — run + (gateway or models discriminator)
623
- * 6. Hyperdrive — connect + connectionString + host
624
- * 7. Queue Producer — send, sendBatch (not get)
625
- * 8. Analytics Engine — writeDataPoint
626
- * 9. Images — info, input
627
- * 10. Service Binding — fetch (broadest, must be last)
628
- *
629
- * Not auto-detected (manual only):
630
- * - Rate Limiter — limit() alone too generic
631
- * - Browser Rendering — indistinguishable from Service Binding
632
- */
633
- const envCache = new WeakMap<object, Record<string, any>>();
634
-
635
- export function instrumentBindings(env: Record<string, any>): Record<string, any> {
636
- const cached = envCache.get(env);
637
- if (cached) return cached;
638
-
639
- const instrumented: Record<string, any> = {};
640
-
641
- for (const [key, value] of Object.entries(env)) {
642
- if (!value || typeof value !== 'object') {
643
- instrumented[key] = value;
644
- continue;
645
- }
646
-
647
- // Skip already-instrumented bindings
648
- if (isWrapped(value)) {
649
- instrumented[key] = value;
650
- continue;
651
- }
652
-
653
- // 1. R2 — most specific (has head)
654
- if (hasExactMethods(value, ['get', 'put', 'delete', 'list', 'head'])) {
655
- instrumented[key] = instrumentR2(value as R2Bucket, key);
656
- continue;
657
- }
658
-
659
- // 2. KV — like R2 but without head
660
- if (hasExactMethods(value, ['get', 'put', 'delete', 'list']) && !('head' in value)) {
661
- instrumented[key] = instrumentKV(value as KVNamespace, key);
662
- continue;
663
- }
664
-
665
- // 3. D1
666
- if (hasExactMethods(value, ['prepare', 'exec'])) {
667
- instrumented[key] = instrumentD1(value as D1Database, key);
668
- continue;
669
- }
670
-
671
- // 4. Vectorize
672
- if (hasExactMethods(value, ['query', 'insert', 'upsert', 'describe'])) {
673
- instrumented[key] = instrumentVectorize(value as VectorizeIndex, key);
674
- continue;
675
- }
676
-
677
- // 5. AI — has run() + discriminator properties
678
- if (hasMethod(value, 'run') && ('gateway' in value || 'models' in value)) {
679
- instrumented[key] = instrumentAI(value as Ai, key);
680
- continue;
681
- }
682
-
683
- // 6. Hyperdrive — connect + connection properties
684
- if (hasMethod(value, 'connect') && 'connectionString' in value && 'host' in value) {
685
- instrumented[key] = instrumentHyperdrive(value as Hyperdrive, key);
686
- continue;
687
- }
688
-
689
- // 7. Queue Producer — send + sendBatch (not get, to avoid KV collision)
690
- if (hasExactMethods(value, ['send', 'sendBatch']) && !('get' in value)) {
691
- instrumented[key] = instrumentQueueProducer(value as Queue, key);
692
- continue;
693
- }
694
-
695
- // 8. Analytics Engine
696
- if (hasMethod(value, 'writeDataPoint')) {
697
- instrumented[key] = instrumentAnalyticsEngine(value as AnalyticsEngineDataset, key);
698
- continue;
699
- }
700
-
701
- // 9. Images
702
- if (hasExactMethods(value, ['info', 'input'])) {
703
- instrumented[key] = instrumentImages(value as any, key);
704
- continue;
705
- }
706
-
707
- // 10. Service Binding (broadest — must be last)
708
- if (hasMethod(value, 'fetch')) {
709
- instrumented[key] = instrumentServiceBinding(value as Fetcher, key);
710
- continue;
711
- }
712
-
713
- // Unknown binding type — pass through
714
- instrumented[key] = value;
715
- }
716
-
717
- envCache.set(env, instrumented);
718
- return instrumented;
719
- }
720
-