autotel-cloudflare 2.15.0 → 2.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.
@@ -22,7 +22,7 @@ import {
22
22
  SpanKind,
23
23
  SpanStatusCode,
24
24
  } from '@opentelemetry/api';
25
- import { WorkerTracer } from 'autotel-edge';
25
+ import { WorkerTracer, getActiveConfig } from 'autotel-edge';
26
26
  import { wrap, isWrapped } from './common';
27
27
  import { instrumentAI } from './ai';
28
28
  import { instrumentVectorize } from './vectorize';
@@ -31,6 +31,20 @@ import { instrumentQueueProducer } from './queue-producer';
31
31
  import { instrumentAnalyticsEngine } from './analytics-engine';
32
32
  import { instrumentImages } from './images';
33
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
+
34
48
  /**
35
49
  * Instrument KV namespace
36
50
  */
@@ -43,10 +57,10 @@ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: strin
43
57
 
44
58
  if (prop === 'get' && typeof value === 'function') {
45
59
  return new Proxy(value, {
46
- apply: (fnTarget, thisArg, args) => {
60
+ apply: (fnTarget, _thisArg, args) => {
47
61
  const [key, options] = args as [string, KVNamespaceGetOptions<unknown> | undefined];
48
62
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
49
-
63
+
50
64
  return tracer.startActiveSpan(
51
65
  `KV ${name}: get`,
52
66
  {
@@ -61,7 +75,7 @@ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: strin
61
75
  },
62
76
  async (span) => {
63
77
  try {
64
- const result = await Reflect.apply(fnTarget, thisArg, args);
78
+ const result = await Reflect.apply(fnTarget, target, args);
65
79
  span.setAttribute('db.result.type', result === null ? 'null' : typeof result);
66
80
  span.setStatus({ code: SpanStatusCode.OK });
67
81
  return result;
@@ -83,10 +97,10 @@ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: strin
83
97
 
84
98
  if (prop === 'put' && typeof value === 'function') {
85
99
  return new Proxy(value, {
86
- apply: (fnTarget, thisArg, args) => {
100
+ apply: (fnTarget, _thisArg, args) => {
87
101
  const [key] = args as [string, unknown, KVNamespacePutOptions | undefined];
88
102
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
89
-
103
+
90
104
  return tracer.startActiveSpan(
91
105
  `KV ${name}: put`,
92
106
  {
@@ -100,7 +114,7 @@ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: strin
100
114
  },
101
115
  async (span) => {
102
116
  try {
103
- const result = await Reflect.apply(fnTarget, thisArg, args);
117
+ const result = await Reflect.apply(fnTarget, target, args);
104
118
  span.setStatus({ code: SpanStatusCode.OK });
105
119
  return result;
106
120
  } catch (error) {
@@ -121,10 +135,10 @@ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: strin
121
135
 
122
136
  if (prop === 'delete' && typeof value === 'function') {
123
137
  return new Proxy(value, {
124
- apply: (fnTarget, thisArg, args) => {
138
+ apply: (fnTarget, _thisArg, args) => {
125
139
  const [key] = args as [string];
126
140
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
127
-
141
+
128
142
  return tracer.startActiveSpan(
129
143
  `KV ${name}: delete`,
130
144
  {
@@ -138,7 +152,7 @@ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: strin
138
152
  },
139
153
  async (span) => {
140
154
  try {
141
- const result = await Reflect.apply(fnTarget, thisArg, args);
155
+ const result = await Reflect.apply(fnTarget, target, args);
142
156
  span.setStatus({ code: SpanStatusCode.OK });
143
157
  return result;
144
158
  } catch (error) {
@@ -159,10 +173,10 @@ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: strin
159
173
 
160
174
  if (prop === 'list' && typeof value === 'function') {
161
175
  return new Proxy(value, {
162
- apply: (fnTarget, thisArg, args) => {
176
+ apply: (fnTarget, _thisArg, args) => {
163
177
  const [options] = args as [KVNamespaceListOptions | undefined];
164
178
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
165
-
179
+
166
180
  return tracer.startActiveSpan(
167
181
  `KV ${name}: list`,
168
182
  {
@@ -177,7 +191,7 @@ export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: strin
177
191
  },
178
192
  async (span) => {
179
193
  try {
180
- const result = await Reflect.apply(fnTarget, thisArg, args);
194
+ const result = await Reflect.apply(fnTarget, target, args);
181
195
  span.setAttribute('db.result.keys_count', result.keys.length);
182
196
  span.setStatus({ code: SpanStatusCode.OK });
183
197
  return result;
@@ -216,10 +230,10 @@ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R
216
230
 
217
231
  if (prop === 'get' && typeof value === 'function') {
218
232
  return new Proxy(value, {
219
- apply: (fnTarget, thisArg, args) => {
233
+ apply: (fnTarget, _thisArg, args) => {
220
234
  const [key] = args as [string, R2GetOptions | undefined];
221
235
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
222
-
236
+
223
237
  return tracer.startActiveSpan(
224
238
  `R2 ${name}: get`,
225
239
  {
@@ -233,7 +247,7 @@ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R
233
247
  },
234
248
  async (span) => {
235
249
  try {
236
- const result = await Reflect.apply(fnTarget, thisArg, args);
250
+ const result = await Reflect.apply(fnTarget, target, args);
237
251
  if (result) {
238
252
  span.setAttribute('db.result.size', result.size);
239
253
  span.setAttribute('db.result.etag', result.etag);
@@ -261,10 +275,10 @@ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R
261
275
 
262
276
  if (prop === 'put' && typeof value === 'function') {
263
277
  return new Proxy(value, {
264
- apply: (fnTarget, thisArg, args) => {
278
+ apply: (fnTarget, _thisArg, args) => {
265
279
  const [key] = args as [string, ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, R2PutOptions | undefined];
266
280
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
267
-
281
+
268
282
  return tracer.startActiveSpan(
269
283
  `R2 ${name}: put`,
270
284
  {
@@ -278,7 +292,7 @@ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R
278
292
  },
279
293
  async (span) => {
280
294
  try {
281
- const result = await Reflect.apply(fnTarget, thisArg, args);
295
+ const result = await Reflect.apply(fnTarget, target, args);
282
296
  span.setAttribute('db.result.etag', result.etag);
283
297
  span.setAttribute('db.result.uploaded', result.uploaded);
284
298
  span.setStatus({ code: SpanStatusCode.OK });
@@ -301,10 +315,10 @@ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R
301
315
 
302
316
  if (prop === 'delete' && typeof value === 'function') {
303
317
  return new Proxy(value, {
304
- apply: (fnTarget, thisArg, args) => {
318
+ apply: (fnTarget, _thisArg, args) => {
305
319
  const keys = args as string[];
306
320
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
307
-
321
+
308
322
  return tracer.startActiveSpan(
309
323
  `R2 ${name}: delete`,
310
324
  {
@@ -318,7 +332,7 @@ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R
318
332
  },
319
333
  async (span) => {
320
334
  try {
321
- const result = await Reflect.apply(fnTarget, thisArg, args);
335
+ const result = await Reflect.apply(fnTarget, target, args);
322
336
  span.setStatus({ code: SpanStatusCode.OK });
323
337
  return result;
324
338
  } catch (error) {
@@ -339,10 +353,10 @@ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R
339
353
 
340
354
  if (prop === 'list' && typeof value === 'function') {
341
355
  return new Proxy(value, {
342
- apply: (fnTarget, thisArg, args) => {
356
+ apply: (fnTarget, _thisArg, args) => {
343
357
  const [options] = args as [R2ListOptions | undefined];
344
358
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
345
-
359
+
346
360
  return tracer.startActiveSpan(
347
361
  `R2 ${name}: list`,
348
362
  {
@@ -357,7 +371,7 @@ export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R
357
371
  },
358
372
  async (span) => {
359
373
  try {
360
- const result = await Reflect.apply(fnTarget, thisArg, args);
374
+ const result = await Reflect.apply(fnTarget, target, args);
361
375
  span.setAttribute('db.result.objects_count', result.objects.length);
362
376
  span.setAttribute('db.result.truncated', result.truncated);
363
377
  span.setStatus({ code: SpanStatusCode.OK });
@@ -397,11 +411,11 @@ export function instrumentD1<D extends D1Database>(d1: D, databaseName?: string)
397
411
 
398
412
  if (prop === 'prepare' && typeof value === 'function') {
399
413
  return new Proxy(value, {
400
- apply: (fnTarget, thisArg, args) => {
414
+ apply: (fnTarget, _thisArg, args) => {
401
415
  const [query] = args as [string];
402
416
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
403
-
404
- const prepared = Reflect.apply(fnTarget, thisArg, args);
417
+
418
+ const prepared = Reflect.apply(fnTarget, target, args);
405
419
 
406
420
  // Instrument the prepared statement
407
421
  const preparedHandler: ProxyHandler<typeof prepared> = {
@@ -410,21 +424,27 @@ export function instrumentD1<D extends D1Database>(d1: D, databaseName?: string)
410
424
 
411
425
  if (prop === 'first' || prop === 'run' || prop === 'all' || prop === 'raw') {
412
426
  return new Proxy(value, {
413
- apply: (fnTarget, thisArg, args) => {
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
+ }
414
439
  return tracer.startActiveSpan(
415
440
  `D1 ${name}: ${prop}`,
416
441
  {
417
442
  kind: SpanKind.CLIENT,
418
- attributes: {
419
- 'db.system': 'cloudflare-d1',
420
- 'db.operation': prop,
421
- 'db.name': name,
422
- 'db.statement': query,
423
- },
443
+ attributes,
424
444
  },
425
445
  async (span) => {
426
446
  try {
427
- const result = await Reflect.apply(fnTarget, thisArg, args);
447
+ const result = await Reflect.apply(fnTarget, target, args);
428
448
  if (prop === 'all' && Array.isArray(result)) {
429
449
  span.setAttribute('db.result.rows_count', result.length);
430
450
  } else if (prop === 'first' && result) {
@@ -459,24 +479,30 @@ export function instrumentD1<D extends D1Database>(d1: D, databaseName?: string)
459
479
 
460
480
  if (prop === 'exec' && typeof value === 'function') {
461
481
  return new Proxy(value, {
462
- apply: (fnTarget, thisArg, args) => {
482
+ apply: (fnTarget, _thisArg, args) => {
463
483
  const [query] = args as [string];
464
484
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
465
-
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
+
466
497
  return tracer.startActiveSpan(
467
498
  `D1 ${name}: exec`,
468
499
  {
469
500
  kind: SpanKind.CLIENT,
470
- attributes: {
471
- 'db.system': 'cloudflare-d1',
472
- 'db.operation': 'exec',
473
- 'db.name': name,
474
- 'db.statement': query,
475
- },
501
+ attributes,
476
502
  },
477
503
  async (span) => {
478
504
  try {
479
- const result = await Reflect.apply(fnTarget, thisArg, args);
505
+ const result = await Reflect.apply(fnTarget, target, args);
480
506
  span.setAttribute('db.result.count', result.count);
481
507
  span.setStatus({ code: SpanStatusCode.OK });
482
508
  return result;
@@ -515,11 +541,11 @@ export function instrumentServiceBinding<F extends Fetcher>(fetcher: F, serviceN
515
541
 
516
542
  if (prop === 'fetch' && typeof value === 'function') {
517
543
  return new Proxy(value, {
518
- apply: (fnTarget, thisArg, args) => {
544
+ apply: (fnTarget, _thisArg, args) => {
519
545
  const [input, init] = args as [RequestInfo | URL, RequestInit | undefined];
520
546
  const request = new Request(input, init);
521
547
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
522
-
548
+
523
549
  return tracer.startActiveSpan(
524
550
  `Service ${name}: ${request.method}`,
525
551
  {
@@ -533,7 +559,7 @@ export function instrumentServiceBinding<F extends Fetcher>(fetcher: F, serviceN
533
559
  },
534
560
  async (span) => {
535
561
  try {
536
- const response = await Reflect.apply(fnTarget, thisArg, args);
562
+ const response = await Reflect.apply(fnTarget, target, args);
537
563
  span.setAttribute('http.response.status_code', response.status);
538
564
  span.setStatus({ code: SpanStatusCode.OK });
539
565
  return response;
@@ -588,7 +614,12 @@ const hasExactMethods = (obj: any, methods: string[]): boolean =>
588
614
  * - Rate Limiter — limit() alone too generic
589
615
  * - Browser Rendering — indistinguishable from Service Binding
590
616
  */
617
+ const envCache = new WeakMap<object, Record<string, any>>();
618
+
591
619
  export function instrumentBindings(env: Record<string, any>): Record<string, any> {
620
+ const cached = envCache.get(env);
621
+ if (cached) return cached;
622
+
592
623
  const instrumented: Record<string, any> = {};
593
624
 
594
625
  for (const [key, value] of Object.entries(env)) {
@@ -667,6 +698,7 @@ export function instrumentBindings(env: Record<string, any>): Record<string, any
667
698
  instrumented[key] = value;
668
699
  }
669
700
 
701
+ envCache.set(env, instrumented);
670
702
  return instrumented;
671
703
  }
672
704
 
@@ -129,6 +129,22 @@ describe('Browser Rendering Instrumentation', () => {
129
129
  });
130
130
  });
131
131
 
132
+ describe('this-binding', () => {
133
+ it('should invoke fetch() with original object as this, not the proxy', async () => {
134
+ let receivedThis: any;
135
+ const mockBrowserObj = {
136
+ fetch: vi.fn(async function(this: any) {
137
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
138
+ receivedThis = this;
139
+ return new Response('ok', { status: 200 });
140
+ }),
141
+ };
142
+ const instrumented = instrumentBrowserRendering(mockBrowserObj, 'test');
143
+ await instrumented.fetch('https://example.com');
144
+ expect(receivedThis).toBe(mockBrowserObj);
145
+ });
146
+ });
147
+
132
148
  describe('Non-instrumented methods', () => {
133
149
  it('should pass through non-instrumented methods unchanged', () => {
134
150
  const instrumented = instrumentBrowserRendering(mockBrowser, 'my-browser');
@@ -26,7 +26,7 @@ export function instrumentBrowserRendering<T extends BrowserRenderingLike>(brows
26
26
 
27
27
  if (prop === 'fetch' && typeof value === 'function') {
28
28
  return new Proxy(value, {
29
- apply: (fnTarget, thisArg, args) => {
29
+ apply: (fnTarget, _thisArg, args) => {
30
30
  const [input] = args as [RequestInfo | URL, RequestInit | undefined];
31
31
  const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
32
32
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
@@ -42,7 +42,7 @@ export function instrumentBrowserRendering<T extends BrowserRenderingLike>(brows
42
42
  },
43
43
  async (span) => {
44
44
  try {
45
- const result = await Reflect.apply(fnTarget, thisArg, args);
45
+ const result = await Reflect.apply(fnTarget, target, args);
46
46
  setAttr(span, 'http.response.status_code', result?.status);
47
47
  span.setStatus({ code: SpanStatusCode.OK });
48
48
  return result;
@@ -113,6 +113,28 @@ describe('Hyperdrive Binding Instrumentation', () => {
113
113
  });
114
114
  });
115
115
 
116
+ describe('this-binding', () => {
117
+ it('should invoke connect() with original object as this, not the proxy', async () => {
118
+ let receivedThis: any;
119
+ const mockHyperdrive = {
120
+ connect: vi.fn(async function(this: any) {
121
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
122
+ receivedThis = this;
123
+ return {} as Socket;
124
+ }),
125
+ connectionString: 'postgresql://user:pass@host:5432/db',
126
+ host: 'host',
127
+ port: 5432,
128
+ user: 'user',
129
+ password: 'pass',
130
+ database: 'db',
131
+ } as unknown as Hyperdrive;
132
+ const instrumented = instrumentHyperdrive(mockHyperdrive, 'test');
133
+ await instrumented.connect();
134
+ expect(receivedThis).toBe(mockHyperdrive);
135
+ });
136
+ });
137
+
116
138
  describe('non-instrumented properties', () => {
117
139
  it('should pass through non-instrumented properties', () => {
118
140
  const mockHyperdrive = createMockHyperdrive();
@@ -22,7 +22,7 @@ export function instrumentHyperdrive<T extends Hyperdrive>(hyperdrive: T, bindin
22
22
 
23
23
  if (prop === 'connect' && typeof value === 'function') {
24
24
  return new Proxy(value, {
25
- apply: (fnTarget, thisArg, args) => {
25
+ apply: (fnTarget, _thisArg, args) => {
26
26
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
27
27
 
28
28
  const attributes: Record<string, string | number> = {
@@ -47,7 +47,7 @@ export function instrumentHyperdrive<T extends Hyperdrive>(hyperdrive: T, bindin
47
47
  },
48
48
  async (span) => {
49
49
  try {
50
- const result = await Reflect.apply(fnTarget, thisArg, args);
50
+ const result = await Reflect.apply(fnTarget, target, args);
51
51
  span.setStatus({ code: SpanStatusCode.OK });
52
52
  return result;
53
53
  } catch (error) {
@@ -206,6 +206,39 @@ describe('Images Binding Instrumentation', () => {
206
206
  });
207
207
  });
208
208
 
209
+ describe('this-binding', () => {
210
+ it('should invoke info() with original object as this, not the proxy', async () => {
211
+ let receivedThis: any;
212
+ const mockImagesObj = {
213
+ info: vi.fn(async function(this: any) {
214
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
215
+ receivedThis = this;
216
+ return { width: 800, height: 600, format: 'png' };
217
+ }),
218
+ input: vi.fn(() => ({ transform: vi.fn(), draw: vi.fn(), output: vi.fn() })),
219
+ };
220
+ const instrumented = instrumentImages(mockImagesObj as any, 'test');
221
+ await instrumented.info(new ArrayBuffer(8));
222
+ expect(receivedThis).toBe(mockImagesObj);
223
+ });
224
+
225
+ it('should invoke input() with original object as this, not the proxy', () => {
226
+ let receivedThis: any;
227
+ const mockTransformer = { transform: vi.fn(), draw: vi.fn(), output: vi.fn(async () => ({})) };
228
+ const mockImagesObj = {
229
+ info: vi.fn(async () => ({ width: 800, height: 600, format: 'png' })),
230
+ input: vi.fn(function(this: any) {
231
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
232
+ receivedThis = this;
233
+ return mockTransformer;
234
+ }),
235
+ };
236
+ const instrumented = instrumentImages(mockImagesObj as any, 'test');
237
+ instrumented.input(new ArrayBuffer(8));
238
+ expect(receivedThis).toBe(mockImagesObj);
239
+ });
240
+ });
241
+
209
242
  describe('non-instrumented methods', () => {
210
243
  it('should pass through non-instrumented methods unchanged', () => {
211
244
  const { images } = createMockImages();
@@ -128,7 +128,7 @@ export function instrumentImages<T extends ImagesLike>(images: T, bindingName?:
128
128
 
129
129
  if (prop === 'info' && typeof value === 'function') {
130
130
  return new Proxy(value, {
131
- apply: (fnTarget, thisArg, args) => {
131
+ apply: (fnTarget, _thisArg, args) => {
132
132
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
133
133
 
134
134
  return tracer.startActiveSpan(
@@ -142,7 +142,7 @@ export function instrumentImages<T extends ImagesLike>(images: T, bindingName?:
142
142
  },
143
143
  async (span) => {
144
144
  try {
145
- const result = await Reflect.apply(fnTarget, thisArg, args);
145
+ const result = await Reflect.apply(fnTarget, target, args);
146
146
  setAttr(span, 'images.width', result?.width);
147
147
  setAttr(span, 'images.height', result?.height);
148
148
  setAttr(span, 'images.format', result?.format);
@@ -166,8 +166,8 @@ export function instrumentImages<T extends ImagesLike>(images: T, bindingName?:
166
166
 
167
167
  if (prop === 'input' && typeof value === 'function') {
168
168
  return new Proxy(value, {
169
- apply: (fnTarget, thisArg, args) => {
170
- const transformer = Reflect.apply(fnTarget, thisArg, args) as ImageTransformerLike;
169
+ apply: (fnTarget, _thisArg, args) => {
170
+ const transformer = Reflect.apply(fnTarget, target, args) as ImageTransformerLike;
171
171
  const meta: PipelineMeta = { operationCount: 0 };
172
172
  return proxyTransformer(transformer, meta, name);
173
173
  },
@@ -179,6 +179,38 @@ describe('Queue Producer Binding Instrumentation', () => {
179
179
  });
180
180
  });
181
181
 
182
+ describe('this-binding', () => {
183
+ it('should invoke send() with original object as this, not the proxy', async () => {
184
+ let receivedThis: any;
185
+ const mockQueue = {
186
+ send: vi.fn(async function(this: any) {
187
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
188
+ receivedThis = this;
189
+ return { messageId: 'msg-123' };
190
+ }),
191
+ sendBatch: vi.fn(async () => ({})),
192
+ } as unknown as Queue;
193
+ const instrumented = instrumentQueueProducer(mockQueue, 'test');
194
+ await instrumented.send({ data: 'test' });
195
+ expect(receivedThis).toBe(mockQueue);
196
+ });
197
+
198
+ it('should invoke sendBatch() with original object as this, not the proxy', async () => {
199
+ let receivedThis: any;
200
+ const mockQueue = {
201
+ send: vi.fn(async () => ({ messageId: 'msg-123' })),
202
+ sendBatch: vi.fn(async function(this: any) {
203
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
204
+ receivedThis = this;
205
+ return {};
206
+ }),
207
+ } as unknown as Queue;
208
+ const instrumented = instrumentQueueProducer(mockQueue, 'test');
209
+ await instrumented.sendBatch([{ body: { data: 'test' } }]);
210
+ expect(receivedThis).toBe(mockQueue);
211
+ });
212
+ });
213
+
182
214
  describe('non-instrumented methods', () => {
183
215
  it('should pass through non-instrumented properties', () => {
184
216
  const mockQueue = createMockQueue();
@@ -22,7 +22,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, queueName?: s
22
22
 
23
23
  if (prop === 'send' && typeof value === 'function') {
24
24
  return new Proxy(value, {
25
- apply: (fnTarget, thisArg, args) => {
25
+ apply: (fnTarget, _thisArg, args) => {
26
26
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
27
27
 
28
28
  return tracer.startActiveSpan(
@@ -38,7 +38,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, queueName?: s
38
38
  },
39
39
  async (span) => {
40
40
  try {
41
- const result = await Reflect.apply(fnTarget, thisArg, args);
41
+ const result = await Reflect.apply(fnTarget, target, args);
42
42
  setAttr(span, 'messaging.message.id', (result as any)?.messageId);
43
43
  span.setStatus({ code: SpanStatusCode.OK });
44
44
  return result;
@@ -60,7 +60,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, queueName?: s
60
60
 
61
61
  if (prop === 'sendBatch' && typeof value === 'function') {
62
62
  return new Proxy(value, {
63
- apply: (fnTarget, thisArg, args) => {
63
+ apply: (fnTarget, _thisArg, args) => {
64
64
  const [messages] = args as [{ body: unknown }[]];
65
65
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
66
66
 
@@ -78,7 +78,7 @@ export function instrumentQueueProducer<T extends Queue>(queue: T, queueName?: s
78
78
  },
79
79
  async (span) => {
80
80
  try {
81
- const result = await Reflect.apply(fnTarget, thisArg, args);
81
+ const result = await Reflect.apply(fnTarget, target, args);
82
82
  span.setStatus({ code: SpanStatusCode.OK });
83
83
  return result;
84
84
  } catch (error) {
@@ -109,6 +109,22 @@ describe('Rate Limiter Instrumentation', () => {
109
109
  });
110
110
  });
111
111
 
112
+ describe('this-binding', () => {
113
+ it('should invoke limit() with original object as this, not the proxy', async () => {
114
+ let receivedThis: any;
115
+ const mockLim = {
116
+ limit: vi.fn(async function(this: any) {
117
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
118
+ receivedThis = this;
119
+ return { success: true };
120
+ }),
121
+ };
122
+ const instrumented = instrumentRateLimiter(mockLim, 'test');
123
+ await instrumented.limit({ key: 'user-123' });
124
+ expect(receivedThis).toBe(mockLim);
125
+ });
126
+ });
127
+
112
128
  describe('Non-instrumented methods', () => {
113
129
  it('should pass through non-instrumented methods unchanged', () => {
114
130
  const instrumented = instrumentRateLimiter(mockLimiter, 'my-limiter');
@@ -26,7 +26,7 @@ export function instrumentRateLimiter<T extends RateLimiterLike>(limiter: T, bin
26
26
 
27
27
  if (prop === 'limit' && typeof value === 'function') {
28
28
  return new Proxy(value, {
29
- apply: (fnTarget, thisArg, args) => {
29
+ apply: (fnTarget, _thisArg, args) => {
30
30
  const [options] = args as [{ key: string }];
31
31
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
32
32
 
@@ -41,7 +41,7 @@ export function instrumentRateLimiter<T extends RateLimiterLike>(limiter: T, bin
41
41
  },
42
42
  async (span) => {
43
43
  try {
44
- const result = await Reflect.apply(fnTarget, thisArg, args);
44
+ const result = await Reflect.apply(fnTarget, target, args);
45
45
  setAttr(span, 'rate_limiter.success', result?.success);
46
46
  span.setStatus({ code: SpanStatusCode.OK });
47
47
  return result;
@@ -205,6 +205,28 @@ describe('Vectorize Binding Instrumentation', () => {
205
205
  });
206
206
  });
207
207
 
208
+ describe('this-binding', () => {
209
+ it('should invoke methods with original object as this, not the proxy', async () => {
210
+ let receivedThis: any;
211
+ const mockVec = {
212
+ query: vi.fn(async function(this: any) {
213
+ // eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
214
+ receivedThis = this;
215
+ return { matches: [], count: 0 };
216
+ }),
217
+ insert: vi.fn(async () => ({ mutationId: 'mut-1', count: 0 })),
218
+ upsert: vi.fn(async () => ({ mutationId: 'mut-2', count: 0 })),
219
+ deleteByIds: vi.fn(async () => ({ mutationId: 'mut-3', count: 0 })),
220
+ getByIds: vi.fn(async () => []),
221
+ describe: vi.fn(async () => ({ dimensions: 128, vectorCount: 0, processedUpTo: '' })),
222
+ } as unknown as VectorizeIndex;
223
+
224
+ const instrumented = instrumentVectorize(mockVec, 'test');
225
+ await instrumented.query([0.1, 0.2] as any, {} as any);
226
+ expect(receivedThis).toBe(mockVec);
227
+ });
228
+ });
229
+
208
230
  describe('Error handling', () => {
209
231
  it('should record exception and set error status on query() failure', async () => {
210
232
  mockVectorize.query = vi.fn(async () => {
@@ -24,7 +24,7 @@ export function instrumentVectorize<T extends VectorizeIndex>(vectorize: T, inde
24
24
 
25
25
  if (typeof prop === 'string' && TRACED_METHODS.includes(prop as any) && typeof value === 'function') {
26
26
  return new Proxy(value, {
27
- apply: (fnTarget, thisArg, args) => {
27
+ apply: (fnTarget, _thisArg, args) => {
28
28
  const operation = prop as string;
29
29
  const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
30
30
 
@@ -54,7 +54,7 @@ export function instrumentVectorize<T extends VectorizeIndex>(vectorize: T, inde
54
54
  },
55
55
  async (span) => {
56
56
  try {
57
- const result = await Reflect.apply(fnTarget, thisArg, args);
57
+ const result = await Reflect.apply(fnTarget, target, args);
58
58
 
59
59
  if (operation === 'query' && result?.matches) {
60
60
  setAttr(span, 'db.vectorize.matches_count', result.matches.length);