autotel-edge 3.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.
- package/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/chunk-F32WSLNX.js +309 -0
- package/dist/chunk-F32WSLNX.js.map +1 -0
- package/dist/events.d.ts +86 -0
- package/dist/events.js +157 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +326 -0
- package/dist/index.js +921 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +89 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/sampling.d.ts +166 -0
- package/dist/sampling.js +108 -0
- package/dist/sampling.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-Dj85cPUj.d.ts +182 -0
- package/package.json +88 -0
- package/src/api/logger.test.ts +367 -0
- package/src/api/logger.ts +197 -0
- package/src/compose.ts +243 -0
- package/src/core/buffer.ts +16 -0
- package/src/core/config.test.ts +388 -0
- package/src/core/config.ts +167 -0
- package/src/core/context.ts +224 -0
- package/src/core/exporter.ts +99 -0
- package/src/core/provider.ts +45 -0
- package/src/core/span.ts +222 -0
- package/src/core/spanprocessor.test.ts +521 -0
- package/src/core/spanprocessor.ts +232 -0
- package/src/core/trace-context.ts +66 -0
- package/src/core/tracer.test.ts +123 -0
- package/src/core/tracer.ts +216 -0
- package/src/events/index.test.ts +242 -0
- package/src/events/index.ts +338 -0
- package/src/events.ts +6 -0
- package/src/functional.test.ts +702 -0
- package/src/functional.ts +846 -0
- package/src/index.ts +81 -0
- package/src/logger.ts +13 -0
- package/src/sampling/index.test.ts +297 -0
- package/src/sampling/index.ts +276 -0
- package/src/sampling.ts +6 -0
- package/src/testing/index.ts +9 -0
- package/src/testing.ts +6 -0
- package/src/types.ts +267 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
3
|
+
import { trace, withTracing, instrument, span } from './functional';
|
|
4
|
+
import { trace as otelTrace, SpanStatusCode } from '@opentelemetry/api';
|
|
5
|
+
|
|
6
|
+
describe('Functional API', () => {
|
|
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, optionsOrFn, maybeFn) => {
|
|
30
|
+
const fn = typeof optionsOrFn === 'function' ? optionsOrFn : maybeFn;
|
|
31
|
+
try {
|
|
32
|
+
const result = fn(mockSpan);
|
|
33
|
+
// If it's a promise, ensure errors are properly propagated
|
|
34
|
+
if (result && typeof result.then === 'function') {
|
|
35
|
+
return result.catch((error: any) => {
|
|
36
|
+
// Re-throw to maintain error behavior but ensure it's in promise chain
|
|
37
|
+
throw error;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// Convert sync errors to rejected promises to match OTel behavior
|
|
43
|
+
return Promise.reject(error);
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
getTracerSpy = vi.spyOn(otelTrace, 'getTracer').mockReturnValue(mockTracer as any);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
getTracerSpy.mockRestore();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('trace() - Simple Usage', () => {
|
|
56
|
+
it('does not execute function during instrumentation', () => {
|
|
57
|
+
let executions = 0;
|
|
58
|
+
const traced = trace(function add(a: number, b: number) {
|
|
59
|
+
executions += 1;
|
|
60
|
+
return a + b;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(executions).toBe(0);
|
|
64
|
+
const result = traced(2, 3);
|
|
65
|
+
expect(result).toBe(5);
|
|
66
|
+
expect(executions).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should auto-name span from function name', async () => {
|
|
70
|
+
const testFunction = trace(async function createUser(email: string) {
|
|
71
|
+
return { id: '123', email };
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const result = await testFunction('test@example.com');
|
|
75
|
+
|
|
76
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
77
|
+
'createUser',
|
|
78
|
+
{},
|
|
79
|
+
expect.any(Function),
|
|
80
|
+
);
|
|
81
|
+
expect(result).toEqual({ id: '123', email: 'test@example.com' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should set span status to OK on success', async () => {
|
|
85
|
+
const testFunction = trace(async function successFunction() {
|
|
86
|
+
return 'success';
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await testFunction();
|
|
90
|
+
|
|
91
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
92
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should record exception and set error status with message on failure', async () => {
|
|
96
|
+
const error = new Error('test error');
|
|
97
|
+
const testFunction = trace(async function failingFunction() {
|
|
98
|
+
throw error;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await expect(testFunction()).rejects.toThrow('test error');
|
|
102
|
+
|
|
103
|
+
expect(mockSpan.recordException).toHaveBeenCalledWith(error);
|
|
104
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
105
|
+
code: SpanStatusCode.ERROR,
|
|
106
|
+
message: 'test error',
|
|
107
|
+
});
|
|
108
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle non-Error exceptions', async () => {
|
|
112
|
+
const testFunction = trace(async function failingFunction() {
|
|
113
|
+
throw 'string error';
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await expect(testFunction()).rejects.toBe('string error');
|
|
117
|
+
|
|
118
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
119
|
+
code: SpanStatusCode.ERROR,
|
|
120
|
+
message: 'string error',
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('should support synchronous functions', () => {
|
|
124
|
+
const testFunction = trace(function multiply(a: number, b: number) {
|
|
125
|
+
return a * b;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = testFunction(3, 4);
|
|
129
|
+
|
|
130
|
+
expect(result).toBe(12);
|
|
131
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
132
|
+
'multiply',
|
|
133
|
+
{},
|
|
134
|
+
expect.any(Function),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('trace() - Named Spans', () => {
|
|
140
|
+
it('should use custom span name', async () => {
|
|
141
|
+
const testFunction = trace('user.create', async function(email: string) {
|
|
142
|
+
return { id: '123', email };
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await testFunction('test@example.com');
|
|
146
|
+
|
|
147
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
148
|
+
'user.create',
|
|
149
|
+
{},
|
|
150
|
+
expect.any(Function),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should work with arrow functions', async () => {
|
|
155
|
+
const testFunction = trace('custom.name', async (email: string) => {
|
|
156
|
+
return { id: '123', email };
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await testFunction('test@example.com');
|
|
160
|
+
|
|
161
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
162
|
+
'custom.name',
|
|
163
|
+
{},
|
|
164
|
+
expect.any(Function),
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('trace() - Full Options', () => {
|
|
170
|
+
it('should extract attributes from arguments', async () => {
|
|
171
|
+
const testFunction = trace({
|
|
172
|
+
name: 'user.create',
|
|
173
|
+
attributesFromArgs: ([email]: [string]) => ({ 'user.email': email }),
|
|
174
|
+
}, async function(email: string) {
|
|
175
|
+
return { id: '123', email };
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await testFunction('test@example.com');
|
|
179
|
+
|
|
180
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({ 'user.email': 'test@example.com' });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should extract attributes from result', async () => {
|
|
184
|
+
const testFunction = trace({
|
|
185
|
+
name: 'user.create',
|
|
186
|
+
attributesFromResult: (user: any) => ({ 'user.id': user.id }),
|
|
187
|
+
}, async function(email: string) {
|
|
188
|
+
return { id: '123', email };
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await testFunction('test@example.com');
|
|
192
|
+
|
|
193
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({ 'user.id': '123' });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should extract attributes from both args and result', async () => {
|
|
197
|
+
const testFunction = trace({
|
|
198
|
+
name: 'user.create',
|
|
199
|
+
attributesFromArgs: ([email]: [string]) => ({ 'user.email': email }),
|
|
200
|
+
attributesFromResult: (user: any) => ({ 'user.id': user.id }),
|
|
201
|
+
}, async function(email: string) {
|
|
202
|
+
return { id: '123', email };
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await testFunction('test@example.com');
|
|
206
|
+
|
|
207
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({ 'user.email': 'test@example.com' });
|
|
208
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({ 'user.id': '123' });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should add static attributes', async () => {
|
|
212
|
+
const testFunction = trace({
|
|
213
|
+
name: 'user.create',
|
|
214
|
+
attributes: { 'service.type': 'user-management' },
|
|
215
|
+
}, async function(email: string) {
|
|
216
|
+
return { id: '123', email };
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await testFunction('test@example.com');
|
|
220
|
+
|
|
221
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({ 'service.type': 'user-management' });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should use serviceName to prefix function name', async () => {
|
|
225
|
+
const testFunction = trace({
|
|
226
|
+
serviceName: 'user',
|
|
227
|
+
}, async function createUser(email: string) {
|
|
228
|
+
return { id: '123', email };
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await testFunction('test@example.com');
|
|
232
|
+
|
|
233
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
234
|
+
'user.createUser',
|
|
235
|
+
{},
|
|
236
|
+
expect.any(Function),
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('trace() - Sampler Option', () => {
|
|
242
|
+
it('should pass sampler to startActiveSpan when provided', async () => {
|
|
243
|
+
const mockSampler = {
|
|
244
|
+
shouldSample: vi.fn(() => ({
|
|
245
|
+
decision: 1, // RECORD_AND_SAMPLED
|
|
246
|
+
attributes: {},
|
|
247
|
+
})),
|
|
248
|
+
toString: () => 'MockSampler',
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const testFunction = trace({
|
|
252
|
+
name: 'test.function',
|
|
253
|
+
sampler: mockSampler as any,
|
|
254
|
+
}, async function() {
|
|
255
|
+
return 'success';
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await testFunction();
|
|
259
|
+
|
|
260
|
+
// Verify startActiveSpan was called with options containing the sampler
|
|
261
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
262
|
+
'test.function',
|
|
263
|
+
{ sampler: mockSampler },
|
|
264
|
+
expect.any(Function),
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should NOT pass options when sampler is not provided', async () => {
|
|
269
|
+
const testFunction = trace({
|
|
270
|
+
name: 'test.function',
|
|
271
|
+
}, async function() {
|
|
272
|
+
return 'success';
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await testFunction();
|
|
276
|
+
|
|
277
|
+
// Verify startActiveSpan was called WITHOUT sampler options
|
|
278
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
279
|
+
'test.function',
|
|
280
|
+
{},
|
|
281
|
+
expect.any(Function),
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should work with custom sampler that rejects sampling', async () => {
|
|
286
|
+
const rejectSampler = {
|
|
287
|
+
shouldSample: vi.fn(() => ({
|
|
288
|
+
decision: 0, // NOT_RECORD
|
|
289
|
+
attributes: {},
|
|
290
|
+
})),
|
|
291
|
+
toString: () => 'RejectSampler',
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const testFunction = trace({
|
|
295
|
+
name: 'test.function',
|
|
296
|
+
sampler: rejectSampler as any,
|
|
297
|
+
}, async function() {
|
|
298
|
+
return 'success';
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const result = await testFunction();
|
|
302
|
+
|
|
303
|
+
expect(result).toBe('success');
|
|
304
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
305
|
+
'test.function',
|
|
306
|
+
{ sampler: rejectSampler },
|
|
307
|
+
expect.any(Function),
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should combine sampler with other options', async () => {
|
|
312
|
+
const mockSampler = {
|
|
313
|
+
shouldSample: vi.fn(() => ({
|
|
314
|
+
decision: 1,
|
|
315
|
+
attributes: {},
|
|
316
|
+
})),
|
|
317
|
+
toString: () => 'MockSampler',
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const testFunction = (trace as any)({
|
|
321
|
+
name: 'test.function',
|
|
322
|
+
sampler: mockSampler as any,
|
|
323
|
+
attributes: { 'custom.tag': 'value' },
|
|
324
|
+
attributesFromArgs: ([arg]: [string]) => ({ 'arg.value': arg }),
|
|
325
|
+
}, async function(arg: string) {
|
|
326
|
+
return 'success';
|
|
327
|
+
}) as any;
|
|
328
|
+
|
|
329
|
+
await testFunction('test-arg');
|
|
330
|
+
|
|
331
|
+
// Verify sampler is passed
|
|
332
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
333
|
+
'test.function',
|
|
334
|
+
{ sampler: mockSampler },
|
|
335
|
+
expect.any(Function),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Verify attributes are still added
|
|
339
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({ 'custom.tag': 'value' });
|
|
340
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({ 'arg.value': 'test-arg' });
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('trace() - Sampler Integration', () => {
|
|
345
|
+
it('should verify sampler is actually invoked by WorkerTracer', async () => {
|
|
346
|
+
// This test should be in an integration test file that uses real WorkerTracer
|
|
347
|
+
// For now, we document that the sampler needs to be passed through
|
|
348
|
+
// The actual sampling logic is tested in tracer.test.ts
|
|
349
|
+
expect(true).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('withTracing() - Composable Middleware', () => {
|
|
354
|
+
it('should create prefixed middleware', async () => {
|
|
355
|
+
const withUserTracing = withTracing({ serviceName: 'user' });
|
|
356
|
+
const createUserFn = withUserTracing(async function myCreateUser(email: string) {
|
|
357
|
+
return { id: '123', email };
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await createUserFn('test@example.com');
|
|
361
|
+
|
|
362
|
+
const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
|
|
363
|
+
expect(spanName).toMatch(/^user\./);
|
|
364
|
+
expect(typeof mockTracer.startActiveSpan.mock.calls[0][2]).toBe('function');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should work with multiple functions', async () => {
|
|
368
|
+
const withUserTracing = withTracing({ serviceName: 'user' });
|
|
369
|
+
|
|
370
|
+
const createUserFn = withUserTracing(async function createUserAction(email: string) {
|
|
371
|
+
return { id: '123', email };
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const updateUserFn = withUserTracing(async function updateUserAction(id: string, data: any) {
|
|
375
|
+
return { id, ...data };
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
await createUserFn('test@example.com');
|
|
379
|
+
await updateUserFn('123', { name: 'Test' });
|
|
380
|
+
|
|
381
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(2);
|
|
382
|
+
|
|
383
|
+
const firstCall = mockTracer.startActiveSpan.mock.calls[0][0];
|
|
384
|
+
const secondCall = mockTracer.startActiveSpan.mock.calls[1][0];
|
|
385
|
+
|
|
386
|
+
expect(firstCall).toMatch(/^user\./);
|
|
387
|
+
expect(secondCall).toMatch(/^user\./);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should support custom attribute extractors', async () => {
|
|
391
|
+
const withUserTracing = withTracing({
|
|
392
|
+
serviceName: 'user',
|
|
393
|
+
attributesFromArgs: ([email]: [string]) => ({ 'user.email': email }),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const createUser = withUserTracing(async function createUser(email: string) {
|
|
397
|
+
return { id: '123', email };
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await createUser('test@example.com');
|
|
401
|
+
|
|
402
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({ 'user.email': 'test@example.com' });
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('instrument() - Batch Instrumentation', () => {
|
|
407
|
+
it('should instrument multiple functions', async () => {
|
|
408
|
+
const instrumented = (instrument as any)({
|
|
409
|
+
functions: {
|
|
410
|
+
createUser: async (email: string) => ({ id: '123', email }),
|
|
411
|
+
updateUser: async (id: string, data: any) => ({ id, ...data }),
|
|
412
|
+
deleteUser: async (id: string) => ({ id }),
|
|
413
|
+
} as any,
|
|
414
|
+
serviceName: 'user',
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await instrumented.createUser('test@example.com');
|
|
418
|
+
await instrumented.updateUser('123', { name: 'Test' });
|
|
419
|
+
await instrumented.deleteUser('123');
|
|
420
|
+
|
|
421
|
+
expect(mockTracer.startActiveSpan).toHaveBeenNthCalledWith(
|
|
422
|
+
1,
|
|
423
|
+
'user.createUser',
|
|
424
|
+
{},
|
|
425
|
+
expect.any(Function),
|
|
426
|
+
);
|
|
427
|
+
expect(mockTracer.startActiveSpan).toHaveBeenNthCalledWith(
|
|
428
|
+
2,
|
|
429
|
+
'user.updateUser',
|
|
430
|
+
{},
|
|
431
|
+
expect.any(Function),
|
|
432
|
+
);
|
|
433
|
+
expect(mockTracer.startActiveSpan).toHaveBeenNthCalledWith(
|
|
434
|
+
3,
|
|
435
|
+
'user.deleteUser',
|
|
436
|
+
{},
|
|
437
|
+
expect.any(Function),
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('should skip functions based on pattern', async () => {
|
|
442
|
+
const instrumented = (instrument as any)({
|
|
443
|
+
functions: {
|
|
444
|
+
createUser: async (email: string) => ({ id: '123', email }),
|
|
445
|
+
_internal: async () => 'internal',
|
|
446
|
+
testHelper: async () => 'helper',
|
|
447
|
+
} as any,
|
|
448
|
+
serviceName: 'user',
|
|
449
|
+
skip: ['_internal', /test/],
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await instrumented.createUser('test@example.com');
|
|
453
|
+
await instrumented._internal();
|
|
454
|
+
await instrumented.testHelper();
|
|
455
|
+
|
|
456
|
+
// Only createUser should be trace
|
|
457
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledTimes(1);
|
|
458
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
459
|
+
'user.createUser',
|
|
460
|
+
{},
|
|
461
|
+
expect.any(Function),
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should support per-function overrides', async () => {
|
|
466
|
+
const instrumented = (instrument as any)({
|
|
467
|
+
functions: {
|
|
468
|
+
createUser: async (email: string) => ({ id: '123', email }),
|
|
469
|
+
updateUser: async (id: string, data: any) => ({ id, ...data }),
|
|
470
|
+
} as any,
|
|
471
|
+
serviceName: 'user',
|
|
472
|
+
overrides: {
|
|
473
|
+
updateUser: {
|
|
474
|
+
attributes: { 'operation.type': 'update' },
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
await instrumented.createUser('test@example.com');
|
|
480
|
+
await instrumented.updateUser('123', { name: 'Test' });
|
|
481
|
+
|
|
482
|
+
// Check that updateUser has the custom attribute
|
|
483
|
+
const updateUserCall = mockSpan.setAttributes.mock.calls.find(
|
|
484
|
+
(call: any) => call[0]['operation.type'] === 'update',
|
|
485
|
+
);
|
|
486
|
+
expect(updateUserCall).toBeDefined();
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('Edge Cases', () => {
|
|
491
|
+
it('should handle functions returning void', async () => {
|
|
492
|
+
const voidFunction = trace(async function logSomething() {
|
|
493
|
+
console.log('logging');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const result = await voidFunction();
|
|
497
|
+
|
|
498
|
+
expect(result).toBeUndefined();
|
|
499
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('should handle functions with no arguments', async () => {
|
|
503
|
+
const noArgsFunction = trace(async function getCurrentTime() {
|
|
504
|
+
return Date.now();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const result = await noArgsFunction();
|
|
508
|
+
|
|
509
|
+
expect(typeof result).toBe('number');
|
|
510
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should handle functions with many arguments', async () => {
|
|
514
|
+
const manyArgsFunction = trace(
|
|
515
|
+
async function complexFunction(a: number, b: string, c: boolean, d: object) {
|
|
516
|
+
return { a, b, c, d };
|
|
517
|
+
},
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const result = await manyArgsFunction(1, 'test', true, { key: 'value' });
|
|
521
|
+
|
|
522
|
+
expect(result).toEqual({ a: 1, b: 'test', c: true, d: { key: 'value' } });
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should handle functions returning promises', async () => {
|
|
526
|
+
const promiseFunction = trace(async function getDataAsync() {
|
|
527
|
+
return { data: 'test' };
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const result = await promiseFunction();
|
|
531
|
+
|
|
532
|
+
expect(result).toEqual({ data: 'test' });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should handle rejected promises', async () => {
|
|
536
|
+
const rejectingFunction = trace(async function rejectAsync() {
|
|
537
|
+
throw new Error('async error');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await expect(rejectingFunction()).rejects.toThrow('async error');
|
|
541
|
+
|
|
542
|
+
expect(mockSpan.recordException).toHaveBeenCalled();
|
|
543
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
544
|
+
code: SpanStatusCode.ERROR,
|
|
545
|
+
message: 'async error',
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('should use "unknown" as span name for anonymous functions without explicit name', async () => {
|
|
550
|
+
const anonymousFunction = trace(async () => {
|
|
551
|
+
return 'result';
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
await anonymousFunction();
|
|
555
|
+
|
|
556
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
557
|
+
'unknown',
|
|
558
|
+
{},
|
|
559
|
+
expect.any(Function),
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should set code.function in trace context for named functions', async () => {
|
|
564
|
+
const createUser = trace(async function createUser(name: string) {
|
|
565
|
+
return { name };
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
await createUser('Alice');
|
|
569
|
+
|
|
570
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith(
|
|
571
|
+
'code.function',
|
|
572
|
+
expect.stringMatching(/^createUser/),
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should support span helper for async code blocks', async () => {
|
|
577
|
+
const result = await span({ name: 'child', attributes: { level: 1 } }, async (childSpan) => {
|
|
578
|
+
childSpan.setAttribute('test', true);
|
|
579
|
+
return 42;
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(result).toBe(42);
|
|
583
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('test', true);
|
|
584
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should support span helper for synchronous code blocks', () => {
|
|
588
|
+
const value = span({ name: 'sync-child', attributes: { level: 2 } }, () => 7);
|
|
589
|
+
|
|
590
|
+
expect(value).toBe(7);
|
|
591
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe('Immediate execution pattern', () => {
|
|
596
|
+
it('should execute async function immediately with context', async () => {
|
|
597
|
+
const result = await trace(async (ctx: any) => {
|
|
598
|
+
ctx.setAttribute('test.key', 'value');
|
|
599
|
+
return { data: 'test' };
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
expect(result).toEqual({ data: 'test' });
|
|
603
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('test.key', 'value');
|
|
604
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
605
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('should execute sync function immediately with context', () => {
|
|
609
|
+
const result = trace((ctx: any) => {
|
|
610
|
+
ctx.setAttribute('test.key', 'sync-value');
|
|
611
|
+
return 42;
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
expect(result).toBe(42);
|
|
615
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('test.key', 'sync-value');
|
|
616
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
|
|
617
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should support custom name with immediate execution', async () => {
|
|
621
|
+
const result = await trace('custom.operation', async (ctx: any) => {
|
|
622
|
+
ctx.setAttribute('operation.id', '123');
|
|
623
|
+
return 'success';
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
expect(result).toBe('success');
|
|
627
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
628
|
+
'custom.operation',
|
|
629
|
+
expect.any(Function),
|
|
630
|
+
);
|
|
631
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('operation.id', '123');
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should support options with immediate execution', async () => {
|
|
635
|
+
const result = await trace(
|
|
636
|
+
{ name: 'options.test', attributes: { test: 'enabled' } },
|
|
637
|
+
async (ctx: any) => {
|
|
638
|
+
ctx.setAttribute('test.option', 'enabled');
|
|
639
|
+
return 100;
|
|
640
|
+
},
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
expect(result).toBe(100);
|
|
644
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
645
|
+
'options.test',
|
|
646
|
+
expect.any(Function),
|
|
647
|
+
);
|
|
648
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('test.option', 'enabled');
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('should distinguish between factory and immediate execution', async () => {
|
|
652
|
+
// Factory pattern - returns a function
|
|
653
|
+
const factory = trace((ctx: any) => async (name: string) => {
|
|
654
|
+
ctx.setAttribute('user.name', name);
|
|
655
|
+
return { name };
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Immediate execution - returns result directly
|
|
659
|
+
const immediate = await trace(async (ctx: any) => {
|
|
660
|
+
ctx.setAttribute('immediate', true);
|
|
661
|
+
return 'done';
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
expect(typeof factory).toBe('function');
|
|
665
|
+
expect(immediate).toBe('done');
|
|
666
|
+
|
|
667
|
+
// Now call the factory
|
|
668
|
+
const factoryResult = await factory('Alice');
|
|
669
|
+
expect(factoryResult).toEqual({ name: 'Alice' });
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('should work with wrapper function pattern from feedback', async () => {
|
|
673
|
+
// The exact use case from the feedback
|
|
674
|
+
function timed<T>(
|
|
675
|
+
requestId: string,
|
|
676
|
+
operation: string,
|
|
677
|
+
fn: () => Promise<T>,
|
|
678
|
+
): Promise<T> {
|
|
679
|
+
return trace(operation, async (ctx: any) => {
|
|
680
|
+
ctx.setAttribute('request.id', requestId);
|
|
681
|
+
ctx.setAttribute('operation.name', operation);
|
|
682
|
+
return await fn();
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Test it
|
|
687
|
+
const mockFn = async () => {
|
|
688
|
+
return { userId: '123', status: 'active' };
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const result = await timed('req-456', 'fetchUser', mockFn);
|
|
692
|
+
|
|
693
|
+
expect(result).toEqual({ userId: '123', status: 'active' });
|
|
694
|
+
expect(mockTracer.startActiveSpan).toHaveBeenCalledWith(
|
|
695
|
+
'fetchUser',
|
|
696
|
+
expect.any(Function),
|
|
697
|
+
);
|
|
698
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('request.id', 'req-456');
|
|
699
|
+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('operation.name', 'fetchUser');
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
});
|