autotel-cloudflare 2.1.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +432 -0
  3. package/dist/actors.d.ts +248 -0
  4. package/dist/actors.js +1030 -0
  5. package/dist/actors.js.map +1 -0
  6. package/dist/agents.d.ts +219 -0
  7. package/dist/agents.js +276 -0
  8. package/dist/agents.js.map +1 -0
  9. package/dist/bindings.d.ts +40 -0
  10. package/dist/bindings.js +4 -0
  11. package/dist/bindings.js.map +1 -0
  12. package/dist/chunk-JDPN3HND.js +520 -0
  13. package/dist/chunk-JDPN3HND.js.map +1 -0
  14. package/dist/chunk-QXFYTHQF.js +298 -0
  15. package/dist/chunk-QXFYTHQF.js.map +1 -0
  16. package/dist/chunk-SKKRPS5K.js +50 -0
  17. package/dist/chunk-SKKRPS5K.js.map +1 -0
  18. package/dist/events.d.ts +1 -0
  19. package/dist/events.js +3 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/handlers.d.ts +121 -0
  22. package/dist/handlers.js +4 -0
  23. package/dist/handlers.js.map +1 -0
  24. package/dist/index.d.ts +144 -0
  25. package/dist/index.js +576 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/logger.d.ts +1 -0
  28. package/dist/logger.js +3 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/sampling.d.ts +4 -0
  31. package/dist/sampling.js +3 -0
  32. package/dist/sampling.js.map +1 -0
  33. package/dist/testing.d.ts +1 -0
  34. package/dist/testing.js +3 -0
  35. package/dist/testing.js.map +1 -0
  36. package/package.json +107 -0
  37. package/src/actors/alarms.ts +225 -0
  38. package/src/actors/index.ts +36 -0
  39. package/src/actors/instrument-actor.test.ts +179 -0
  40. package/src/actors/instrument-actor.ts +574 -0
  41. package/src/actors/sockets.ts +217 -0
  42. package/src/actors/storage.ts +263 -0
  43. package/src/actors/traced-handler.ts +300 -0
  44. package/src/actors/types.ts +98 -0
  45. package/src/actors.ts +50 -0
  46. package/src/agents/index.ts +42 -0
  47. package/src/agents/otel-observability.test.ts +329 -0
  48. package/src/agents/otel-observability.ts +465 -0
  49. package/src/agents/types.ts +167 -0
  50. package/src/agents.ts +76 -0
  51. package/src/bindings/bindings.ts +621 -0
  52. package/src/bindings/common.ts +75 -0
  53. package/src/bindings/index.ts +12 -0
  54. package/src/bindings.ts +6 -0
  55. package/src/events.ts +6 -0
  56. package/src/global/cache.test.ts +292 -0
  57. package/src/global/cache.ts +164 -0
  58. package/src/global/fetch.test.ts +344 -0
  59. package/src/global/fetch.ts +134 -0
  60. package/src/global/index.ts +7 -0
  61. package/src/handlers/durable-objects.test.ts +524 -0
  62. package/src/handlers/durable-objects.ts +250 -0
  63. package/src/handlers/index.ts +6 -0
  64. package/src/handlers/workflows.ts +318 -0
  65. package/src/handlers.ts +6 -0
  66. package/src/index.ts +57 -0
  67. package/src/logger.ts +6 -0
  68. package/src/sampling.ts +6 -0
  69. package/src/testing.ts +6 -0
  70. package/src/wrappers/index.ts +8 -0
  71. package/src/wrappers/instrument.integration.test.ts +468 -0
  72. package/src/wrappers/instrument.ts +643 -0
  73. package/src/wrappers/wrap-do.ts +34 -0
  74. package/src/wrappers/wrap-module.ts +37 -0
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Bindings instrumentation entry point
3
+ * Entry point: autotel-cloudflare/bindings
4
+ */
5
+
6
+ export * from './bindings/index';
package/src/events.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Events system entry point (re-export from autotel-edge)
3
+ * Entry point: autotel-cloudflare/events
4
+ */
5
+
6
+ export * from 'autotel-edge/events';
@@ -0,0 +1,292 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { instrumentGlobalCache } from './cache';
3
+ import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
4
+
5
+ describe('Global Cache Instrumentation', () => {
6
+ let mockTracer: any;
7
+ let mockSpan: any;
8
+ let getTracerSpy: any;
9
+ let originalCaches: typeof globalThis.caches;
10
+
11
+ beforeEach(() => {
12
+ // Save original caches
13
+ originalCaches = globalThis.caches;
14
+
15
+ mockSpan = {
16
+ spanContext: () => ({
17
+ traceId: 'test-trace-id',
18
+ spanId: 'test-span-id',
19
+ traceFlags: 1,
20
+ }),
21
+ setAttribute: vi.fn(),
22
+ setAttributes: vi.fn(),
23
+ setStatus: vi.fn(),
24
+ recordException: vi.fn(),
25
+ end: vi.fn(),
26
+ isRecording: () => true,
27
+ updateName: vi.fn(),
28
+ addEvent: vi.fn(),
29
+ };
30
+
31
+ mockTracer = {
32
+ startActiveSpan: vi.fn((name, options, fn) => {
33
+ return fn(mockSpan);
34
+ }),
35
+ };
36
+
37
+ getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
38
+
39
+ // Mock caches API
40
+ const mockCache = {
41
+ match: vi.fn(async (key) => {
42
+ // Simulate cache hit/miss
43
+ const url = key instanceof Request ? key.url : key;
44
+ if (typeof url === 'string' && url.includes('cached')) {
45
+ return new Response('cached data');
46
+ }
47
+ return undefined;
48
+ }),
49
+ put: vi.fn(async () => {}),
50
+ delete: vi.fn(async () => true),
51
+ keys: vi.fn(async () => []),
52
+ add: vi.fn(async () => {}),
53
+ addAll: vi.fn(async () => {}),
54
+ };
55
+
56
+ (globalThis as any).caches = {
57
+ default: mockCache,
58
+ open: vi.fn(async (name: string) => mockCache),
59
+ };
60
+ });
61
+
62
+ afterEach(() => {
63
+ // Restore original caches
64
+ (globalThis as any).caches = originalCaches;
65
+ getTracerSpy.mockRestore();
66
+ });
67
+
68
+ describe('instrumentGlobalCache()', () => {
69
+ it('should wrap globalThis.caches', () => {
70
+ const originalCaches = globalThis.caches;
71
+ instrumentGlobalCache();
72
+
73
+ expect(globalThis.caches).not.toBe(originalCaches);
74
+ expect(globalThis.caches.default).toBeDefined();
75
+ });
76
+
77
+ it('should instrument caches.default', async () => {
78
+ instrumentGlobalCache();
79
+
80
+ const request = new Request('https://example.com/test');
81
+ await caches.default.match(request);
82
+
83
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
84
+
85
+ const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
86
+ expect(spanName).toContain('Cache');
87
+ expect(spanName).toContain('default');
88
+ expect(spanName).toContain('match');
89
+ });
90
+ });
91
+
92
+ describe('cache.match() instrumentation', () => {
93
+ it('should create span for cache.match()', async () => {
94
+ instrumentGlobalCache();
95
+
96
+ const request = new Request('https://example.com/api/data');
97
+ await caches.default.match(request);
98
+
99
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
100
+
101
+ const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
102
+ expect(spanName).toBe('Cache default.match');
103
+ });
104
+
105
+ it('should record cache hit (result found)', async () => {
106
+ instrumentGlobalCache();
107
+
108
+ const request = new Request('https://example.com/cached/data');
109
+ const result = await caches.default.match(request);
110
+
111
+ expect(result).toBeDefined();
112
+ expect(mockSpan.setAttribute).toHaveBeenCalledWith('cache.hit', true);
113
+ });
114
+
115
+ it('should record cache miss (result not found)', async () => {
116
+ // Create a mock that explicitly returns undefined
117
+ const missCache = {
118
+ match: vi.fn(async () => undefined),
119
+ put: vi.fn(async () => {}),
120
+ delete: vi.fn(async () => true),
121
+ };
122
+
123
+ (globalThis as any).caches = {
124
+ default: missCache,
125
+ open: vi.fn(),
126
+ };
127
+
128
+ instrumentGlobalCache();
129
+
130
+ const request = new Request('https://example.com/uncached/data');
131
+ const result = await caches.default.match(request);
132
+
133
+ expect(result).toBeUndefined();
134
+ expect(mockSpan.setAttribute).toHaveBeenCalledWith('cache.hit', false);
135
+ });
136
+
137
+ it('should sanitize URL in attributes', async () => {
138
+ instrumentGlobalCache();
139
+
140
+ const request = new Request('https://example.com/api/data?secret=123&token=abc');
141
+ await caches.default.match(request);
142
+
143
+ const options = mockTracer.startActiveSpan.mock.calls[0][1];
144
+
145
+ // URL should be sanitized (query params removed)
146
+ expect(options.attributes['cache.key']).toBe('https://example.com/api/data');
147
+ expect(options.attributes['cache.key']).not.toContain('secret');
148
+ expect(options.attributes['cache.key']).not.toContain('token');
149
+ });
150
+
151
+ it('should add cache operation attributes', async () => {
152
+ instrumentGlobalCache();
153
+
154
+ const request = new Request('https://example.com/test');
155
+ await caches.default.match(request);
156
+
157
+ const options = mockTracer.startActiveSpan.mock.calls[0][1];
158
+ expect(options.kind).toBe(SpanKind.CLIENT);
159
+ expect(options.attributes['cache.name']).toBe('default');
160
+ expect(options.attributes['cache.operation']).toBe('match');
161
+ });
162
+ });
163
+
164
+ describe('cache.put() instrumentation', () => {
165
+ it('should create span for cache.put()', async () => {
166
+ instrumentGlobalCache();
167
+
168
+ const request = new Request('https://example.com/test');
169
+ const response = new Response('data');
170
+ await caches.default.put(request, response);
171
+
172
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
173
+
174
+ const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
175
+ expect(spanName).toBe('Cache default.put');
176
+ });
177
+
178
+ it('should add cache name and operation attributes', async () => {
179
+ instrumentGlobalCache();
180
+
181
+ const request = new Request('https://example.com/test');
182
+ const response = new Response('data');
183
+ await caches.default.put(request, response);
184
+
185
+ const options = mockTracer.startActiveSpan.mock.calls[0][1];
186
+ expect(options.attributes['cache.name']).toBe('default');
187
+ expect(options.attributes['cache.operation']).toBe('put');
188
+ });
189
+
190
+ it('should handle errors in cache.put()', async () => {
191
+ const errorCache = {
192
+ ...globalThis.caches.default,
193
+ put: vi.fn(async () => {
194
+ throw new Error('Cache error');
195
+ }),
196
+ };
197
+
198
+ (globalThis as any).caches = {
199
+ default: errorCache,
200
+ open: vi.fn(),
201
+ };
202
+
203
+ instrumentGlobalCache();
204
+
205
+ const request = new Request('https://example.com/test');
206
+ const response = new Response('data');
207
+
208
+ await expect(caches.default.put(request, response)).rejects.toThrow('Cache error');
209
+
210
+ expect(mockSpan.recordException).toHaveBeenCalled();
211
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({
212
+ code: SpanStatusCode.ERROR,
213
+ message: 'Cache error',
214
+ });
215
+ });
216
+ });
217
+
218
+ describe('cache.delete() instrumentation', () => {
219
+ it('should create span for cache.delete()', async () => {
220
+ instrumentGlobalCache();
221
+
222
+ const request = new Request('https://example.com/test');
223
+ await caches.default.delete(request);
224
+
225
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
226
+
227
+ const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
228
+ expect(spanName).toBe('Cache default.delete');
229
+ });
230
+
231
+ it('should add cache operation attributes for delete', async () => {
232
+ instrumentGlobalCache();
233
+
234
+ const request = new Request('https://example.com/test');
235
+ await caches.default.delete(request);
236
+
237
+ const options = mockTracer.startActiveSpan.mock.calls[0][1];
238
+ expect(options.attributes['cache.name']).toBe('default');
239
+ expect(options.attributes['cache.operation']).toBe('delete');
240
+ });
241
+ });
242
+
243
+ describe('caches.open() instrumentation', () => {
244
+ it('should wrap caches.open() to instrument named caches', async () => {
245
+ instrumentGlobalCache();
246
+
247
+ const namedCache = await caches.open('my-cache');
248
+
249
+ // After instrumentation, caches.open is wrapped
250
+ // The key is that we can still call it and get a cache back
251
+ expect(namedCache).toBeDefined();
252
+ expect(typeof namedCache.match).toBe('function');
253
+ });
254
+
255
+ it('should instrument operations on named caches', async () => {
256
+ instrumentGlobalCache();
257
+
258
+ const namedCache = await caches.open('my-custom-cache');
259
+ const request = new Request('https://example.com/test');
260
+ await namedCache.match(request);
261
+
262
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
263
+
264
+ const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
265
+ expect(spanName).toBe('Cache my-custom-cache.match');
266
+ });
267
+ });
268
+
269
+ describe('Edge cases', () => {
270
+ it('should handle string keys for match()', async () => {
271
+ instrumentGlobalCache();
272
+
273
+ // Some implementations allow string keys
274
+ await caches.default.match('https://example.com/test');
275
+
276
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
277
+
278
+ const options = mockTracer.startActiveSpan.mock.calls[0][1];
279
+ expect(options.attributes['cache.key']).toBe('https://example.com/test');
280
+ });
281
+
282
+ it('should set OK status on successful operations', async () => {
283
+ instrumentGlobalCache();
284
+
285
+ const request = new Request('https://example.com/test');
286
+ await caches.default.match(request);
287
+
288
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
289
+ expect(mockSpan.end).toHaveBeenCalled();
290
+ });
291
+ });
292
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Global Cache API instrumentation for Cloudflare Workers
3
+ *
4
+ * Automatically traces cache operations:
5
+ * - cache.match() - Read from cache
6
+ * - cache.put() - Write to cache
7
+ * - cache.delete() - Delete from cache
8
+ */
9
+
10
+ import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
11
+ import { wrap } from '../bindings/common';
12
+ import { WorkerTracer } from 'autotel-edge';
13
+
14
+ type CacheOperation = 'match' | 'put' | 'delete';
15
+
16
+ /**
17
+ * Sanitize URL for span attributes (remove query params that might contain sensitive data)
18
+ */
19
+ function sanitizeURL(url: string): string {
20
+ const u = new URL(url);
21
+ return `${u.protocol}//${u.host}${u.pathname}`;
22
+ }
23
+
24
+ /**
25
+ * Instrument a cache method (match, put, delete)
26
+ */
27
+ function instrumentCacheMethod<T extends Function>(
28
+ fn: T,
29
+ cacheName: string,
30
+ operation: CacheOperation,
31
+ ): T {
32
+ const handler: ProxyHandler<T> = {
33
+ async apply(target, thisArg, argArray) {
34
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
35
+
36
+ // Extract URL from first argument (Request or string)
37
+ const firstArg = argArray[0];
38
+ const url =
39
+ firstArg instanceof Request
40
+ ? firstArg.url
41
+ : typeof firstArg === 'string'
42
+ ? firstArg
43
+ : undefined;
44
+
45
+ const spanName = `Cache ${cacheName}.${operation}`;
46
+
47
+ return tracer.startActiveSpan(
48
+ spanName,
49
+ {
50
+ kind: SpanKind.CLIENT,
51
+ attributes: {
52
+ 'cache.name': cacheName,
53
+ 'cache.operation': operation,
54
+ 'cache.key': url ? sanitizeURL(url) : undefined,
55
+ },
56
+ },
57
+ async (span) => {
58
+ try {
59
+ const result = await Reflect.apply(target, thisArg, argArray);
60
+
61
+ // For match operations, record whether it was a hit or miss
62
+ if (operation === 'match') {
63
+ span.setAttribute('cache.hit', !!result);
64
+ }
65
+
66
+ span.setStatus({ code: SpanStatusCode.OK });
67
+ return result;
68
+ } catch (error) {
69
+ span.recordException(error as Error);
70
+ span.setStatus({
71
+ code: SpanStatusCode.ERROR,
72
+ message: error instanceof Error ? error.message : String(error),
73
+ });
74
+ throw error;
75
+ } finally {
76
+ span.end();
77
+ }
78
+ },
79
+ );
80
+ },
81
+ };
82
+
83
+ return wrap(fn, handler);
84
+ }
85
+
86
+ /**
87
+ * Instrument a Cache instance
88
+ */
89
+ function instrumentCache(cache: Cache, cacheName: string): Cache {
90
+ const handler: ProxyHandler<Cache> = {
91
+ get(target, prop) {
92
+ const value = Reflect.get(target, prop);
93
+
94
+ // Instrument the cache operation methods
95
+ if (
96
+ (prop === 'match' || prop === 'put' || prop === 'delete') &&
97
+ typeof value === 'function'
98
+ ) {
99
+ return instrumentCacheMethod(
100
+ value.bind(target),
101
+ cacheName,
102
+ prop as CacheOperation,
103
+ );
104
+ }
105
+
106
+ // Bind other methods to preserve `this` context
107
+ if (typeof value === 'function') {
108
+ return value.bind(target);
109
+ }
110
+
111
+ return value;
112
+ },
113
+ };
114
+
115
+ return wrap(cache, handler);
116
+ }
117
+
118
+ /**
119
+ * Instrument caches.open()
120
+ */
121
+ function instrumentCachesOpen(
122
+ openFn: CacheStorage['open'],
123
+ ): CacheStorage['open'] {
124
+ const handler: ProxyHandler<CacheStorage['open']> = {
125
+ async apply(target, thisArg, argArray) {
126
+ const cacheName = argArray[0];
127
+ const cache = await Reflect.apply(target, thisArg, argArray);
128
+ return instrumentCache(cache, cacheName);
129
+ },
130
+ };
131
+
132
+ return wrap(openFn, handler);
133
+ }
134
+
135
+ /**
136
+ * Instrument the global caches API
137
+ *
138
+ * This wraps globalThis.caches to automatically create spans for all cache operations.
139
+ *
140
+ * **Note:** This is called automatically when the library is initialized with
141
+ * `instrumentation.instrumentGlobalCache: true` (default).
142
+ */
143
+ export function instrumentGlobalCache(): void {
144
+ const handler: ProxyHandler<typeof caches> = {
145
+ get(target, prop) {
146
+ if (prop === 'default') {
147
+ // Wrap the default cache
148
+ return instrumentCache(target.default, 'default');
149
+ } else if (prop === 'open') {
150
+ // Wrap the open method
151
+ const openFn = Reflect.get(target, prop);
152
+ if (typeof openFn === 'function') {
153
+ return instrumentCachesOpen(openFn.bind(target));
154
+ }
155
+ }
156
+
157
+ return Reflect.get(target, prop);
158
+ },
159
+ };
160
+
161
+ // Replace global caches
162
+ // @ts-ignore - TypeScript doesn't like reassigning globalThis.caches
163
+ globalThis.caches = wrap(caches, handler);
164
+ }