autotel-cloudflare 2.11.0 → 2.13.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.
- package/dist/actors.js +1 -1
- package/dist/bindings.d.ts +113 -1
- package/dist/bindings.js +2 -2
- package/dist/chunk-4UG2QCPQ.js +1060 -0
- package/dist/chunk-4UG2QCPQ.js.map +1 -0
- package/dist/{chunk-5NL62W4L.js → chunk-O4IYKWPJ.js} +8 -3
- package/dist/chunk-O4IYKWPJ.js.map +1 -0
- package/dist/{chunk-ADWSZ5GY.js → chunk-ZJPX4N7S.js} +3 -3
- package/dist/{chunk-ADWSZ5GY.js.map → chunk-ZJPX4N7S.js.map} +1 -1
- package/dist/handlers.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +34 -6
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/bindings/ai.test.ts +156 -0
- package/src/bindings/ai.ts +71 -0
- package/src/bindings/analytics-engine.test.ts +160 -0
- package/src/bindings/analytics-engine.ts +78 -0
- package/src/bindings/bindings-detection.test.ts +235 -0
- package/src/bindings/bindings.ts +98 -47
- package/src/bindings/browser-rendering.test.ts +144 -0
- package/src/bindings/browser-rendering.ts +70 -0
- package/src/bindings/common.ts +9 -0
- package/src/bindings/hyperdrive.test.ts +154 -0
- package/src/bindings/hyperdrive.ts +74 -0
- package/src/bindings/images.test.ts +229 -0
- package/src/bindings/images.ts +182 -0
- package/src/bindings/index.ts +8 -0
- package/src/bindings/queue-producer.test.ts +192 -0
- package/src/bindings/queue-producer.ts +105 -0
- package/src/bindings/rate-limiter.test.ts +124 -0
- package/src/bindings/rate-limiter.ts +69 -0
- package/src/bindings/vectorize.test.ts +340 -0
- package/src/bindings/vectorize.ts +86 -0
- package/src/index.ts +8 -0
- package/src/wrappers/cf-attributes.test.ts +275 -0
- package/src/wrappers/instrument.ts +38 -0
- package/dist/chunk-5NL62W4L.js.map +0 -1
- package/dist/chunk-UPQE3J4I.js +0 -520
- package/dist/chunk-UPQE3J4I.js.map +0 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { instrumentAnalyticsEngine } from './analytics-engine';
|
|
3
|
+
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
describe('Analytics Engine Instrumentation', () => {
|
|
6
|
+
let mockTracer: any;
|
|
7
|
+
let mockSpan: any;
|
|
8
|
+
let getTracerSpy: any;
|
|
9
|
+
let mockAE: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockSpan = {
|
|
13
|
+
spanContext: () => ({
|
|
14
|
+
traceId: 'test-trace-id',
|
|
15
|
+
spanId: 'test-span-id',
|
|
16
|
+
traceFlags: 1,
|
|
17
|
+
}),
|
|
18
|
+
setAttribute: vi.fn(),
|
|
19
|
+
setAttributes: vi.fn(),
|
|
20
|
+
setStatus: vi.fn(),
|
|
21
|
+
recordException: vi.fn(),
|
|
22
|
+
end: vi.fn(),
|
|
23
|
+
isRecording: () => true,
|
|
24
|
+
updateName: vi.fn(),
|
|
25
|
+
addEvent: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
mockTracer = {
|
|
29
|
+
startActiveSpan: vi.fn((name, options, fn) => {
|
|
30
|
+
return fn(mockSpan);
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
|
|
35
|
+
|
|
36
|
+
mockAE = {
|
|
37
|
+
writeDataPoint: vi.fn(),
|
|
38
|
+
someOtherMethod: vi.fn(() => 'passthrough-value'),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
getTracerSpy.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('writeDataPoint()', () => {
|
|
47
|
+
it('should create span with correct attributes', () => {
|
|
48
|
+
const instrumented = instrumentAnalyticsEngine(mockAE, 'my-dataset');
|
|
49
|
+
|
|
50
|
+
instrumented.writeDataPoint({
|
|
51
|
+
indexes: ['idx1'],
|
|
52
|
+
doubles: [1, 2],
|
|
53
|
+
blobs: ['blob1'],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
57
|
+
|
|
58
|
+
const [spanName, spanOptions] = mockTracer.startActiveSpan.mock.calls[0];
|
|
59
|
+
expect(spanName).toBe('AnalyticsEngine my-dataset: writeDataPoint');
|
|
60
|
+
expect(spanOptions.kind).toBe(SpanKind.CLIENT);
|
|
61
|
+
expect(spanOptions.attributes['analytics.system']).toBe('cloudflare-analytics-engine');
|
|
62
|
+
expect(spanOptions.attributes['analytics.operation']).toBe('writeDataPoint');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should record indexes_count, doubles_count, blobs_count', () => {
|
|
66
|
+
const instrumented = instrumentAnalyticsEngine(mockAE, 'my-dataset');
|
|
67
|
+
|
|
68
|
+
instrumented.writeDataPoint({
|
|
69
|
+
indexes: ['idx1', 'idx2'],
|
|
70
|
+
doubles: [1, 2, 3],
|
|
71
|
+
blobs: ['blob1'],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const spanOptions = mockTracer.startActiveSpan.mock.calls[0][1];
|
|
75
|
+
expect(spanOptions.attributes['analytics.indexes_count']).toBe(2);
|
|
76
|
+
expect(spanOptions.attributes['analytics.doubles_count']).toBe(3);
|
|
77
|
+
expect(spanOptions.attributes['analytics.blobs_count']).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle a single index (non-array) by recording indexes_count as 1', () => {
|
|
81
|
+
const instrumented = instrumentAnalyticsEngine(mockAE, 'my-dataset');
|
|
82
|
+
|
|
83
|
+
instrumented.writeDataPoint({
|
|
84
|
+
indexes: 'single-index' as any,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const spanOptions = mockTracer.startActiveSpan.mock.calls[0][1];
|
|
88
|
+
expect(spanOptions.attributes['analytics.indexes_count']).toBe(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should work with no datapoint argument', () => {
|
|
92
|
+
const instrumented = instrumentAnalyticsEngine(mockAE);
|
|
93
|
+
|
|
94
|
+
instrumented.writeDataPoint(undefined as any);
|
|
95
|
+
|
|
96
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
97
|
+
|
|
98
|
+
const spanOptions = mockTracer.startActiveSpan.mock.calls[0][1];
|
|
99
|
+
expect(spanOptions.attributes['analytics.system']).toBe('cloudflare-analytics-engine');
|
|
100
|
+
expect(spanOptions.attributes['analytics.operation']).toBe('writeDataPoint');
|
|
101
|
+
// No count attributes should be set when no datapoint
|
|
102
|
+
expect(spanOptions.attributes['analytics.indexes_count']).toBeUndefined();
|
|
103
|
+
expect(spanOptions.attributes['analytics.doubles_count']).toBeUndefined();
|
|
104
|
+
expect(spanOptions.attributes['analytics.blobs_count']).toBeUndefined();
|
|
105
|
+
|
|
106
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
107
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle errors by recording exception and rethrowing', () => {
|
|
111
|
+
const error = new Error('writeDataPoint failed');
|
|
112
|
+
mockAE.writeDataPoint = vi.fn(() => {
|
|
113
|
+
throw error;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const instrumented = instrumentAnalyticsEngine(mockAE, 'my-dataset');
|
|
117
|
+
|
|
118
|
+
expect(() => instrumented.writeDataPoint({ indexes: ['idx1'] })).toThrow('writeDataPoint failed');
|
|
119
|
+
|
|
120
|
+
expect(mockSpan.recordException).toHaveBeenCalledWith(error);
|
|
121
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
122
|
+
code: SpanStatusCode.ERROR,
|
|
123
|
+
message: 'writeDataPoint failed',
|
|
124
|
+
});
|
|
125
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should use default dataset name when none provided', () => {
|
|
129
|
+
const instrumented = instrumentAnalyticsEngine(mockAE);
|
|
130
|
+
|
|
131
|
+
instrumented.writeDataPoint({ indexes: ['idx1'] });
|
|
132
|
+
|
|
133
|
+
const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
|
|
134
|
+
expect(spanName).toBe('AnalyticsEngine analytics-engine: writeDataPoint');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should set OK status and end span on success', () => {
|
|
138
|
+
const instrumented = instrumentAnalyticsEngine(mockAE, 'my-dataset');
|
|
139
|
+
|
|
140
|
+
instrumented.writeDataPoint({ doubles: [42] });
|
|
141
|
+
|
|
142
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
143
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
144
|
+
expect(mockAE.writeDataPoint).toHaveBeenCalledWith({ doubles: [42] });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Non-instrumented methods', () => {
|
|
149
|
+
it('should pass through non-instrumented methods unchanged', () => {
|
|
150
|
+
const instrumented = instrumentAnalyticsEngine(mockAE, 'my-dataset');
|
|
151
|
+
|
|
152
|
+
const result = instrumented.someOtherMethod();
|
|
153
|
+
|
|
154
|
+
expect(result).toBe('passthrough-value');
|
|
155
|
+
expect(mockAE.someOtherMethod).toHaveBeenCalled();
|
|
156
|
+
// No span should be created for non-instrumented methods
|
|
157
|
+
expect(mockTracer.startActiveSpan).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Engine binding instrumentation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
trace,
|
|
7
|
+
SpanKind,
|
|
8
|
+
SpanStatusCode,
|
|
9
|
+
} from '@opentelemetry/api';
|
|
10
|
+
import type { WorkerTracer } from 'autotel-edge';
|
|
11
|
+
import { wrap } from './common';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Instrument Analytics Engine binding
|
|
15
|
+
*/
|
|
16
|
+
export function instrumentAnalyticsEngine<T extends AnalyticsEngineDataset>(ae: T, datasetName?: string): T {
|
|
17
|
+
const name = datasetName || 'analytics-engine';
|
|
18
|
+
|
|
19
|
+
const handler: ProxyHandler<T> = {
|
|
20
|
+
get(target, prop) {
|
|
21
|
+
const value = Reflect.get(target, prop);
|
|
22
|
+
|
|
23
|
+
if (prop === 'writeDataPoint' && typeof value === 'function') {
|
|
24
|
+
return new Proxy(value, {
|
|
25
|
+
apply: (fnTarget, thisArg, args) => {
|
|
26
|
+
const [dataPoint] = args as [AnalyticsEngineDataPoint | undefined];
|
|
27
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
28
|
+
|
|
29
|
+
const attributes: Record<string, string | number> = {
|
|
30
|
+
'analytics.system': 'cloudflare-analytics-engine',
|
|
31
|
+
'analytics.operation': 'writeDataPoint',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (dataPoint) {
|
|
35
|
+
if (dataPoint.indexes) {
|
|
36
|
+
attributes['analytics.indexes_count'] = Array.isArray(dataPoint.indexes) ? dataPoint.indexes.length : 1;
|
|
37
|
+
}
|
|
38
|
+
if (dataPoint.doubles) {
|
|
39
|
+
attributes['analytics.doubles_count'] = dataPoint.doubles.length;
|
|
40
|
+
}
|
|
41
|
+
if (dataPoint.blobs) {
|
|
42
|
+
attributes['analytics.blobs_count'] = dataPoint.blobs.length;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return tracer.startActiveSpan(
|
|
47
|
+
`AnalyticsEngine ${name}: writeDataPoint`,
|
|
48
|
+
{
|
|
49
|
+
kind: SpanKind.CLIENT,
|
|
50
|
+
attributes,
|
|
51
|
+
},
|
|
52
|
+
(span) => {
|
|
53
|
+
try {
|
|
54
|
+
// writeDataPoint is synchronous/void
|
|
55
|
+
Reflect.apply(fnTarget, thisArg, args);
|
|
56
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
span.recordException(error as Error);
|
|
59
|
+
span.setStatus({
|
|
60
|
+
code: SpanStatusCode.ERROR,
|
|
61
|
+
message: error instanceof Error ? error.message : String(error),
|
|
62
|
+
});
|
|
63
|
+
throw error;
|
|
64
|
+
} finally {
|
|
65
|
+
span.end();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return value;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return wrap(ae, handler);
|
|
78
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
|
|
3
|
+
import { instrumentBindings } from './bindings';
|
|
4
|
+
import { isWrapped, wrap } from './common';
|
|
5
|
+
|
|
6
|
+
describe('instrumentBindings() detection logic', () => {
|
|
7
|
+
let mockTracer: any;
|
|
8
|
+
let mockSpan: any;
|
|
9
|
+
let getTracerSpy: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockSpan = {
|
|
13
|
+
spanContext: () => ({
|
|
14
|
+
traceId: 'test-trace-id',
|
|
15
|
+
spanId: 'test-span-id',
|
|
16
|
+
traceFlags: 1,
|
|
17
|
+
}),
|
|
18
|
+
setAttribute: vi.fn(),
|
|
19
|
+
setAttributes: vi.fn(),
|
|
20
|
+
setStatus: vi.fn(),
|
|
21
|
+
recordException: vi.fn(),
|
|
22
|
+
end: vi.fn(),
|
|
23
|
+
isRecording: () => true,
|
|
24
|
+
updateName: vi.fn(),
|
|
25
|
+
addEvent: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
mockTracer = {
|
|
29
|
+
startActiveSpan: vi.fn((name, options, fn) => {
|
|
30
|
+
return fn(mockSpan);
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
getTracerSpy.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should detect R2 (object with get, put, delete, list, head methods)', () => {
|
|
42
|
+
const mockR2 = {
|
|
43
|
+
get: vi.fn(),
|
|
44
|
+
put: vi.fn(),
|
|
45
|
+
delete: vi.fn(),
|
|
46
|
+
list: vi.fn(),
|
|
47
|
+
head: vi.fn(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const result = instrumentBindings({ MY_R2: mockR2 });
|
|
51
|
+
|
|
52
|
+
expect(result.MY_R2).not.toBe(mockR2);
|
|
53
|
+
expect(isWrapped(result.MY_R2)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should detect KV (object with get, put, delete, list but NOT head)', () => {
|
|
57
|
+
const mockKV = {
|
|
58
|
+
get: vi.fn(),
|
|
59
|
+
put: vi.fn(),
|
|
60
|
+
delete: vi.fn(),
|
|
61
|
+
list: vi.fn(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const result = instrumentBindings({ MY_KV: mockKV });
|
|
65
|
+
|
|
66
|
+
expect(result.MY_KV).not.toBe(mockKV);
|
|
67
|
+
expect(isWrapped(result.MY_KV)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should detect D1 (object with prepare, exec methods)', () => {
|
|
71
|
+
const mockD1 = {
|
|
72
|
+
prepare: vi.fn(),
|
|
73
|
+
exec: vi.fn(),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const result = instrumentBindings({ MY_DB: mockD1 });
|
|
77
|
+
|
|
78
|
+
expect(result.MY_DB).not.toBe(mockD1);
|
|
79
|
+
expect(isWrapped(result.MY_DB)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should detect Vectorize (object with query, insert, upsert, describe methods)', () => {
|
|
83
|
+
const mockVectorize = {
|
|
84
|
+
query: vi.fn(),
|
|
85
|
+
insert: vi.fn(),
|
|
86
|
+
upsert: vi.fn(),
|
|
87
|
+
describe: vi.fn(),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const result = instrumentBindings({ MY_INDEX: mockVectorize });
|
|
91
|
+
|
|
92
|
+
expect(result.MY_INDEX).not.toBe(mockVectorize);
|
|
93
|
+
expect(isWrapped(result.MY_INDEX)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should detect AI (object with run method AND gateway property)', () => {
|
|
97
|
+
const mockAI = {
|
|
98
|
+
run: vi.fn(),
|
|
99
|
+
gateway: {},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = instrumentBindings({ AI: mockAI });
|
|
103
|
+
|
|
104
|
+
expect(result.AI).not.toBe(mockAI);
|
|
105
|
+
expect(isWrapped(result.AI)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should detect Hyperdrive (object with connect method AND connectionString, host properties)', () => {
|
|
109
|
+
const mockHyperdrive = {
|
|
110
|
+
connect: vi.fn(),
|
|
111
|
+
connectionString: 'postgresql://user:pass@host:5432/db',
|
|
112
|
+
host: 'db.example.com',
|
|
113
|
+
port: 5432,
|
|
114
|
+
user: 'user',
|
|
115
|
+
password: 'pass',
|
|
116
|
+
database: 'db',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const result = instrumentBindings({ HYPERDRIVE: mockHyperdrive });
|
|
120
|
+
|
|
121
|
+
expect(result.HYPERDRIVE).not.toBe(mockHyperdrive);
|
|
122
|
+
expect(isWrapped(result.HYPERDRIVE)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should detect Queue Producer (object with send, sendBatch but NOT get)', () => {
|
|
126
|
+
const mockQueue = {
|
|
127
|
+
send: vi.fn(),
|
|
128
|
+
sendBatch: vi.fn(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = instrumentBindings({ MY_QUEUE: mockQueue });
|
|
132
|
+
|
|
133
|
+
expect(result.MY_QUEUE).not.toBe(mockQueue);
|
|
134
|
+
expect(isWrapped(result.MY_QUEUE)).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should detect Analytics Engine (object with writeDataPoint method)', () => {
|
|
138
|
+
const mockAE = {
|
|
139
|
+
writeDataPoint: vi.fn(),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = instrumentBindings({ ANALYTICS: mockAE });
|
|
143
|
+
|
|
144
|
+
expect(result.ANALYTICS).not.toBe(mockAE);
|
|
145
|
+
expect(isWrapped(result.ANALYTICS)).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should detect Images (object with info, input methods)', () => {
|
|
149
|
+
const mockImages = {
|
|
150
|
+
info: vi.fn(),
|
|
151
|
+
input: vi.fn(),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = instrumentBindings({ IMAGES: mockImages });
|
|
155
|
+
|
|
156
|
+
expect(result.IMAGES).not.toBe(mockImages);
|
|
157
|
+
expect(isWrapped(result.IMAGES)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should detect Service Binding (object with fetch method) - last', () => {
|
|
161
|
+
const mockService = {
|
|
162
|
+
fetch: vi.fn(),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = instrumentBindings({ MY_SERVICE: mockService });
|
|
166
|
+
|
|
167
|
+
expect(result.MY_SERVICE).not.toBe(mockService);
|
|
168
|
+
expect(isWrapped(result.MY_SERVICE)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should detect R2 before KV (object with head gets R2, without head gets KV)', () => {
|
|
172
|
+
const withHead = {
|
|
173
|
+
get: vi.fn(),
|
|
174
|
+
put: vi.fn(),
|
|
175
|
+
delete: vi.fn(),
|
|
176
|
+
list: vi.fn(),
|
|
177
|
+
head: vi.fn(),
|
|
178
|
+
};
|
|
179
|
+
const withoutHead = {
|
|
180
|
+
get: vi.fn(),
|
|
181
|
+
put: vi.fn(),
|
|
182
|
+
delete: vi.fn(),
|
|
183
|
+
list: vi.fn(),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const result = instrumentBindings({
|
|
187
|
+
R2_BUCKET: withHead,
|
|
188
|
+
KV_STORE: withoutHead,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Both should be wrapped but detected as different types
|
|
192
|
+
expect(isWrapped(result.R2_BUCKET)).toBe(true);
|
|
193
|
+
expect(isWrapped(result.KV_STORE)).toBe(true);
|
|
194
|
+
expect(result.R2_BUCKET).not.toBe(withHead);
|
|
195
|
+
expect(result.KV_STORE).not.toBe(withoutHead);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should skip already-wrapped bindings', () => {
|
|
199
|
+
const mockKV = {
|
|
200
|
+
get: vi.fn(),
|
|
201
|
+
put: vi.fn(),
|
|
202
|
+
delete: vi.fn(),
|
|
203
|
+
list: vi.fn(),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Pre-wrap the binding using the wrap helper
|
|
207
|
+
const preWrapped = wrap(mockKV, {
|
|
208
|
+
get(target, prop) {
|
|
209
|
+
return Reflect.get(target, prop);
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = instrumentBindings({ MY_KV: preWrapped });
|
|
214
|
+
|
|
215
|
+
// Should be the same already-wrapped object, not double-wrapped
|
|
216
|
+
expect(result.MY_KV).toBe(preWrapped);
|
|
217
|
+
expect(isWrapped(result.MY_KV)).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should pass through non-object values (strings, numbers)', () => {
|
|
221
|
+
const result = instrumentBindings({
|
|
222
|
+
API_KEY: 'my-secret-key',
|
|
223
|
+
TIMEOUT: 5000,
|
|
224
|
+
ENABLED: true,
|
|
225
|
+
EMPTY: null,
|
|
226
|
+
UNDEF: undefined,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result.API_KEY).toBe('my-secret-key');
|
|
230
|
+
expect(result.TIMEOUT).toBe(5000);
|
|
231
|
+
expect(result.ENABLED).toBe(true);
|
|
232
|
+
expect(result.EMPTY).toBe(null);
|
|
233
|
+
expect(result.UNDEF).toBe(undefined);
|
|
234
|
+
});
|
|
235
|
+
});
|
package/src/bindings/bindings.ts
CHANGED
|
@@ -23,7 +23,13 @@ import {
|
|
|
23
23
|
SpanStatusCode,
|
|
24
24
|
} from '@opentelemetry/api';
|
|
25
25
|
import { WorkerTracer } from 'autotel-edge';
|
|
26
|
-
import { wrap } from './common';
|
|
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';
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
35
|
* Instrument KV namespace
|
|
@@ -554,68 +560,113 @@ export function instrumentServiceBinding<F extends Fetcher>(fetcher: F, serviceN
|
|
|
554
560
|
return wrap(fetcher, fetcherHandler);
|
|
555
561
|
}
|
|
556
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Detection helpers
|
|
565
|
+
*/
|
|
566
|
+
const hasMethod = (obj: any, m: string): boolean =>
|
|
567
|
+
typeof obj?.[m] === 'function';
|
|
568
|
+
|
|
569
|
+
const hasExactMethods = (obj: any, methods: string[]): boolean =>
|
|
570
|
+
methods.every(m => hasMethod(obj, m));
|
|
571
|
+
|
|
557
572
|
/**
|
|
558
573
|
* Auto-instrument all Cloudflare bindings in the environment
|
|
574
|
+
*
|
|
575
|
+
* Detection order (most specific first):
|
|
576
|
+
* 1. R2 — get, put, delete, list, head
|
|
577
|
+
* 2. KV — get, put, delete, list (not head)
|
|
578
|
+
* 3. D1 — prepare, exec
|
|
579
|
+
* 4. Vectorize — query, insert, upsert, describe
|
|
580
|
+
* 5. AI — run + (gateway or models discriminator)
|
|
581
|
+
* 6. Hyperdrive — connect + connectionString + host
|
|
582
|
+
* 7. Queue Producer — send, sendBatch (not get)
|
|
583
|
+
* 8. Analytics Engine — writeDataPoint
|
|
584
|
+
* 9. Images — info, input
|
|
585
|
+
* 10. Service Binding — fetch (broadest, must be last)
|
|
586
|
+
*
|
|
587
|
+
* Not auto-detected (manual only):
|
|
588
|
+
* - Rate Limiter — limit() alone too generic
|
|
589
|
+
* - Browser Rendering — indistinguishable from Service Binding
|
|
559
590
|
*/
|
|
560
591
|
export function instrumentBindings(env: Record<string, any>): Record<string, any> {
|
|
561
592
|
const instrumented: Record<string, any> = {};
|
|
562
|
-
|
|
593
|
+
|
|
563
594
|
for (const [key, value] of Object.entries(env)) {
|
|
564
595
|
if (!value || typeof value !== 'object') {
|
|
565
596
|
instrumented[key] = value;
|
|
566
597
|
continue;
|
|
567
598
|
}
|
|
568
|
-
|
|
569
|
-
//
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
instrumented[key] = instrumentKV(value as KVNamespace, key);
|
|
574
|
-
continue;
|
|
575
|
-
} catch {
|
|
576
|
-
// Not KV, continue checking
|
|
577
|
-
}
|
|
599
|
+
|
|
600
|
+
// Skip already-instrumented bindings
|
|
601
|
+
if (isWrapped(value)) {
|
|
602
|
+
instrumented[key] = value;
|
|
603
|
+
continue;
|
|
578
604
|
}
|
|
579
|
-
|
|
580
|
-
//
|
|
581
|
-
if ('get'
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
instrumented[key] = instrumentR2(value as R2Bucket, key);
|
|
585
|
-
continue;
|
|
586
|
-
} catch {
|
|
587
|
-
// Not R2, continue checking
|
|
588
|
-
}
|
|
605
|
+
|
|
606
|
+
// 1. R2 — most specific (has head)
|
|
607
|
+
if (hasExactMethods(value, ['get', 'put', 'delete', 'list', 'head'])) {
|
|
608
|
+
instrumented[key] = instrumentR2(value as R2Bucket, key);
|
|
609
|
+
continue;
|
|
589
610
|
}
|
|
590
|
-
|
|
591
|
-
//
|
|
592
|
-
if ('
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
instrumented[key] = instrumentD1(value as D1Database, key);
|
|
596
|
-
continue;
|
|
597
|
-
} catch {
|
|
598
|
-
// Not D1, continue checking
|
|
599
|
-
}
|
|
611
|
+
|
|
612
|
+
// 2. KV — like R2 but without head
|
|
613
|
+
if (hasExactMethods(value, ['get', 'put', 'delete', 'list']) && !('head' in value)) {
|
|
614
|
+
instrumented[key] = instrumentKV(value as KVNamespace, key);
|
|
615
|
+
continue;
|
|
600
616
|
}
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
if ('
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
617
|
+
|
|
618
|
+
// 3. D1
|
|
619
|
+
if (hasExactMethods(value, ['prepare', 'exec'])) {
|
|
620
|
+
instrumented[key] = instrumentD1(value as D1Database, key);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 4. Vectorize
|
|
625
|
+
if (hasExactMethods(value, ['query', 'insert', 'upsert', 'describe'])) {
|
|
626
|
+
instrumented[key] = instrumentVectorize(value as VectorizeIndex, key);
|
|
627
|
+
continue;
|
|
611
628
|
}
|
|
612
|
-
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
|
|
629
|
+
|
|
630
|
+
// 5. AI — has run() + discriminator properties
|
|
631
|
+
if (hasMethod(value, 'run') && ('gateway' in value || 'models' in value)) {
|
|
632
|
+
instrumented[key] = instrumentAI(value as Ai, key);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// 6. Hyperdrive — connect + connection properties
|
|
637
|
+
if (hasMethod(value, 'connect') && 'connectionString' in value && 'host' in value) {
|
|
638
|
+
instrumented[key] = instrumentHyperdrive(value as Hyperdrive, key);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 7. Queue Producer — send + sendBatch (not get, to avoid KV collision)
|
|
643
|
+
if (hasExactMethods(value, ['send', 'sendBatch']) && !('get' in value)) {
|
|
644
|
+
instrumented[key] = instrumentQueueProducer(value as Queue, key);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// 8. Analytics Engine
|
|
649
|
+
if (hasMethod(value, 'writeDataPoint')) {
|
|
650
|
+
instrumented[key] = instrumentAnalyticsEngine(value as AnalyticsEngineDataset, key);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// 9. Images
|
|
655
|
+
if (hasExactMethods(value, ['info', 'input'])) {
|
|
656
|
+
instrumented[key] = instrumentImages(value as any, key);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// 10. Service Binding (broadest — must be last)
|
|
661
|
+
if (hasMethod(value, 'fetch')) {
|
|
662
|
+
instrumented[key] = instrumentServiceBinding(value as Fetcher, key);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Unknown binding type — pass through
|
|
616
667
|
instrumented[key] = value;
|
|
617
668
|
}
|
|
618
|
-
|
|
669
|
+
|
|
619
670
|
return instrumented;
|
|
620
671
|
}
|
|
621
672
|
|