autotel-cloudflare 2.12.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 +1 -1
- 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,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Rendering 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
|
+
interface BrowserRenderingLike {
|
|
14
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Instrument Browser Rendering binding (manual only — not auto-detected)
|
|
19
|
+
*/
|
|
20
|
+
export function instrumentBrowserRendering<T extends BrowserRenderingLike>(browser: T, bindingName?: string): T {
|
|
21
|
+
const name = bindingName || 'browser';
|
|
22
|
+
|
|
23
|
+
const handler: ProxyHandler<T> = {
|
|
24
|
+
get(target, prop) {
|
|
25
|
+
const value = Reflect.get(target, prop);
|
|
26
|
+
|
|
27
|
+
if (prop === 'fetch' && typeof value === 'function') {
|
|
28
|
+
return new Proxy(value, {
|
|
29
|
+
apply: (fnTarget, thisArg, args) => {
|
|
30
|
+
const [input] = args as [RequestInfo | URL, RequestInit | undefined];
|
|
31
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
32
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
33
|
+
|
|
34
|
+
return tracer.startActiveSpan(
|
|
35
|
+
`BrowserRendering ${name}: fetch`,
|
|
36
|
+
{
|
|
37
|
+
kind: SpanKind.CLIENT,
|
|
38
|
+
attributes: {
|
|
39
|
+
'browser.system': 'cloudflare-browser-rendering',
|
|
40
|
+
'url.full': url,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
async (span) => {
|
|
44
|
+
try {
|
|
45
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
46
|
+
setAttr(span, 'http.response.status_code', result?.status);
|
|
47
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
48
|
+
return result;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
span.recordException(error as Error);
|
|
51
|
+
span.setStatus({
|
|
52
|
+
code: SpanStatusCode.ERROR,
|
|
53
|
+
message: error instanceof Error ? error.message : String(error),
|
|
54
|
+
});
|
|
55
|
+
throw error;
|
|
56
|
+
} finally {
|
|
57
|
+
span.end();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return value;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return wrap(browser, handler);
|
|
70
|
+
}
|
package/src/bindings/common.ts
CHANGED
|
@@ -60,6 +60,15 @@ export function unwrap<T extends object>(item: T): T {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Set attribute only if value is defined and non-null
|
|
65
|
+
*/
|
|
66
|
+
export function setAttr(span: { setAttribute: (key: string, value: any) => void }, key: string, value: unknown): void {
|
|
67
|
+
if (value !== undefined && value !== null) {
|
|
68
|
+
span.setAttribute(key, value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
export function wrap<T extends object>(
|
|
64
73
|
item: T,
|
|
65
74
|
handler: ProxyHandler<T>,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { instrumentHyperdrive } from './hyperdrive';
|
|
3
|
+
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
describe('Hyperdrive Binding Instrumentation', () => {
|
|
6
|
+
let mockTracer: any;
|
|
7
|
+
let mockSpan: any;
|
|
8
|
+
let getTracerSpy: any;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockSpan = {
|
|
12
|
+
spanContext: () => ({
|
|
13
|
+
traceId: 'test-trace-id',
|
|
14
|
+
spanId: 'test-span-id',
|
|
15
|
+
traceFlags: 1,
|
|
16
|
+
}),
|
|
17
|
+
setAttribute: vi.fn(),
|
|
18
|
+
setAttributes: vi.fn(),
|
|
19
|
+
setStatus: vi.fn(),
|
|
20
|
+
recordException: vi.fn(),
|
|
21
|
+
end: vi.fn(),
|
|
22
|
+
isRecording: () => true,
|
|
23
|
+
updateName: vi.fn(),
|
|
24
|
+
addEvent: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
mockTracer = {
|
|
28
|
+
startActiveSpan: vi.fn((name, options, fn) => {
|
|
29
|
+
return fn(mockSpan);
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
getTracerSpy.mockRestore();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function createMockHyperdrive(overrides: Partial<Hyperdrive> = {}): Hyperdrive {
|
|
41
|
+
return {
|
|
42
|
+
connect: vi.fn(async () => ({} as Socket)),
|
|
43
|
+
connectionString: 'postgresql://user:secret-password@db.example.com:5432/mydb',
|
|
44
|
+
host: 'db.example.com',
|
|
45
|
+
port: 5432,
|
|
46
|
+
user: 'user',
|
|
47
|
+
password: 'secret-password',
|
|
48
|
+
database: 'mydb',
|
|
49
|
+
...overrides,
|
|
50
|
+
} as unknown as Hyperdrive;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('connect()', () => {
|
|
54
|
+
it('should create span with correct attributes', async () => {
|
|
55
|
+
const mockHyperdrive = createMockHyperdrive();
|
|
56
|
+
const instrumented = instrumentHyperdrive(mockHyperdrive, 'my-db');
|
|
57
|
+
|
|
58
|
+
await instrumented.connect();
|
|
59
|
+
|
|
60
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
61
|
+
|
|
62
|
+
const [spanName, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
63
|
+
expect(spanName).toBe('Hyperdrive my-db: connect');
|
|
64
|
+
expect(options.kind).toBe(SpanKind.CLIENT);
|
|
65
|
+
expect(options.attributes['db.system']).toBe('cloudflare-hyperdrive');
|
|
66
|
+
expect(options.attributes['db.operation']).toBe('connect');
|
|
67
|
+
expect(options.attributes['server.address']).toBe('db.example.com');
|
|
68
|
+
expect(options.attributes['server.port']).toBe(5432);
|
|
69
|
+
expect(options.attributes['db.user']).toBe('user');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle errors correctly', async () => {
|
|
73
|
+
const connectError = new Error('Connection refused');
|
|
74
|
+
const mockHyperdrive = createMockHyperdrive({
|
|
75
|
+
connect: vi.fn(async () => {
|
|
76
|
+
throw connectError;
|
|
77
|
+
}) as any,
|
|
78
|
+
});
|
|
79
|
+
const instrumented = instrumentHyperdrive(mockHyperdrive, 'my-db');
|
|
80
|
+
|
|
81
|
+
await expect(instrumented.connect()).rejects.toThrow('Connection refused');
|
|
82
|
+
|
|
83
|
+
expect(mockSpan.recordException).toHaveBeenCalledWith(connectError);
|
|
84
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
85
|
+
code: SpanStatusCode.ERROR,
|
|
86
|
+
message: 'Connection refused',
|
|
87
|
+
});
|
|
88
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should set OK status and end span on success', async () => {
|
|
92
|
+
const mockSocket = { readable: true } as unknown as Socket;
|
|
93
|
+
const mockHyperdrive = createMockHyperdrive({
|
|
94
|
+
connect: vi.fn(async () => mockSocket) as any,
|
|
95
|
+
});
|
|
96
|
+
const instrumented = instrumentHyperdrive(mockHyperdrive, 'my-db');
|
|
97
|
+
|
|
98
|
+
const result = await instrumented.connect();
|
|
99
|
+
|
|
100
|
+
expect(result).toBe(mockSocket);
|
|
101
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
102
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should use default binding name when none provided', async () => {
|
|
106
|
+
const mockHyperdrive = createMockHyperdrive();
|
|
107
|
+
const instrumented = instrumentHyperdrive(mockHyperdrive);
|
|
108
|
+
|
|
109
|
+
await instrumented.connect();
|
|
110
|
+
|
|
111
|
+
const [spanName] = mockTracer.startActiveSpan.mock.calls[0];
|
|
112
|
+
expect(spanName).toBe('Hyperdrive hyperdrive: connect');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('non-instrumented properties', () => {
|
|
117
|
+
it('should pass through non-instrumented properties', () => {
|
|
118
|
+
const mockHyperdrive = createMockHyperdrive();
|
|
119
|
+
const instrumented = instrumentHyperdrive(mockHyperdrive, 'my-db');
|
|
120
|
+
|
|
121
|
+
expect(instrumented.connectionString).toBe('postgresql://user:secret-password@db.example.com:5432/mydb');
|
|
122
|
+
expect(instrumented.host).toBe('db.example.com');
|
|
123
|
+
expect(instrumented.port).toBe(5432);
|
|
124
|
+
expect(instrumented.user).toBe('user');
|
|
125
|
+
expect(instrumented.database).toBe('mydb');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('security', () => {
|
|
130
|
+
it('should never record password as an attribute', async () => {
|
|
131
|
+
const mockHyperdrive = createMockHyperdrive({
|
|
132
|
+
password: 'super-secret-password',
|
|
133
|
+
});
|
|
134
|
+
const instrumented = instrumentHyperdrive(mockHyperdrive, 'my-db');
|
|
135
|
+
|
|
136
|
+
await instrumented.connect();
|
|
137
|
+
|
|
138
|
+
const [, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
139
|
+
|
|
140
|
+
// Verify password is not in the attributes
|
|
141
|
+
const attributeKeys = Object.keys(options.attributes);
|
|
142
|
+
for (const key of attributeKeys) {
|
|
143
|
+
expect(options.attributes[key]).not.toBe('super-secret-password');
|
|
144
|
+
}
|
|
145
|
+
expect(options.attributes['db.password']).toBeUndefined();
|
|
146
|
+
expect(options.attributes['password']).toBeUndefined();
|
|
147
|
+
|
|
148
|
+
// Also verify setAttribute was not called with the password
|
|
149
|
+
for (const call of mockSpan.setAttribute.mock.calls) {
|
|
150
|
+
expect(call[1]).not.toBe('super-secret-password');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hyperdrive 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 Hyperdrive binding
|
|
15
|
+
*/
|
|
16
|
+
export function instrumentHyperdrive<T extends Hyperdrive>(hyperdrive: T, bindingName?: string): T {
|
|
17
|
+
const name = bindingName || 'hyperdrive';
|
|
18
|
+
|
|
19
|
+
const handler: ProxyHandler<T> = {
|
|
20
|
+
get(target, prop) {
|
|
21
|
+
const value = Reflect.get(target, prop);
|
|
22
|
+
|
|
23
|
+
if (prop === 'connect' && typeof value === 'function') {
|
|
24
|
+
return new Proxy(value, {
|
|
25
|
+
apply: (fnTarget, thisArg, args) => {
|
|
26
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
27
|
+
|
|
28
|
+
const attributes: Record<string, string | number> = {
|
|
29
|
+
'db.system': 'cloudflare-hyperdrive',
|
|
30
|
+
'db.operation': 'connect',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Extract connection info safely (never record password)
|
|
34
|
+
try {
|
|
35
|
+
setAttr({ setAttribute: (k: string, v: any) => { if (v !== undefined && v !== null) attributes[k] = v; } }, 'server.address', target.host);
|
|
36
|
+
setAttr({ setAttribute: (k: string, v: any) => { if (v !== undefined && v !== null) attributes[k] = v; } }, 'server.port', target.port);
|
|
37
|
+
setAttr({ setAttribute: (k: string, v: any) => { if (v !== undefined && v !== null) attributes[k] = v; } }, 'db.user', target.user);
|
|
38
|
+
} catch {
|
|
39
|
+
// Properties may not be accessible in all environments
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return tracer.startActiveSpan(
|
|
43
|
+
`Hyperdrive ${name}: connect`,
|
|
44
|
+
{
|
|
45
|
+
kind: SpanKind.CLIENT,
|
|
46
|
+
attributes,
|
|
47
|
+
},
|
|
48
|
+
async (span) => {
|
|
49
|
+
try {
|
|
50
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
51
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
52
|
+
return result;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
span.recordException(error as Error);
|
|
55
|
+
span.setStatus({
|
|
56
|
+
code: SpanStatusCode.ERROR,
|
|
57
|
+
message: error instanceof Error ? error.message : String(error),
|
|
58
|
+
});
|
|
59
|
+
throw error;
|
|
60
|
+
} finally {
|
|
61
|
+
span.end();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return value;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return wrap(hyperdrive, handler);
|
|
74
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
|
|
3
|
+
import { instrumentImages } from './images';
|
|
4
|
+
|
|
5
|
+
describe('Images Binding Instrumentation', () => {
|
|
6
|
+
let mockTracer: any;
|
|
7
|
+
let mockSpan: any;
|
|
8
|
+
let getTracerSpy: any;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockSpan = {
|
|
12
|
+
spanContext: () => ({
|
|
13
|
+
traceId: 'test-trace-id',
|
|
14
|
+
spanId: 'test-span-id',
|
|
15
|
+
traceFlags: 1,
|
|
16
|
+
}),
|
|
17
|
+
setAttribute: vi.fn(),
|
|
18
|
+
setAttributes: vi.fn(),
|
|
19
|
+
setStatus: vi.fn(),
|
|
20
|
+
recordException: vi.fn(),
|
|
21
|
+
end: vi.fn(),
|
|
22
|
+
isRecording: () => true,
|
|
23
|
+
updateName: vi.fn(),
|
|
24
|
+
addEvent: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
mockTracer = {
|
|
28
|
+
startActiveSpan: vi.fn((name, options, fn) => {
|
|
29
|
+
return fn(mockSpan);
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
getTracerSpy.mockRestore();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function createMockImages() {
|
|
41
|
+
const mockTransformer: any = {
|
|
42
|
+
transform: vi.fn(function (this: any) {
|
|
43
|
+
return mockTransformer;
|
|
44
|
+
}),
|
|
45
|
+
draw: vi.fn(function (this: any) {
|
|
46
|
+
return mockTransformer;
|
|
47
|
+
}),
|
|
48
|
+
output: vi.fn(async () => ({
|
|
49
|
+
response: () => new Response('image-data'),
|
|
50
|
+
blob: async () => new Blob(['image-data']),
|
|
51
|
+
arrayBuffer: async () => new ArrayBuffer(8),
|
|
52
|
+
})),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
images: {
|
|
57
|
+
info: vi.fn(async () => ({ width: 800, height: 600, format: 'png' })),
|
|
58
|
+
input: vi.fn(() => mockTransformer),
|
|
59
|
+
someOtherMethod: vi.fn(() => 'passthrough'),
|
|
60
|
+
someProperty: 'test-value',
|
|
61
|
+
},
|
|
62
|
+
mockTransformer,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('info()', () => {
|
|
67
|
+
it('should create its own span with correct attributes', async () => {
|
|
68
|
+
const { images } = createMockImages();
|
|
69
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
70
|
+
|
|
71
|
+
await instrumented.info(new ArrayBuffer(8));
|
|
72
|
+
|
|
73
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
74
|
+
|
|
75
|
+
const [spanName, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
76
|
+
expect(spanName).toBe('Images my-images: info');
|
|
77
|
+
expect(options.kind).toBe(SpanKind.CLIENT);
|
|
78
|
+
expect(options.attributes['images.system']).toBe('cloudflare-images');
|
|
79
|
+
expect(options.attributes['images.operation']).toBe('info');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should record width, height, format from result', async () => {
|
|
83
|
+
const { images } = createMockImages();
|
|
84
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
85
|
+
|
|
86
|
+
await instrumented.info(new ArrayBuffer(8));
|
|
87
|
+
|
|
88
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('images.width', 800);
|
|
89
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('images.height', 600);
|
|
90
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('images.format', 'png');
|
|
91
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
92
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle errors', async () => {
|
|
96
|
+
const { images } = createMockImages();
|
|
97
|
+
const testError = new Error('Image info failed');
|
|
98
|
+
images.info = vi.fn(async () => {
|
|
99
|
+
throw testError;
|
|
100
|
+
});
|
|
101
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
102
|
+
|
|
103
|
+
await expect(instrumented.info(new ArrayBuffer(8))).rejects.toThrow('Image info failed');
|
|
104
|
+
|
|
105
|
+
expect(mockSpan.recordException).toHaveBeenCalledWith(testError);
|
|
106
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
107
|
+
code: SpanStatusCode.ERROR,
|
|
108
|
+
message: 'Image info failed',
|
|
109
|
+
});
|
|
110
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('pipeline: input() -> output()', () => {
|
|
115
|
+
it('should create a single span at output() with operation_count = 0', async () => {
|
|
116
|
+
const { images } = createMockImages();
|
|
117
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
118
|
+
|
|
119
|
+
const transformer = instrumented.input(new ArrayBuffer(8));
|
|
120
|
+
await transformer.output();
|
|
121
|
+
|
|
122
|
+
// Only one span created at output(), not at input()
|
|
123
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
124
|
+
|
|
125
|
+
const [spanName, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
126
|
+
expect(spanName).toBe('Images my-images: output');
|
|
127
|
+
expect(options.attributes['images.system']).toBe('cloudflare-images');
|
|
128
|
+
expect(options.attributes['images.pipeline.operation_count']).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('pipeline: input() -> transform() -> output()', () => {
|
|
133
|
+
it('should create span with operation_count = 1', async () => {
|
|
134
|
+
const { images } = createMockImages();
|
|
135
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
136
|
+
|
|
137
|
+
const transformer = instrumented.input(new ArrayBuffer(8));
|
|
138
|
+
const transformed = transformer.transform({ width: 400 });
|
|
139
|
+
await transformed.output();
|
|
140
|
+
|
|
141
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
142
|
+
|
|
143
|
+
const [, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
144
|
+
expect(options.attributes['images.pipeline.operation_count']).toBe(1);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('pipeline: input() -> transform() -> draw() -> output()', () => {
|
|
149
|
+
it('should create span with operation_count = 2', async () => {
|
|
150
|
+
const { images } = createMockImages();
|
|
151
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
152
|
+
|
|
153
|
+
const transformer = instrumented.input(new ArrayBuffer(8));
|
|
154
|
+
const transformed = transformer.transform({ width: 400 });
|
|
155
|
+
const drawn = transformed.draw({});
|
|
156
|
+
await drawn.output();
|
|
157
|
+
|
|
158
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
159
|
+
|
|
160
|
+
const [, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
161
|
+
expect(options.attributes['images.pipeline.operation_count']).toBe(2);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('output() format capture', () => {
|
|
166
|
+
it('should capture format from string arg', async () => {
|
|
167
|
+
const { images } = createMockImages();
|
|
168
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
169
|
+
|
|
170
|
+
const transformer = instrumented.input(new ArrayBuffer(8));
|
|
171
|
+
await transformer.output('webp');
|
|
172
|
+
|
|
173
|
+
const [, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
174
|
+
expect(options.attributes['images.output.format']).toBe('webp');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should capture format from options object', async () => {
|
|
178
|
+
const { images } = createMockImages();
|
|
179
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
180
|
+
|
|
181
|
+
const transformer = instrumented.input(new ArrayBuffer(8));
|
|
182
|
+
await transformer.output({ format: 'avif' });
|
|
183
|
+
|
|
184
|
+
const [, options] = mockTracer.startActiveSpan.mock.calls[0];
|
|
185
|
+
expect(options.attributes['images.output.format']).toBe('avif');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle errors in output()', async () => {
|
|
189
|
+
const { images, mockTransformer } = createMockImages();
|
|
190
|
+
const testError = new Error('Output failed');
|
|
191
|
+
mockTransformer.output = vi.fn(async () => {
|
|
192
|
+
throw testError;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
196
|
+
const transformer = instrumented.input(new ArrayBuffer(8));
|
|
197
|
+
|
|
198
|
+
await expect(transformer.output()).rejects.toThrow('Output failed');
|
|
199
|
+
|
|
200
|
+
expect(mockSpan.recordException).toHaveBeenCalledWith(testError);
|
|
201
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
202
|
+
code: SpanStatusCode.ERROR,
|
|
203
|
+
message: 'Output failed',
|
|
204
|
+
});
|
|
205
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('non-instrumented methods', () => {
|
|
210
|
+
it('should pass through non-instrumented methods unchanged', () => {
|
|
211
|
+
const { images } = createMockImages();
|
|
212
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
213
|
+
|
|
214
|
+
const result = (instrumented as any).someOtherMethod();
|
|
215
|
+
|
|
216
|
+
expect(result).toBe('passthrough');
|
|
217
|
+
expect(images.someOtherMethod).toHaveBeenCalled();
|
|
218
|
+
expect(mockTracer.startActiveSpan).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should pass through non-instrumented properties unchanged', () => {
|
|
222
|
+
const { images } = createMockImages();
|
|
223
|
+
const instrumented = instrumentImages(images as any, 'my-images');
|
|
224
|
+
|
|
225
|
+
expect((instrumented as any).someProperty).toBe('test-value');
|
|
226
|
+
expect(mockTracer.startActiveSpan).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|