autotel-cloudflare 3.1.0 → 4.0.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 (76) hide show
  1. package/dist/agents.d.ts.map +1 -1
  2. package/dist/agents.js +7 -0
  3. package/dist/agents.js.map +1 -1
  4. package/package.json +8 -4
  5. package/src/actors/alarms.ts +0 -225
  6. package/src/actors/index.ts +0 -36
  7. package/src/actors/instrument-actor.test.ts +0 -179
  8. package/src/actors/instrument-actor.ts +0 -574
  9. package/src/actors/sockets.ts +0 -217
  10. package/src/actors/storage.ts +0 -263
  11. package/src/actors/traced-handler.ts +0 -300
  12. package/src/actors/types.ts +0 -98
  13. package/src/actors.ts +0 -50
  14. package/src/agents/agent.ts +0 -327
  15. package/src/agents/base.ts +0 -26
  16. package/src/agents/index.ts +0 -46
  17. package/src/agents/mcp.ts +0 -38
  18. package/src/agents/observability.ts +0 -145
  19. package/src/agents/otel-observability.test.ts +0 -269
  20. package/src/agents/otel-observability.ts +0 -557
  21. package/src/agents/types.ts +0 -61
  22. package/src/agents.ts +0 -87
  23. package/src/bindings/ai.test.ts +0 -170
  24. package/src/bindings/ai.ts +0 -73
  25. package/src/bindings/analytics-engine.test.ts +0 -175
  26. package/src/bindings/analytics-engine.ts +0 -78
  27. package/src/bindings/bindings-cache.test.ts +0 -80
  28. package/src/bindings/bindings-detection.test.ts +0 -235
  29. package/src/bindings/bindings-this-binding.test.ts +0 -294
  30. package/src/bindings/bindings.ts +0 -720
  31. package/src/bindings/browser-rendering.test.ts +0 -160
  32. package/src/bindings/browser-rendering.ts +0 -70
  33. package/src/bindings/common.ts +0 -84
  34. package/src/bindings/hyperdrive.test.ts +0 -176
  35. package/src/bindings/hyperdrive.ts +0 -74
  36. package/src/bindings/images.test.ts +0 -262
  37. package/src/bindings/images.ts +0 -182
  38. package/src/bindings/index.ts +0 -20
  39. package/src/bindings/queue-producer.test.ts +0 -224
  40. package/src/bindings/queue-producer.ts +0 -105
  41. package/src/bindings/rate-limiter.test.ts +0 -140
  42. package/src/bindings/rate-limiter.ts +0 -69
  43. package/src/bindings/vectorize.test.ts +0 -362
  44. package/src/bindings/vectorize.ts +0 -86
  45. package/src/bindings.ts +0 -6
  46. package/src/events.ts +0 -6
  47. package/src/execution-logger.test.ts +0 -127
  48. package/src/execution-logger.ts +0 -112
  49. package/src/global/cache.test.ts +0 -292
  50. package/src/global/cache.ts +0 -164
  51. package/src/global/fetch.test.ts +0 -399
  52. package/src/global/fetch.ts +0 -136
  53. package/src/global/index.ts +0 -7
  54. package/src/handlers/durable-objects.test.ts +0 -524
  55. package/src/handlers/durable-objects.ts +0 -250
  56. package/src/handlers/index.ts +0 -6
  57. package/src/handlers/workflows.test.ts +0 -411
  58. package/src/handlers/workflows.ts +0 -345
  59. package/src/handlers.ts +0 -6
  60. package/src/index.ts +0 -91
  61. package/src/logger.ts +0 -15
  62. package/src/native/native-tracing.test.ts +0 -54
  63. package/src/native/native-tracing.ts +0 -83
  64. package/src/native.ts +0 -12
  65. package/src/parse-error.ts +0 -1
  66. package/src/sampling.ts +0 -6
  67. package/src/testing.ts +0 -6
  68. package/src/wrappers/cf-attributes.test.ts +0 -334
  69. package/src/wrappers/define-worker-fetch.test.ts +0 -102
  70. package/src/wrappers/define-worker-fetch.ts +0 -71
  71. package/src/wrappers/index.ts +0 -13
  72. package/src/wrappers/instrument.integration.test.ts +0 -578
  73. package/src/wrappers/instrument.ts +0 -806
  74. package/src/wrappers/native-instrument.test.ts +0 -153
  75. package/src/wrappers/wrap-do.ts +0 -34
  76. package/src/wrappers/wrap-module.ts +0 -37
@@ -1,250 +0,0 @@
1
- /**
2
- * Durable Objects instrumentation for Cloudflare Workers
3
- *
4
- * Note: This file uses Cloudflare Workers types (DurableObjectId, DurableObjectState, etc.)
5
- * which are globally available via @cloudflare/workers-types when listed in tsconfig.json.
6
- * These types are devDependencies only - they're not runtime dependencies.
7
- * At runtime, Cloudflare Workers runtime provides the actual implementations.
8
- */
9
-
10
- import {
11
- trace,
12
- context as api_context,
13
- propagation,
14
- SpanStatusCode,
15
- SpanKind,
16
- } from '@opentelemetry/api';
17
- import type { ConfigurationOption } from 'autotel-edge';
18
- import { createInitialiser, setConfig, WorkerTracer } from 'autotel-edge';
19
- import { wrap } from '../bindings/common';
20
-
21
- // Durable Object types
22
- type DOFetchFn = (request: Request) => Response | Promise<Response>;
23
- type DOAlarmFn = () => void | Promise<void>;
24
-
25
- /**
26
- * Track cold starts per DO class
27
- */
28
- const coldStarts = new WeakMap<any, boolean>();
29
-
30
- function isColdStart(doClass: any): boolean {
31
- if (!coldStarts.has(doClass)) {
32
- coldStarts.set(doClass, true);
33
- return true;
34
- }
35
- return false;
36
- }
37
-
38
- /**
39
- * Instrument a Durable Object fetch method
40
- */
41
- function instrumentDOFetch(
42
- fetchFn: DOFetchFn,
43
- id: DurableObjectId,
44
- doClass: any,
45
- ): DOFetchFn {
46
- return async function instrumentedFetch(
47
- this: any,
48
- request: Request,
49
- ): Promise<Response> {
50
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
51
-
52
- // Extract parent context from request headers
53
- const parentContext = propagation.extract(
54
- api_context.active(),
55
- request.headers,
56
- );
57
-
58
- const url = new URL(request.url);
59
- const spanName = `DO ${id.name || id.toString()}: ${request.method} ${url.pathname}`;
60
-
61
- return tracer.startActiveSpan(
62
- spanName,
63
- {
64
- kind: SpanKind.SERVER,
65
- attributes: {
66
- 'http.request.method': request.method,
67
- 'url.full': request.url,
68
- 'do.id': id.toString(),
69
- 'do.id.name': id.name || '',
70
- 'faas.trigger': 'http',
71
- 'faas.coldstart': isColdStart(doClass),
72
- },
73
- },
74
- parentContext,
75
- async (span) => {
76
- try {
77
- const response = await fetchFn.call(this, request);
78
-
79
- span.setAttributes({
80
- 'http.response.status_code': response.status,
81
- });
82
-
83
- if (response.ok) {
84
- span.setStatus({ code: SpanStatusCode.OK });
85
- } else {
86
- span.setStatus({ code: SpanStatusCode.ERROR });
87
- }
88
-
89
- return response;
90
- } catch (error) {
91
- span.recordException(error as Error);
92
- span.setStatus({
93
- code: SpanStatusCode.ERROR,
94
- message: error instanceof Error ? error.message : String(error),
95
- });
96
- throw error;
97
- } finally {
98
- span.end();
99
- }
100
- },
101
- );
102
- };
103
- }
104
-
105
- /**
106
- * Instrument a Durable Object alarm method
107
- */
108
- function instrumentDOAlarm(
109
- alarmFn: DOAlarmFn,
110
- id: DurableObjectId,
111
- doClass: any,
112
- ): DOAlarmFn {
113
- return async function instrumentedAlarm(this: any): Promise<void> {
114
- const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
115
-
116
- const spanName = `DO ${id.name || id.toString()}: alarm`;
117
-
118
- return tracer.startActiveSpan(
119
- spanName,
120
- {
121
- kind: SpanKind.INTERNAL,
122
- attributes: {
123
- 'do.id': id.toString(),
124
- 'do.id.name': id.name || '',
125
- 'faas.trigger': 'timer',
126
- 'faas.coldstart': isColdStart(doClass),
127
- },
128
- },
129
- async (span) => {
130
- try {
131
- await alarmFn.call(this);
132
- span.setStatus({ code: SpanStatusCode.OK });
133
- } catch (error) {
134
- span.recordException(error as Error);
135
- span.setStatus({
136
- code: SpanStatusCode.ERROR,
137
- message: error instanceof Error ? error.message : String(error),
138
- });
139
- throw error;
140
- } finally {
141
- span.end();
142
- }
143
- },
144
- );
145
- };
146
- }
147
-
148
- /**
149
- * Instrument a Durable Object instance
150
- */
151
- function instrumentDOInstance(
152
- doInstance: any,
153
- state: DurableObjectState,
154
- _env: any,
155
- doClass: any,
156
- ): any {
157
- const instanceHandler: ProxyHandler<any> = {
158
- get(target, prop) {
159
- const value = Reflect.get(target, prop);
160
-
161
- if (prop === 'fetch' && typeof value === 'function') {
162
- return instrumentDOFetch(value.bind(target), state.id, doClass);
163
- }
164
-
165
- if (prop === 'alarm' && typeof value === 'function') {
166
- return instrumentDOAlarm(value.bind(target), state.id, doClass);
167
- }
168
-
169
- // Bind other methods to the target
170
- if (typeof value === 'function') {
171
- return value.bind(target);
172
- }
173
-
174
- return value;
175
- },
176
- };
177
-
178
- return wrap(doInstance, instanceHandler);
179
- }
180
-
181
- /**
182
- * Instrument a Durable Object class
183
- *
184
- * This wraps the DO class to automatically trace all fetch and alarm calls,
185
- * as well as initialize the telemetry configuration.
186
- *
187
- * **Usage:**
188
- * ```typescript
189
- * import { DurableObject } from 'cloudflare:workers'
190
- * import { instrumentDO } from 'autotel-edge'
191
- *
192
- * export class Counter extends DurableObject<Env> {
193
- * async fetch(request: Request) {
194
- * // Your DO logic here
195
- * return new Response('OK')
196
- * }
197
- * }
198
- *
199
- * // Wrap the class before exporting
200
- * export const CounterDO = instrumentDO(Counter, (env: Env) => ({
201
- * exporter: {
202
- * url: env.OTLP_ENDPOINT,
203
- * headers: { 'x-api-key': env.API_KEY }
204
- * },
205
- * service: {
206
- * name: 'my-durable-object',
207
- * version: '1.0.0'
208
- * }
209
- * }))
210
- * ```
211
- *
212
- * **What you get:**
213
- * - 🎯 Automatic spans for fetch() calls with HTTP attributes
214
- * - ⏰ Automatic spans for alarm() calls
215
- * - 🥶 Cold start tracking
216
- * - 🔗 Context propagation from incoming requests
217
- * - ⚡ Automatic span lifecycle management
218
- *
219
- * @param doClass - The Durable Object class to instrument
220
- * @param config - Configuration or configuration function
221
- * @returns Instrumented Durable Object class
222
- */
223
- export function instrumentDO<C extends new (state: DurableObjectState, env: any) => any>(
224
- doClass: C,
225
- config: ConfigurationOption,
226
- ): C {
227
- const initialiser = createInitialiser(config);
228
-
229
- const classHandler: ProxyHandler<C> = {
230
- construct(target, [state, env]: [DurableObjectState, any]) {
231
- // Initialize config for this DO instance
232
- const trigger = {
233
- id: state.id.toString(),
234
- name: state.id.name,
235
- };
236
- const doConfig = initialiser(env, trigger);
237
- const context = setConfig(doConfig);
238
-
239
- // Create the DO instance within the config context
240
- const doInstance = api_context.with(context, () => {
241
- return new target(state, env);
242
- });
243
-
244
- // Instrument the instance
245
- return instrumentDOInstance(doInstance, state, env, doClass);
246
- },
247
- };
248
-
249
- return wrap(doClass, classHandler);
250
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Handler instrumentation for Cloudflare Workers
3
- */
4
-
5
- export { instrumentDO } from './durable-objects';
6
- export { instrumentWorkflow } from './workflows';
@@ -1,411 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { instrumentWorkflow } from './workflows';
3
- import { getWorkflowLogger } from '../execution-logger';
4
- import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
5
-
6
- describe('Workflow Instrumentation', () => {
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
- addLink: vi.fn(),
27
- addLinks: vi.fn(),
28
- };
29
-
30
- mockTracer = {
31
- startActiveSpan: vi.fn((name, options, context, fn) => {
32
- if (typeof options === 'function') return options(mockSpan);
33
- if (typeof context === 'function') return context(mockSpan);
34
- if (typeof fn === 'function') return fn(mockSpan);
35
- return Promise.resolve();
36
- }),
37
- };
38
-
39
- getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
40
- });
41
-
42
- afterEach(() => {
43
- getTracerSpy.mockRestore();
44
- });
45
-
46
- describe('instrumentWorkflow()', () => {
47
- it('should wrap workflow class constructor', () => {
48
- class TestWorkflow {
49
- constructor(public ctx: any, public env: any) {}
50
- async run() {}
51
- }
52
-
53
- const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
54
- service: { name: 'test' },
55
- });
56
-
57
- expect(Instrumented).toBeDefined();
58
- expect(typeof Instrumented).toBe('function');
59
- });
60
-
61
- it('should create workflow instance with instrumented run()', () => {
62
- class TestWorkflow {
63
- constructor(public ctx: any, public env: any) {}
64
- async run() { return 'done'; }
65
- }
66
-
67
- const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
68
- service: { name: 'test' },
69
- });
70
-
71
- const instance = new Instrumented({}, {});
72
-
73
- expect(instance).toBeDefined();
74
- expect(typeof instance.run).toBe('function');
75
- });
76
-
77
- it('should accept static config', () => {
78
- class TestWorkflow {
79
- constructor(public ctx: any, public env: any) {}
80
- async run() {}
81
- }
82
-
83
- const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
84
- service: { name: 'test', version: '1.0.0' },
85
- exporter: { url: 'http://localhost:4318/v1/traces' },
86
- });
87
-
88
- expect(Instrumented).toBeDefined();
89
- });
90
-
91
- it('should accept config function', () => {
92
- class TestWorkflow {
93
- constructor(public ctx: any, public env: any) {}
94
- async run() {}
95
- }
96
-
97
- interface Env {
98
- OTLP_ENDPOINT: string;
99
- }
100
-
101
- const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', (env: Env) => ({
102
- service: { name: 'test' },
103
- exporter: { url: env.OTLP_ENDPOINT },
104
- }));
105
-
106
- expect(Instrumented).toBeDefined();
107
- });
108
- });
109
-
110
- describe('run() instrumentation', () => {
111
- it('should preserve run() return value', async () => {
112
- const output = { status: 'ok', orderId: 'ord-123' };
113
-
114
- class TestWorkflow {
115
- constructor(public ctx: any, public env: any) {}
116
- async run(event: any, step: any) {
117
- return output;
118
- }
119
- }
120
-
121
- const Instrumented = instrumentWorkflow(TestWorkflow, 'return-test', {
122
- service: { name: 'test' },
123
- });
124
-
125
- const instance = new Instrumented({}, {});
126
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-return' };
127
- const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
128
-
129
- await expect(instance.run(event, step)).resolves.toEqual(output);
130
- });
131
-
132
- it('should create span for run() with workflow attributes', async () => {
133
- class TestWorkflow {
134
- constructor(public ctx: any, public env: any) {}
135
- async run(event: any, step: any) {}
136
- }
137
-
138
- const Instrumented = instrumentWorkflow(TestWorkflow, 'test-workflow', {
139
- service: { name: 'test' },
140
- });
141
-
142
- const instance = new Instrumented({}, {});
143
- const event = { payload: { foo: 'bar' }, timestamp: new Date(), instanceId: 'wf-123' };
144
- const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
145
-
146
- await instance.run(event, step);
147
-
148
- expect(mockTracer.startActiveSpan).toHaveBeenCalled();
149
-
150
- const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
151
- expect(spanName).toBe('Workflow test-workflow: run');
152
-
153
- const options = mockTracer.startActiveSpan.mock.calls[0][1];
154
- expect(options.kind).toBe(SpanKind.INTERNAL);
155
- expect(options.attributes['workflow.name']).toBe('test-workflow');
156
- expect(options.attributes['workflow.instance_id']).toBe('wf-123');
157
- expect(options.attributes['faas.trigger']).toBe('workflow');
158
- });
159
-
160
- it('should track cold starts', async () => {
161
- class TestWorkflow {
162
- constructor(public ctx: any, public env: any) {}
163
- async run(event: any, step: any) {}
164
- }
165
-
166
- const Instrumented = instrumentWorkflow(TestWorkflow, 'cold-test', {
167
- service: { name: 'test' },
168
- });
169
-
170
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-1' };
171
- const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
172
-
173
- const instance1 = new Instrumented({}, {});
174
- await instance1.run(event, step);
175
- const firstOptions = mockTracer.startActiveSpan.mock.calls[0][1];
176
- expect(firstOptions.attributes['faas.coldstart']).toBe(true);
177
-
178
- const instance2 = new Instrumented({}, {});
179
- await instance2.run(event, step);
180
- const secondOptions = mockTracer.startActiveSpan.mock.calls[1][1];
181
- expect(secondOptions.attributes['faas.coldstart']).toBe(false);
182
- });
183
-
184
- it('should handle run() errors', async () => {
185
- class TestWorkflow {
186
- constructor(public ctx: any, public env: any) {}
187
- async run() {
188
- throw new Error('Workflow failed');
189
- }
190
- }
191
-
192
- const Instrumented = instrumentWorkflow(TestWorkflow, 'error-test', {
193
- service: { name: 'test' },
194
- });
195
-
196
- const instance = new Instrumented({}, {});
197
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-err' };
198
- const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
199
-
200
- await expect(instance.run(event, step)).rejects.toThrow('Workflow failed');
201
-
202
- expect(mockSpan.recordException).toHaveBeenCalled();
203
- expect(mockSpan.setStatus).toHaveBeenCalledWith({
204
- code: SpanStatusCode.ERROR,
205
- message: 'Workflow failed',
206
- });
207
- expect(mockSpan.end).toHaveBeenCalled();
208
- });
209
-
210
- it('should make getWorkflowLogger() available inside run()', async () => {
211
- const getActiveSpanSpy = vi
212
- .spyOn(trace, 'getActiveSpan')
213
- .mockReturnValue(mockSpan as any);
214
-
215
- class TestWorkflow {
216
- constructor(public ctx: any, public env: any) {}
217
- async run() {
218
- const log = getWorkflowLogger();
219
- log.set({ workflow: { id: 'wf-logger' } });
220
- log.info('workflow started', { provider: { name: 'curvepay' } });
221
- return log.emitNow({ outcome: 'running' });
222
- }
223
- }
224
-
225
- const Instrumented = instrumentWorkflow(TestWorkflow, 'logger-test', {
226
- service: { name: 'test' },
227
- });
228
-
229
- const instance = new Instrumented({}, {});
230
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-logger' };
231
- const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
232
-
233
- const snapshot = await instance.run(event, step);
234
-
235
- expect(snapshot).toMatchObject({
236
- traceId: 'test-trace-id',
237
- spanId: 'test-span-id',
238
- context: {
239
- workflow: { id: 'wf-logger' },
240
- outcome: 'running',
241
- },
242
- });
243
- expect(mockSpan.setAttributes).toHaveBeenCalledWith({
244
- 'workflow.id': 'wf-logger',
245
- });
246
- expect(mockSpan.addEvent).toHaveBeenCalledWith('log.info', {
247
- message: 'workflow started',
248
- 'provider.name': 'curvepay',
249
- });
250
-
251
- getActiveSpanSpy.mockRestore();
252
- });
253
- });
254
-
255
- describe('step.do() instrumentation', () => {
256
- it('should create span for step.do() calls', async () => {
257
- const stepDoResult = { paymentId: 'pay_123' };
258
- const mockStepDo = vi.fn().mockResolvedValue(stepDoResult);
259
-
260
- class TestWorkflow {
261
- constructor(public ctx: any, public env: any) {}
262
- async run(event: any, step: any) {
263
- return await step.do('process payment', async () => stepDoResult);
264
- }
265
- }
266
-
267
- const Instrumented = instrumentWorkflow(TestWorkflow, 'step-test', {
268
- service: { name: 'test' },
269
- });
270
-
271
- const instance = new Instrumented({}, {});
272
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-step' };
273
- const step = {
274
- do: vi.fn(async (_name: string, cb: () => Promise<any>) => cb()),
275
- sleep: vi.fn(),
276
- sleepUntil: vi.fn(),
277
- };
278
-
279
- await instance.run(event, step);
280
-
281
- // Should have spans for both run() and step.do()
282
- expect(mockTracer.startActiveSpan.mock.calls.length).toBeGreaterThanOrEqual(2);
283
-
284
- const stepSpanCall = mockTracer.startActiveSpan.mock.calls[1];
285
- expect(stepSpanCall[0]).toBe('Workflow step-test: process payment');
286
- expect(stepSpanCall[1].attributes['workflow.step.name']).toBe('process payment');
287
- expect(stepSpanCall[1].attributes['workflow.name']).toBe('step-test');
288
- });
289
-
290
- it('should handle step.do() errors', async () => {
291
- class TestWorkflow {
292
- constructor(public ctx: any, public env: any) {}
293
- async run(event: any, step: any) {
294
- await step.do('failing step', async () => {
295
- throw new Error('Step failed');
296
- });
297
- }
298
- }
299
-
300
- const Instrumented = instrumentWorkflow(TestWorkflow, 'step-err', {
301
- service: { name: 'test' },
302
- });
303
-
304
- const instance = new Instrumented({}, {});
305
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-step-err' };
306
- const step = {
307
- do: vi.fn(async (_name: string, cb: () => Promise<any>) => cb()),
308
- sleep: vi.fn(),
309
- sleepUntil: vi.fn(),
310
- };
311
-
312
- await expect(instance.run(event, step)).rejects.toThrow('Step failed');
313
-
314
- expect(mockSpan.recordException).toHaveBeenCalled();
315
- expect(mockSpan.end).toHaveBeenCalled();
316
- });
317
- });
318
-
319
- describe('step.sleep() instrumentation', () => {
320
- it('should create span for step.sleep() calls', async () => {
321
- class TestWorkflow {
322
- constructor(public ctx: any, public env: any) {}
323
- async run(event: any, step: any) {
324
- await step.sleep('wait for settlement', '2 hours');
325
- }
326
- }
327
-
328
- const Instrumented = instrumentWorkflow(TestWorkflow, 'sleep-test', {
329
- service: { name: 'test' },
330
- });
331
-
332
- const instance = new Instrumented({}, {});
333
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-sleep' };
334
- const step = {
335
- do: vi.fn(),
336
- sleep: vi.fn().mockResolvedValue(undefined),
337
- sleepUntil: vi.fn(),
338
- };
339
-
340
- await instance.run(event, step);
341
-
342
- const sleepSpanCall = mockTracer.startActiveSpan.mock.calls[1];
343
- expect(sleepSpanCall[0]).toBe('Workflow sleep-test: sleep wait for settlement');
344
- expect(sleepSpanCall[1].attributes['workflow.sleep.name']).toBe('wait for settlement');
345
- expect(sleepSpanCall[1].attributes['workflow.sleep.duration']).toBe('2 hours');
346
- expect(sleepSpanCall[1].attributes['workflow.name']).toBe('sleep-test');
347
- });
348
-
349
- it('should create span for step.sleepUntil() calls', async () => {
350
- const wakeAt = new Date('2026-04-09T10:00:00.000Z');
351
-
352
- class TestWorkflow {
353
- constructor(public ctx: any, public env: any) {}
354
- async run(event: any, step: any) {
355
- await step.sleepUntil('wait until tomorrow', wakeAt);
356
- }
357
- }
358
-
359
- const Instrumented = instrumentWorkflow(TestWorkflow, 'sleep-until-test', {
360
- service: { name: 'test' },
361
- });
362
-
363
- const instance = new Instrumented({}, {});
364
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-sleep-until' };
365
- const step = {
366
- do: vi.fn(),
367
- sleep: vi.fn(),
368
- sleepUntil: vi.fn().mockResolvedValue(undefined),
369
- };
370
-
371
- await instance.run(event, step);
372
-
373
- const sleepUntilSpanCall = mockTracer.startActiveSpan.mock.calls[1];
374
- expect(sleepUntilSpanCall[0]).toBe(
375
- 'Workflow sleep-until-test: sleepUntil wait until tomorrow',
376
- );
377
- expect(sleepUntilSpanCall[1].attributes['workflow.sleep.name']).toBe(
378
- 'wait until tomorrow',
379
- );
380
- expect(sleepUntilSpanCall[1].attributes['workflow.sleep.until']).toBe(
381
- '2026-04-09T10:00:00.000Z',
382
- );
383
- expect(sleepUntilSpanCall[1].attributes['workflow.name']).toBe(
384
- 'sleep-until-test',
385
- );
386
- });
387
- });
388
-
389
- describe('this binding', () => {
390
- it('should preserve this context in run()', async () => {
391
- class TestWorkflow {
392
- private value = 42;
393
- constructor(public ctx: any, public env: any) {}
394
- async run(event: any, step: any) {
395
- return this.value;
396
- }
397
- }
398
-
399
- const Instrumented = instrumentWorkflow(TestWorkflow, 'this-test', {
400
- service: { name: 'test' },
401
- });
402
-
403
- const instance = new Instrumented({}, {});
404
- const event = { payload: {}, timestamp: new Date(), instanceId: 'wf-this' };
405
- const step = { do: vi.fn(), sleep: vi.fn(), sleepUntil: vi.fn() };
406
-
407
- // Should not throw — this.value should be accessible
408
- await expect(instance.run(event, step)).resolves.not.toThrow();
409
- });
410
- });
411
- });