autotel-cloudflare 3.1.0 → 4.0.1
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/agents.d.ts.map +1 -1
- package/dist/agents.js +7 -0
- package/dist/agents.js.map +1 -1
- package/package.json +8 -4
- package/src/actors/alarms.ts +0 -225
- package/src/actors/index.ts +0 -36
- package/src/actors/instrument-actor.test.ts +0 -179
- package/src/actors/instrument-actor.ts +0 -574
- package/src/actors/sockets.ts +0 -217
- package/src/actors/storage.ts +0 -263
- package/src/actors/traced-handler.ts +0 -300
- package/src/actors/types.ts +0 -98
- package/src/actors.ts +0 -50
- package/src/agents/agent.ts +0 -327
- package/src/agents/base.ts +0 -26
- package/src/agents/index.ts +0 -46
- package/src/agents/mcp.ts +0 -38
- package/src/agents/observability.ts +0 -145
- package/src/agents/otel-observability.test.ts +0 -269
- package/src/agents/otel-observability.ts +0 -557
- package/src/agents/types.ts +0 -61
- package/src/agents.ts +0 -87
- package/src/bindings/ai.test.ts +0 -170
- package/src/bindings/ai.ts +0 -73
- package/src/bindings/analytics-engine.test.ts +0 -175
- package/src/bindings/analytics-engine.ts +0 -78
- package/src/bindings/bindings-cache.test.ts +0 -80
- package/src/bindings/bindings-detection.test.ts +0 -235
- package/src/bindings/bindings-this-binding.test.ts +0 -294
- package/src/bindings/bindings.ts +0 -720
- package/src/bindings/browser-rendering.test.ts +0 -160
- package/src/bindings/browser-rendering.ts +0 -70
- package/src/bindings/common.ts +0 -84
- package/src/bindings/hyperdrive.test.ts +0 -176
- package/src/bindings/hyperdrive.ts +0 -74
- package/src/bindings/images.test.ts +0 -262
- package/src/bindings/images.ts +0 -182
- package/src/bindings/index.ts +0 -20
- package/src/bindings/queue-producer.test.ts +0 -224
- package/src/bindings/queue-producer.ts +0 -105
- package/src/bindings/rate-limiter.test.ts +0 -140
- package/src/bindings/rate-limiter.ts +0 -69
- package/src/bindings/vectorize.test.ts +0 -362
- package/src/bindings/vectorize.ts +0 -86
- package/src/bindings.ts +0 -6
- package/src/events.ts +0 -6
- package/src/execution-logger.test.ts +0 -127
- package/src/execution-logger.ts +0 -112
- package/src/global/cache.test.ts +0 -292
- package/src/global/cache.ts +0 -164
- package/src/global/fetch.test.ts +0 -399
- package/src/global/fetch.ts +0 -136
- package/src/global/index.ts +0 -7
- package/src/handlers/durable-objects.test.ts +0 -524
- package/src/handlers/durable-objects.ts +0 -250
- package/src/handlers/index.ts +0 -6
- package/src/handlers/workflows.test.ts +0 -411
- package/src/handlers/workflows.ts +0 -345
- package/src/handlers.ts +0 -6
- package/src/index.ts +0 -91
- package/src/logger.ts +0 -15
- package/src/native/native-tracing.test.ts +0 -54
- package/src/native/native-tracing.ts +0 -83
- package/src/native.ts +0 -12
- package/src/parse-error.ts +0 -1
- package/src/sampling.ts +0 -6
- package/src/testing.ts +0 -6
- package/src/wrappers/cf-attributes.test.ts +0 -334
- package/src/wrappers/define-worker-fetch.test.ts +0 -102
- package/src/wrappers/define-worker-fetch.ts +0 -71
- package/src/wrappers/index.ts +0 -13
- package/src/wrappers/instrument.integration.test.ts +0 -578
- package/src/wrappers/instrument.ts +0 -806
- package/src/wrappers/native-instrument.test.ts +0 -153
- package/src/wrappers/wrap-do.ts +0 -34
- package/src/wrappers/wrap-module.ts +0 -37
package/src/bindings/ai.test.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
|
|
3
|
-
import { instrumentAI } from './ai';
|
|
4
|
-
|
|
5
|
-
describe('AI Binding Instrumentation', () => {
|
|
6
|
-
let mockTracer: any;
|
|
7
|
-
let mockSpan: any;
|
|
8
|
-
let getTracerSpy: any;
|
|
9
|
-
let mockAI: 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
|
-
mockAI = {
|
|
37
|
-
run: vi.fn(async () => ({ response: 'Hello world' })),
|
|
38
|
-
someOtherMethod: vi.fn(() => 'passthrough'),
|
|
39
|
-
someProperty: 'test-value',
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
getTracerSpy.mockRestore();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('instrumentAI()', () => {
|
|
48
|
-
it('should wrap AI binding with proxy', () => {
|
|
49
|
-
const instrumented = instrumentAI(mockAI);
|
|
50
|
-
|
|
51
|
-
expect(instrumented).not.toBe(mockAI);
|
|
52
|
-
expect(typeof instrumented.run).toBe('function');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should create span with correct name and attributes for run()', async () => {
|
|
56
|
-
const instrumented = instrumentAI(mockAI, 'my-ai');
|
|
57
|
-
|
|
58
|
-
await instrumented.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hello' });
|
|
59
|
-
|
|
60
|
-
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
61
|
-
|
|
62
|
-
const [spanName, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
63
|
-
expect(spanName).toBe('AI my-ai: run @cf/meta/llama-2-7b-chat-int8');
|
|
64
|
-
expect(options.kind).toBe(SpanKind.CLIENT);
|
|
65
|
-
expect(options.attributes['gen_ai.provider.name']).toBe('cloudflare-workers-ai');
|
|
66
|
-
expect(options.attributes['gen_ai.operation.name']).toBe('run');
|
|
67
|
-
expect(options.attributes['gen_ai.request.model']).toBe('@cf/meta/llama-2-7b-chat-int8');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should use default binding name "ai" when no name is provided', async () => {
|
|
71
|
-
const instrumented = instrumentAI(mockAI);
|
|
72
|
-
|
|
73
|
-
await instrumented.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hello' });
|
|
74
|
-
|
|
75
|
-
const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
|
|
76
|
-
expect(spanName).toBe('AI ai: run @cf/meta/llama-2-7b-chat-int8');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should record token usage when result has usage.prompt_tokens and usage.completion_tokens', async () => {
|
|
80
|
-
mockAI.run = vi.fn(async () => ({
|
|
81
|
-
response: 'Hello world',
|
|
82
|
-
usage: {
|
|
83
|
-
prompt_tokens: 10,
|
|
84
|
-
completion_tokens: 25,
|
|
85
|
-
},
|
|
86
|
-
}));
|
|
87
|
-
|
|
88
|
-
const instrumented = instrumentAI(mockAI);
|
|
89
|
-
|
|
90
|
-
await instrumented.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hello' });
|
|
91
|
-
|
|
92
|
-
expect(mockSpan.setAttribute).toHaveBeenCalledWith('gen_ai.usage.input_tokens', 10);
|
|
93
|
-
expect(mockSpan.setAttribute).toHaveBeenCalledWith('gen_ai.usage.output_tokens', 25);
|
|
94
|
-
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
95
|
-
expect(mockSpan.end).toHaveBeenCalled();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should NOT set token attributes when usage is not present', async () => {
|
|
99
|
-
mockAI.run = vi.fn(async () => ({
|
|
100
|
-
response: 'Hello world',
|
|
101
|
-
}));
|
|
102
|
-
|
|
103
|
-
const instrumented = instrumentAI(mockAI);
|
|
104
|
-
|
|
105
|
-
await instrumented.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hello' });
|
|
106
|
-
|
|
107
|
-
expect(mockSpan.setAttribute).not.toHaveBeenCalledWith(
|
|
108
|
-
'gen_ai.usage.input_tokens',
|
|
109
|
-
expect.anything(),
|
|
110
|
-
);
|
|
111
|
-
expect(mockSpan.setAttribute).not.toHaveBeenCalledWith(
|
|
112
|
-
'gen_ai.usage.output_tokens',
|
|
113
|
-
expect.anything(),
|
|
114
|
-
);
|
|
115
|
-
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
116
|
-
expect(mockSpan.end).toHaveBeenCalled();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should handle errors correctly', async () => {
|
|
120
|
-
const testError = new Error('AI model failed');
|
|
121
|
-
mockAI.run = vi.fn(async () => {
|
|
122
|
-
throw testError;
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const instrumented = instrumentAI(mockAI);
|
|
126
|
-
|
|
127
|
-
await expect(
|
|
128
|
-
instrumented.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hello' }),
|
|
129
|
-
).rejects.toThrow('AI model failed');
|
|
130
|
-
|
|
131
|
-
expect(mockSpan.recordException).toHaveBeenCalledWith(testError);
|
|
132
|
-
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
133
|
-
code: SpanStatusCode.ERROR,
|
|
134
|
-
message: 'AI model failed',
|
|
135
|
-
});
|
|
136
|
-
expect(mockSpan.end).toHaveBeenCalled();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should invoke run() with original object as this, not the proxy', async () => {
|
|
140
|
-
let receivedThis: any;
|
|
141
|
-
const mockAIObj = {
|
|
142
|
-
run: vi.fn(async function(this: any) {
|
|
143
|
-
// eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
|
|
144
|
-
receivedThis = this;
|
|
145
|
-
return { response: 'Hello' };
|
|
146
|
-
}),
|
|
147
|
-
};
|
|
148
|
-
const instrumented = instrumentAI(mockAIObj as any, 'test');
|
|
149
|
-
await instrumented.run('@cf/meta/llama-2-7b-chat-int8', { prompt: 'Hello' });
|
|
150
|
-
expect(receivedThis).toBe(mockAIObj);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should pass through non-instrumented methods unchanged', () => {
|
|
154
|
-
const instrumented = instrumentAI(mockAI);
|
|
155
|
-
|
|
156
|
-
const result = instrumented.someOtherMethod();
|
|
157
|
-
|
|
158
|
-
expect(result).toBe('passthrough');
|
|
159
|
-
expect(mockAI.someOtherMethod).toHaveBeenCalled();
|
|
160
|
-
expect(mockTracer.startActiveSpan).not.toHaveBeenCalled();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should pass through non-instrumented properties unchanged', () => {
|
|
164
|
-
const instrumented = instrumentAI(mockAI);
|
|
165
|
-
|
|
166
|
-
expect(instrumented.someProperty).toBe('test-value');
|
|
167
|
-
expect(mockTracer.startActiveSpan).not.toHaveBeenCalled();
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
});
|
package/src/bindings/ai.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workers AI 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, setAttr } from './common';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Instrument Workers AI binding
|
|
15
|
-
*/
|
|
16
|
-
export function instrumentAI<T extends Ai>(ai: T, bindingName?: string): T {
|
|
17
|
-
const name = bindingName || 'ai';
|
|
18
|
-
|
|
19
|
-
const handler: ProxyHandler<T> = {
|
|
20
|
-
get(target, prop) {
|
|
21
|
-
const value = Reflect.get(target, prop);
|
|
22
|
-
|
|
23
|
-
if (prop === 'run' && typeof value === 'function') {
|
|
24
|
-
return new Proxy(value, {
|
|
25
|
-
apply: (fnTarget, _thisArg, args) => {
|
|
26
|
-
const [model] = args as [string, unknown, unknown];
|
|
27
|
-
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
28
|
-
|
|
29
|
-
return tracer.startActiveSpan(
|
|
30
|
-
`AI ${name}: run ${model}`,
|
|
31
|
-
{
|
|
32
|
-
kind: SpanKind.CLIENT,
|
|
33
|
-
attributes: {
|
|
34
|
-
// Canonical GenAI provider attribute (replaces deprecated
|
|
35
|
-
// `gen_ai.system`).
|
|
36
|
-
'gen_ai.provider.name': 'cloudflare-workers-ai',
|
|
37
|
-
'gen_ai.operation.name': 'run',
|
|
38
|
-
'gen_ai.request.model': model,
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
async (span) => {
|
|
42
|
-
try {
|
|
43
|
-
const result = await Reflect.apply(fnTarget, target, args);
|
|
44
|
-
if (result?.usage?.prompt_tokens !== undefined) {
|
|
45
|
-
setAttr(span, 'gen_ai.usage.input_tokens', Number(result.usage.prompt_tokens));
|
|
46
|
-
}
|
|
47
|
-
if (result?.usage?.completion_tokens !== undefined) {
|
|
48
|
-
setAttr(span, 'gen_ai.usage.output_tokens', Number(result.usage.completion_tokens));
|
|
49
|
-
}
|
|
50
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
51
|
-
return result;
|
|
52
|
-
} catch (error) {
|
|
53
|
-
span.recordException(error as Error);
|
|
54
|
-
span.setStatus({
|
|
55
|
-
code: SpanStatusCode.ERROR,
|
|
56
|
-
message: error instanceof Error ? error.message : String(error),
|
|
57
|
-
});
|
|
58
|
-
throw error;
|
|
59
|
-
} finally {
|
|
60
|
-
span.end();
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
);
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return value;
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
return wrap(ai, handler);
|
|
73
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
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('this-binding', () => {
|
|
149
|
-
it('should invoke writeDataPoint() with original object as this, not the proxy', () => {
|
|
150
|
-
let receivedThis: any;
|
|
151
|
-
const mockAEObj = {
|
|
152
|
-
writeDataPoint: vi.fn(function(this: any) {
|
|
153
|
-
// eslint-disable-next-line unicorn/no-this-assignment, @typescript-eslint/no-this-alias
|
|
154
|
-
receivedThis = this;
|
|
155
|
-
}),
|
|
156
|
-
};
|
|
157
|
-
const instrumented = instrumentAnalyticsEngine(mockAEObj as any, 'test');
|
|
158
|
-
instrumented.writeDataPoint({ indexes: ['idx1'] });
|
|
159
|
-
expect(receivedThis).toBe(mockAEObj);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe('Non-instrumented methods', () => {
|
|
164
|
-
it('should pass through non-instrumented methods unchanged', () => {
|
|
165
|
-
const instrumented = instrumentAnalyticsEngine(mockAE, 'my-dataset');
|
|
166
|
-
|
|
167
|
-
const result = instrumented.someOtherMethod();
|
|
168
|
-
|
|
169
|
-
expect(result).toBe('passthrough-value');
|
|
170
|
-
expect(mockAE.someOtherMethod).toHaveBeenCalled();
|
|
171
|
-
// No span should be created for non-instrumented methods
|
|
172
|
-
expect(mockTracer.startActiveSpan).not.toHaveBeenCalled();
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
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, target, 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
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { trace } from '@opentelemetry/api';
|
|
3
|
-
import { instrumentBindings } from './bindings';
|
|
4
|
-
import { isWrapped } from './common';
|
|
5
|
-
|
|
6
|
-
describe('instrumentBindings() caching', () => {
|
|
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 return the same instrumented object for the same env reference', () => {
|
|
42
|
-
const env = {
|
|
43
|
-
MY_KV: { get: vi.fn(), put: vi.fn(), delete: vi.fn(), list: vi.fn() },
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const first = instrumentBindings(env);
|
|
47
|
-
const second = instrumentBindings(env);
|
|
48
|
-
|
|
49
|
-
expect(first).toBe(second);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should return different instrumented objects for different env references', () => {
|
|
53
|
-
const env1 = {
|
|
54
|
-
MY_KV: { get: vi.fn(), put: vi.fn(), delete: vi.fn(), list: vi.fn() },
|
|
55
|
-
};
|
|
56
|
-
const env2 = {
|
|
57
|
-
MY_KV: { get: vi.fn(), put: vi.fn(), delete: vi.fn(), list: vi.fn() },
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const first = instrumentBindings(env1);
|
|
61
|
-
const second = instrumentBindings(env2);
|
|
62
|
-
|
|
63
|
-
expect(first).not.toBe(second);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should correctly instrument bindings even when returning from cache', () => {
|
|
67
|
-
const env = {
|
|
68
|
-
MY_KV: { get: vi.fn(), put: vi.fn(), delete: vi.fn(), list: vi.fn() },
|
|
69
|
-
API_KEY: 'secret',
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const first = instrumentBindings(env);
|
|
73
|
-
const second = instrumentBindings(env);
|
|
74
|
-
|
|
75
|
-
// Cached result should have instrumented bindings
|
|
76
|
-
expect(isWrapped(second.MY_KV)).toBe(true);
|
|
77
|
-
// Non-object values pass through
|
|
78
|
-
expect(second.API_KEY).toBe('secret');
|
|
79
|
-
});
|
|
80
|
-
});
|