autotel-cloudflare 3.1.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 (76) hide show
  1. package/dist/agents.d.ts.map +1 -1
  2. package/dist/agents.js +7 -0
  3. package/dist/agents.js.map +1 -1
  4. package/package.json +8 -4
  5. package/src/actors/alarms.ts +0 -225
  6. package/src/actors/index.ts +0 -36
  7. package/src/actors/instrument-actor.test.ts +0 -179
  8. package/src/actors/instrument-actor.ts +0 -574
  9. package/src/actors/sockets.ts +0 -217
  10. package/src/actors/storage.ts +0 -263
  11. package/src/actors/traced-handler.ts +0 -300
  12. package/src/actors/types.ts +0 -98
  13. package/src/actors.ts +0 -50
  14. package/src/agents/agent.ts +0 -327
  15. package/src/agents/base.ts +0 -26
  16. package/src/agents/index.ts +0 -46
  17. package/src/agents/mcp.ts +0 -38
  18. package/src/agents/observability.ts +0 -145
  19. package/src/agents/otel-observability.test.ts +0 -269
  20. package/src/agents/otel-observability.ts +0 -557
  21. package/src/agents/types.ts +0 -61
  22. package/src/agents.ts +0 -87
  23. package/src/bindings/ai.test.ts +0 -170
  24. package/src/bindings/ai.ts +0 -73
  25. package/src/bindings/analytics-engine.test.ts +0 -175
  26. package/src/bindings/analytics-engine.ts +0 -78
  27. package/src/bindings/bindings-cache.test.ts +0 -80
  28. package/src/bindings/bindings-detection.test.ts +0 -235
  29. package/src/bindings/bindings-this-binding.test.ts +0 -294
  30. package/src/bindings/bindings.ts +0 -720
  31. package/src/bindings/browser-rendering.test.ts +0 -160
  32. package/src/bindings/browser-rendering.ts +0 -70
  33. package/src/bindings/common.ts +0 -84
  34. package/src/bindings/hyperdrive.test.ts +0 -176
  35. package/src/bindings/hyperdrive.ts +0 -74
  36. package/src/bindings/images.test.ts +0 -262
  37. package/src/bindings/images.ts +0 -182
  38. package/src/bindings/index.ts +0 -20
  39. package/src/bindings/queue-producer.test.ts +0 -224
  40. package/src/bindings/queue-producer.ts +0 -105
  41. package/src/bindings/rate-limiter.test.ts +0 -140
  42. package/src/bindings/rate-limiter.ts +0 -69
  43. package/src/bindings/vectorize.test.ts +0 -362
  44. package/src/bindings/vectorize.ts +0 -86
  45. package/src/bindings.ts +0 -6
  46. package/src/events.ts +0 -6
  47. package/src/execution-logger.test.ts +0 -127
  48. package/src/execution-logger.ts +0 -112
  49. package/src/global/cache.test.ts +0 -292
  50. package/src/global/cache.ts +0 -164
  51. package/src/global/fetch.test.ts +0 -399
  52. package/src/global/fetch.ts +0 -136
  53. package/src/global/index.ts +0 -7
  54. package/src/handlers/durable-objects.test.ts +0 -524
  55. package/src/handlers/durable-objects.ts +0 -250
  56. package/src/handlers/index.ts +0 -6
  57. package/src/handlers/workflows.test.ts +0 -411
  58. package/src/handlers/workflows.ts +0 -345
  59. package/src/handlers.ts +0 -6
  60. package/src/index.ts +0 -91
  61. package/src/logger.ts +0 -15
  62. package/src/native/native-tracing.test.ts +0 -54
  63. package/src/native/native-tracing.ts +0 -83
  64. package/src/native.ts +0 -12
  65. package/src/parse-error.ts +0 -1
  66. package/src/sampling.ts +0 -6
  67. package/src/testing.ts +0 -6
  68. package/src/wrappers/cf-attributes.test.ts +0 -334
  69. package/src/wrappers/define-worker-fetch.test.ts +0 -102
  70. package/src/wrappers/define-worker-fetch.ts +0 -71
  71. package/src/wrappers/index.ts +0 -13
  72. package/src/wrappers/instrument.integration.test.ts +0 -578
  73. package/src/wrappers/instrument.ts +0 -806
  74. package/src/wrappers/native-instrument.test.ts +0 -153
  75. package/src/wrappers/wrap-do.ts +0 -34
  76. 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
-