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.
Files changed (40) hide show
  1. package/dist/actors.js +1 -1
  2. package/dist/bindings.d.ts +113 -1
  3. package/dist/bindings.js +2 -2
  4. package/dist/chunk-4UG2QCPQ.js +1060 -0
  5. package/dist/chunk-4UG2QCPQ.js.map +1 -0
  6. package/dist/{chunk-5NL62W4L.js → chunk-O4IYKWPJ.js} +8 -3
  7. package/dist/chunk-O4IYKWPJ.js.map +1 -0
  8. package/dist/{chunk-ADWSZ5GY.js → chunk-ZJPX4N7S.js} +3 -3
  9. package/dist/{chunk-ADWSZ5GY.js.map → chunk-ZJPX4N7S.js.map} +1 -1
  10. package/dist/handlers.js +2 -2
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +34 -6
  13. package/dist/index.js.map +1 -1
  14. package/package.json +1 -1
  15. package/src/bindings/ai.test.ts +156 -0
  16. package/src/bindings/ai.ts +71 -0
  17. package/src/bindings/analytics-engine.test.ts +160 -0
  18. package/src/bindings/analytics-engine.ts +78 -0
  19. package/src/bindings/bindings-detection.test.ts +235 -0
  20. package/src/bindings/bindings.ts +98 -47
  21. package/src/bindings/browser-rendering.test.ts +144 -0
  22. package/src/bindings/browser-rendering.ts +70 -0
  23. package/src/bindings/common.ts +9 -0
  24. package/src/bindings/hyperdrive.test.ts +154 -0
  25. package/src/bindings/hyperdrive.ts +74 -0
  26. package/src/bindings/images.test.ts +229 -0
  27. package/src/bindings/images.ts +182 -0
  28. package/src/bindings/index.ts +8 -0
  29. package/src/bindings/queue-producer.test.ts +192 -0
  30. package/src/bindings/queue-producer.ts +105 -0
  31. package/src/bindings/rate-limiter.test.ts +124 -0
  32. package/src/bindings/rate-limiter.ts +69 -0
  33. package/src/bindings/vectorize.test.ts +340 -0
  34. package/src/bindings/vectorize.ts +86 -0
  35. package/src/index.ts +8 -0
  36. package/src/wrappers/cf-attributes.test.ts +275 -0
  37. package/src/wrappers/instrument.ts +38 -0
  38. package/dist/chunk-5NL62W4L.js.map +0 -1
  39. package/dist/chunk-UPQE3J4I.js +0 -520
  40. package/dist/chunk-UPQE3J4I.js.map +0 -1
@@ -0,0 +1,275 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { instrument } from './instrument';
3
+ import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
4
+
5
+ describe('CF Attributes extraction via instrument()', () => {
6
+ let mockTracer: any;
7
+ let mockSpan: any;
8
+ let getTracerSpy: any;
9
+ let capturedSpanOptions: 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
+ capturedSpanOptions = null;
29
+
30
+ mockTracer = {
31
+ startActiveSpan: vi.fn((...args: any[]) => {
32
+ // startActiveSpan can be called with (name, options, context, fn) or (name, options, fn)
33
+ // The fetch instrumentation calls it with 4 args: (name, options, parentContext, fn)
34
+ const fn = args.at(-1);
35
+ if (args.length >= 2) {
36
+ capturedSpanOptions = args[1];
37
+ }
38
+ return fn(mockSpan);
39
+ }),
40
+ setHeadSampler: vi.fn(),
41
+ forceFlush: vi.fn(async () => {}),
42
+ };
43
+
44
+ getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
45
+ });
46
+
47
+ afterEach(() => {
48
+ getTracerSpy.mockRestore();
49
+ });
50
+
51
+ function createMockCtx() {
52
+ return {
53
+ waitUntil: vi.fn(),
54
+ passThroughOnException: vi.fn(),
55
+ } as any;
56
+ }
57
+
58
+ it('should extract CF attributes when request has .cf object', async () => {
59
+ const handler = {
60
+ async fetch(request: Request) {
61
+ return new Response('OK', { status: 200 });
62
+ },
63
+ };
64
+
65
+ const instrumented = instrument(handler, {
66
+ service: { name: 'test-worker' },
67
+ });
68
+
69
+ const request = new Request('http://example.com/test');
70
+ // Attach .cf properties to the request (Cloudflare Workers runtime does this)
71
+ Object.defineProperty(request, 'cf', {
72
+ value: {
73
+ colo: 'SJC',
74
+ country: 'US',
75
+ city: 'San Jose',
76
+ region: 'California',
77
+ continent: 'NA',
78
+ timezone: 'America/Los_Angeles',
79
+ latitude: '37.3382',
80
+ longitude: '-121.8863',
81
+ asn: 13_335,
82
+ asOrganization: 'Cloudflare Inc',
83
+ httpProtocol: 'HTTP/2',
84
+ tlsVersion: 'TLSv1.3',
85
+ clientTcpRtt: 5,
86
+ },
87
+ writable: false,
88
+ enumerable: true,
89
+ });
90
+
91
+ const env = {} as any;
92
+ const ctx = createMockCtx();
93
+
94
+ await instrumented.fetch!(request, env, ctx);
95
+
96
+ expect(capturedSpanOptions).toBeDefined();
97
+ const attrs = capturedSpanOptions.attributes;
98
+
99
+ expect(attrs['cloudflare.colo']).toBe('SJC');
100
+ expect(attrs['cloudflare.country']).toBe('US');
101
+ expect(attrs['cloudflare.city']).toBe('San Jose');
102
+ expect(attrs['cloudflare.region']).toBe('California');
103
+ expect(attrs['cloudflare.continent']).toBe('NA');
104
+ expect(attrs['cloudflare.timezone']).toBe('America/Los_Angeles');
105
+ expect(attrs['cloudflare.latitude']).toBe('37.3382');
106
+ expect(attrs['cloudflare.longitude']).toBe('-121.8863');
107
+ expect(attrs['cloudflare.asn']).toBe(13_335);
108
+ expect(attrs['cloudflare.as_organization']).toBe('Cloudflare Inc');
109
+ expect(attrs['cloudflare.http_protocol']).toBe('HTTP/2');
110
+ expect(attrs['cloudflare.tls_version']).toBe('TLSv1.3');
111
+ expect(attrs['cloudflare.client_tcp_rtt']).toBe(5);
112
+ });
113
+
114
+ it('should extract CF ray_id from cf-ray header', async () => {
115
+ const handler = {
116
+ async fetch(request: Request) {
117
+ return new Response('OK', { status: 200 });
118
+ },
119
+ };
120
+
121
+ const instrumented = instrument(handler, {
122
+ service: { name: 'test-worker' },
123
+ });
124
+
125
+ const request = new Request('http://example.com/test', {
126
+ headers: {
127
+ 'cf-ray': '8a1b2c3d4e5f6-SJC',
128
+ },
129
+ });
130
+ Object.defineProperty(request, 'cf', {
131
+ value: { colo: 'SJC' },
132
+ writable: false,
133
+ enumerable: true,
134
+ });
135
+
136
+ const env = {} as any;
137
+ const ctx = createMockCtx();
138
+
139
+ await instrumented.fetch!(request, env, ctx);
140
+
141
+ expect(capturedSpanOptions).toBeDefined();
142
+ const attrs = capturedSpanOptions.attributes;
143
+ expect(attrs['cloudflare.ray_id']).toBe('8a1b2c3d4e5f6-SJC');
144
+ expect(attrs['cloudflare.colo']).toBe('SJC');
145
+ });
146
+
147
+ it('should not include CF attributes when request.cf is undefined', async () => {
148
+ const handler = {
149
+ async fetch(request: Request) {
150
+ return new Response('OK', { status: 200 });
151
+ },
152
+ };
153
+
154
+ const instrumented = instrument(handler, {
155
+ service: { name: 'test-worker' },
156
+ });
157
+
158
+ // Standard request without .cf (local dev / Miniflare scenario)
159
+ const request = new Request('http://example.com/test');
160
+ const env = {} as any;
161
+ const ctx = createMockCtx();
162
+
163
+ await instrumented.fetch!(request, env, ctx);
164
+
165
+ expect(capturedSpanOptions).toBeDefined();
166
+ const attrs = capturedSpanOptions.attributes;
167
+
168
+ // Standard HTTP attributes should still be present
169
+ expect(attrs['http.request.method']).toBe('GET');
170
+ expect(attrs['url.full']).toBe('http://example.com/test');
171
+
172
+ // No cloudflare.* attributes should be present
173
+ const cfKeys = Object.keys(attrs).filter(k => k.startsWith('cloudflare.'));
174
+ expect(cfKeys).toHaveLength(0);
175
+ });
176
+
177
+ it('should correctly map all CF fields to cloudflare.* attribute names', async () => {
178
+ const handler = {
179
+ async fetch(request: Request) {
180
+ return new Response('OK', { status: 200 });
181
+ },
182
+ };
183
+
184
+ const instrumented = instrument(handler, {
185
+ service: { name: 'test-worker' },
186
+ });
187
+
188
+ const request = new Request('http://example.com/test', {
189
+ headers: { 'cf-ray': 'abc123-LAX' },
190
+ });
191
+ Object.defineProperty(request, 'cf', {
192
+ value: {
193
+ colo: 'LAX',
194
+ country: 'US',
195
+ city: 'Los Angeles',
196
+ region: 'California',
197
+ continent: 'NA',
198
+ timezone: 'America/Los_Angeles',
199
+ latitude: '34.0522',
200
+ longitude: '-118.2437',
201
+ asn: 13_335,
202
+ asOrganization: 'Cloudflare Inc',
203
+ httpProtocol: 'HTTP/3',
204
+ tlsVersion: 'TLSv1.3',
205
+ clientTcpRtt: 10,
206
+ },
207
+ writable: false,
208
+ enumerable: true,
209
+ });
210
+
211
+ const env = {} as any;
212
+ const ctx = createMockCtx();
213
+
214
+ await instrumented.fetch!(request, env, ctx);
215
+
216
+ const attrs = capturedSpanOptions.attributes;
217
+
218
+ // Verify the exact mapping from CF property names to attribute names
219
+ const expectedMappings: Record<string, [string, any]> = {
220
+ 'cloudflare.colo': ['colo', 'LAX'],
221
+ 'cloudflare.ray_id': ['cf-ray header', 'abc123-LAX'],
222
+ 'cloudflare.country': ['country', 'US'],
223
+ 'cloudflare.city': ['city', 'Los Angeles'],
224
+ 'cloudflare.region': ['region', 'California'],
225
+ 'cloudflare.continent': ['continent', 'NA'],
226
+ 'cloudflare.timezone': ['timezone', 'America/Los_Angeles'],
227
+ 'cloudflare.latitude': ['latitude', '34.0522'],
228
+ 'cloudflare.longitude': ['longitude', '-118.2437'],
229
+ 'cloudflare.asn': ['asn', 13_335],
230
+ 'cloudflare.as_organization': ['asOrganization', 'Cloudflare Inc'],
231
+ 'cloudflare.http_protocol': ['httpProtocol', 'HTTP/3'],
232
+ 'cloudflare.tls_version': ['tlsVersion', 'TLSv1.3'],
233
+ 'cloudflare.client_tcp_rtt': ['clientTcpRtt', 10],
234
+ };
235
+
236
+ for (const [attrKey, [, expectedValue]] of Object.entries(expectedMappings)) {
237
+ expect(attrs[attrKey]).toBe(expectedValue);
238
+ }
239
+ });
240
+
241
+ it('preserves valid falsy numeric CF attributes (0 values)', async () => {
242
+ const handler = {
243
+ async fetch(_request: Request) {
244
+ return new Response('OK', { status: 200 });
245
+ },
246
+ };
247
+
248
+ const instrumented = instrument(handler, {
249
+ service: { name: 'test-worker' },
250
+ });
251
+
252
+ const request = new Request('http://example.com/test');
253
+ Object.defineProperty(request, 'cf', {
254
+ value: {
255
+ latitude: 0,
256
+ longitude: 0,
257
+ asn: 0,
258
+ clientTcpRtt: 0,
259
+ },
260
+ writable: false,
261
+ enumerable: true,
262
+ });
263
+
264
+ const env = {} as any;
265
+ const ctx = createMockCtx();
266
+
267
+ await instrumented.fetch!(request, env, ctx);
268
+
269
+ const attrs = capturedSpanOptions.attributes;
270
+ expect(attrs['cloudflare.latitude']).toBe(0);
271
+ expect(attrs['cloudflare.longitude']).toBe(0);
272
+ expect(attrs['cloudflare.asn']).toBe(0);
273
+ expect(attrs['cloudflare.client_tcp_rtt']).toBe(0);
274
+ });
275
+ });
@@ -75,6 +75,39 @@ type EmailHandler = (
75
75
  /**
76
76
  * Create fetch handler instrumentation with config support for postProcess
77
77
  */
78
+ /**
79
+ * Extract Cloudflare-specific attributes from a request
80
+ */
81
+ function extractCfAttributes(request: Request): Record<string, string | number | boolean> {
82
+ const cf = (request as any).cf;
83
+ if (!cf) return {};
84
+
85
+ const attrs: Record<string, string | number | boolean> = {};
86
+ const set = (key: string, value: unknown) => {
87
+ if (value !== undefined && value !== null) {
88
+ attrs[key] = value as string | number | boolean;
89
+ }
90
+ };
91
+
92
+ set('cloudflare.colo', cf.colo);
93
+ const ray = request.headers.get('cf-ray');
94
+ if (ray) attrs['cloudflare.ray_id'] = ray;
95
+ set('cloudflare.country', cf.country);
96
+ set('cloudflare.city', cf.city);
97
+ set('cloudflare.region', cf.region);
98
+ set('cloudflare.continent', cf.continent);
99
+ set('cloudflare.timezone', cf.timezone);
100
+ set('cloudflare.latitude', cf.latitude);
101
+ set('cloudflare.longitude', cf.longitude);
102
+ set('cloudflare.asn', cf.asn);
103
+ set('cloudflare.as_organization', cf.asOrganization);
104
+ set('cloudflare.http_protocol', cf.httpProtocol);
105
+ set('cloudflare.tls_version', cf.tlsVersion);
106
+ set('cloudflare.client_tcp_rtt', cf.clientTcpRtt);
107
+
108
+ return attrs;
109
+ }
110
+
78
111
  function createFetchInstrumentation(
79
112
  config: ResolvedEdgeConfig,
80
113
  ): HandlerInstrumentation<Request, Response> {
@@ -82,6 +115,10 @@ function createFetchInstrumentation(
82
115
  getInitialSpanInfo: (request: Request): InitialSpanInfo => {
83
116
  const url = new URL(request.url);
84
117
 
118
+ const cfAttrs = (config as any).extractCfAttributes === false
119
+ ? {}
120
+ : extractCfAttributes(request);
121
+
85
122
  return {
86
123
  name: `${request.method} ${url.pathname}`,
87
124
  options: {
@@ -89,6 +126,7 @@ function createFetchInstrumentation(
89
126
  attributes: {
90
127
  'http.request.method': request.method,
91
128
  'url.full': request.url,
129
+ ...cfAttrs,
92
130
  },
93
131
  },
94
132
  context: propagation.extract(api_context.active(), request.headers),
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/bindings/common.ts"],"names":[],"mappings":";AAOO,IAAM,iBAAN,MAAqB;AAAA,EAClB,WAA+B,EAAC;AAAA,EAExC,MAAM,OAAA,EAAiC;AACrC,IAAA,IAAA,CAAK,QAAA,CAAS,KAAK,OAAO,CAAA;AAAA,EAC5B;AAAA,EAEA,MAAM,IAAA,GAAsB;AAC1B,IAAA,MAAM,OAAA,CAAQ,UAAA,CAAW,IAAA,CAAK,QAAQ,CAAA;AAAA,EACxC;AACF,CAAA;AAKO,SAAS,sBAAsB,GAAA,EAGpC;AACA,EAAA,MAAM,OAAA,GAAU,IAAI,cAAA,EAAe;AAEnC,EAAA,MAAM,OAAA,GAAU,IAAI,KAAA,CAAM,GAAA,EAAK;AAAA,IAC7B,GAAA,CAAI,QAAQ,IAAA,EAAM;AAChB,MAAA,IAAI,SAAS,WAAA,EAAa;AACxB,QAAA,OAAO,CAAC,OAAA,KAA8B;AACpC,UAAA,OAAA,CAAQ,MAAM,OAAO,CAAA;AACrB,UAAA,OAAO,MAAA,CAAO,UAAU,OAAO,CAAA;AAAA,QACjC,CAAA;AAAA,MACF;AACA,MAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AAAA,IACjC;AAAA,GACD,CAAA;AAED,EAAA,OAAO,EAAE,GAAA,EAAK,OAAA,EAAS,OAAA,EAAQ;AACjC;AAKA,IAAM,YAAA,0BAAsB,QAAQ,CAAA;AAI7B,SAAS,UAAa,IAAA,EAA6B;AACxD,EAAA,OAAO,IAAA,IAAQ,CAAC,CAAE,IAAA,CAAoB,YAAY,CAAA;AACpD;AAEO,SAAS,OAAyB,IAAA,EAAY;AACnD,EAAA,IAAI,IAAA,IAAQ,SAAA,CAAU,IAAI,CAAA,EAAG;AAC3B,IAAA,OAAO,KAAK,YAAY,CAAA;AAAA,EAC1B,CAAA,MAAO;AACL,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEO,SAAS,IAAA,CACd,MACA,OAAA,EACY;AACZ,EAAA,MAAM,KAAA,GAAQ,IAAI,KAAA,CAAM,IAAA,EAAM,OAAO,CAAA;AACrC,EAAA,MAAA,CAAO,cAAA,CAAe,OAAO,YAAA,EAAc;AAAA,IACzC,KAAA,EAAO,IAAA;AAAA,IACP,QAAA,EAAU,KAAA;AAAA,IACV,UAAA,EAAY,KAAA;AAAA,IACZ,YAAA,EAAc;AAAA,GACf,CAAA;AACD,EAAA,OAAO,KAAA;AACT","file":"chunk-5NL62W4L.js","sourcesContent":["/**\n * Common instrumentation utilities\n */\n\n/**\n * Promise tracker for waitUntil\n */\nexport class PromiseTracker {\n private promises: Promise<unknown>[] = [];\n\n track(promise: Promise<unknown>): void {\n this.promises.push(promise);\n }\n\n async wait(): Promise<void> {\n await Promise.allSettled(this.promises);\n }\n}\n\n/**\n * Proxy ExecutionContext to track waitUntil promises\n */\nexport function proxyExecutionContext(ctx: ExecutionContext): {\n ctx: ExecutionContext;\n tracker: PromiseTracker;\n} {\n const tracker = new PromiseTracker();\n\n const proxied = new Proxy(ctx, {\n get(target, prop) {\n if (prop === 'waitUntil') {\n return (promise: Promise<unknown>) => {\n tracker.track(promise);\n return target.waitUntil(promise);\n };\n }\n return Reflect.get(target, prop);\n },\n });\n\n return { ctx: proxied, tracker };\n}\n\n/**\n * Helper to wrap/unwrap proxied objects\n */\nconst unwrapSymbol = Symbol('unwrap');\n\ntype Wrapped<T> = { [unwrapSymbol]: T } & T;\n\nexport function isWrapped<T>(item: T): item is Wrapped<T> {\n return item && !!(item as Wrapped<T>)[unwrapSymbol];\n}\n\nexport function unwrap<T extends object>(item: T): T {\n if (item && isWrapped(item)) {\n return item[unwrapSymbol];\n } else {\n return item;\n }\n}\n\nexport function wrap<T extends object>(\n item: T,\n handler: ProxyHandler<T>,\n): Wrapped<T> {\n const proxy = new Proxy(item, handler) as Wrapped<T>;\n Object.defineProperty(proxy, unwrapSymbol, {\n value: item,\n writable: false,\n enumerable: false,\n configurable: false,\n });\n return proxy;\n}\n"]}