autotel 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.
- package/LICENSE +21 -0
- package/README.md +1946 -0
- package/dist/chunk-2LNRY4QK.js +273 -0
- package/dist/chunk-2LNRY4QK.js.map +1 -0
- package/dist/chunk-3HENGDW2.js +587 -0
- package/dist/chunk-3HENGDW2.js.map +1 -0
- package/dist/chunk-4OAT42CA.cjs +73 -0
- package/dist/chunk-4OAT42CA.cjs.map +1 -0
- package/dist/chunk-5GWX5LFW.js +70 -0
- package/dist/chunk-5GWX5LFW.js.map +1 -0
- package/dist/chunk-5R2M36QB.js +195 -0
- package/dist/chunk-5R2M36QB.js.map +1 -0
- package/dist/chunk-5ZN622AO.js +73 -0
- package/dist/chunk-5ZN622AO.js.map +1 -0
- package/dist/chunk-77MSMAUQ.cjs +498 -0
- package/dist/chunk-77MSMAUQ.cjs.map +1 -0
- package/dist/chunk-ABPEQ6RK.cjs +596 -0
- package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
- package/dist/chunk-BWYGJKRB.js +95 -0
- package/dist/chunk-BWYGJKRB.js.map +1 -0
- package/dist/chunk-BZHG5IZ4.js +73 -0
- package/dist/chunk-BZHG5IZ4.js.map +1 -0
- package/dist/chunk-G7VZBCD6.cjs +35 -0
- package/dist/chunk-G7VZBCD6.cjs.map +1 -0
- package/dist/chunk-GVLK7YUU.cjs +30 -0
- package/dist/chunk-GVLK7YUU.cjs.map +1 -0
- package/dist/chunk-HCCXC7XG.js +205 -0
- package/dist/chunk-HCCXC7XG.js.map +1 -0
- package/dist/chunk-HE6T6FIX.cjs +203 -0
- package/dist/chunk-HE6T6FIX.cjs.map +1 -0
- package/dist/chunk-KIXWPOCO.cjs +100 -0
- package/dist/chunk-KIXWPOCO.cjs.map +1 -0
- package/dist/chunk-KVGNW3FC.js +87 -0
- package/dist/chunk-KVGNW3FC.js.map +1 -0
- package/dist/chunk-LITNXTTT.js +3 -0
- package/dist/chunk-LITNXTTT.js.map +1 -0
- package/dist/chunk-M4ANN7RL.js +114 -0
- package/dist/chunk-M4ANN7RL.js.map +1 -0
- package/dist/chunk-NC52UBR2.cjs +32 -0
- package/dist/chunk-NC52UBR2.cjs.map +1 -0
- package/dist/chunk-NHCNRQD3.cjs +212 -0
- package/dist/chunk-NHCNRQD3.cjs.map +1 -0
- package/dist/chunk-NZ72VDNY.cjs +4 -0
- package/dist/chunk-NZ72VDNY.cjs.map +1 -0
- package/dist/chunk-P6JUDYNO.js +57 -0
- package/dist/chunk-P6JUDYNO.js.map +1 -0
- package/dist/chunk-RJYY7BWX.js +1349 -0
- package/dist/chunk-RJYY7BWX.js.map +1 -0
- package/dist/chunk-TRI4V5BF.cjs +126 -0
- package/dist/chunk-TRI4V5BF.cjs.map +1 -0
- package/dist/chunk-UL33I6IS.js +139 -0
- package/dist/chunk-UL33I6IS.js.map +1 -0
- package/dist/chunk-URRW6M2C.cjs +61 -0
- package/dist/chunk-URRW6M2C.cjs.map +1 -0
- package/dist/chunk-UY3UYPBZ.cjs +77 -0
- package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
- package/dist/chunk-W3253FGB.cjs +277 -0
- package/dist/chunk-W3253FGB.cjs.map +1 -0
- package/dist/chunk-W7LHZVQF.js +26 -0
- package/dist/chunk-W7LHZVQF.js.map +1 -0
- package/dist/chunk-WBWNM6LB.cjs +1360 -0
- package/dist/chunk-WBWNM6LB.cjs.map +1 -0
- package/dist/chunk-WFJ7L2RV.js +494 -0
- package/dist/chunk-WFJ7L2RV.js.map +1 -0
- package/dist/chunk-X4RMFFMR.js +28 -0
- package/dist/chunk-X4RMFFMR.js.map +1 -0
- package/dist/chunk-Y4Y2S7BM.cjs +92 -0
- package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
- package/dist/chunk-YLPNXZFI.cjs +143 -0
- package/dist/chunk-YLPNXZFI.cjs.map +1 -0
- package/dist/chunk-YTXEZ4SD.cjs +77 -0
- package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
- package/dist/chunk-Z6ZWNWWR.js +30 -0
- package/dist/chunk-Z6ZWNWWR.js.map +1 -0
- package/dist/config.cjs +26 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +75 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -0
- package/dist/db.cjs +233 -0
- package/dist/db.cjs.map +1 -0
- package/dist/db.d.cts +123 -0
- package/dist/db.d.ts +123 -0
- package/dist/db.js +228 -0
- package/dist/db.js.map +1 -0
- package/dist/decorators.cjs +67 -0
- package/dist/decorators.cjs.map +1 -0
- package/dist/decorators.d.cts +91 -0
- package/dist/decorators.d.ts +91 -0
- package/dist/decorators.js +65 -0
- package/dist/decorators.js.map +1 -0
- package/dist/event-subscriber.cjs +6 -0
- package/dist/event-subscriber.cjs.map +1 -0
- package/dist/event-subscriber.d.cts +116 -0
- package/dist/event-subscriber.d.ts +116 -0
- package/dist/event-subscriber.js +3 -0
- package/dist/event-subscriber.js.map +1 -0
- package/dist/event-testing.cjs +21 -0
- package/dist/event-testing.cjs.map +1 -0
- package/dist/event-testing.d.cts +110 -0
- package/dist/event-testing.d.ts +110 -0
- package/dist/event-testing.js +4 -0
- package/dist/event-testing.js.map +1 -0
- package/dist/event.cjs +30 -0
- package/dist/event.cjs.map +1 -0
- package/dist/event.d.cts +282 -0
- package/dist/event.d.ts +282 -0
- package/dist/event.js +13 -0
- package/dist/event.js.map +1 -0
- package/dist/exporters.cjs +17 -0
- package/dist/exporters.cjs.map +1 -0
- package/dist/exporters.d.cts +1 -0
- package/dist/exporters.d.ts +1 -0
- package/dist/exporters.js +4 -0
- package/dist/exporters.js.map +1 -0
- package/dist/functional.cjs +46 -0
- package/dist/functional.cjs.map +1 -0
- package/dist/functional.d.cts +478 -0
- package/dist/functional.d.ts +478 -0
- package/dist/functional.js +13 -0
- package/dist/functional.js.map +1 -0
- package/dist/http.cjs +189 -0
- package/dist/http.cjs.map +1 -0
- package/dist/http.d.cts +169 -0
- package/dist/http.d.ts +169 -0
- package/dist/http.js +184 -0
- package/dist/http.js.map +1 -0
- package/dist/index.cjs +333 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +758 -0
- package/dist/index.d.ts +758 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.cjs +182 -0
- package/dist/instrumentation.cjs.map +1 -0
- package/dist/instrumentation.d.cts +49 -0
- package/dist/instrumentation.d.ts +49 -0
- package/dist/instrumentation.js +179 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/logger.cjs +19 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +146 -0
- package/dist/logger.d.ts +146 -0
- package/dist/logger.js +6 -0
- package/dist/logger.js.map +1 -0
- package/dist/metric-helpers.cjs +31 -0
- package/dist/metric-helpers.cjs.map +1 -0
- package/dist/metric-helpers.d.cts +13 -0
- package/dist/metric-helpers.d.ts +13 -0
- package/dist/metric-helpers.js +6 -0
- package/dist/metric-helpers.js.map +1 -0
- package/dist/metric-testing.cjs +21 -0
- package/dist/metric-testing.cjs.map +1 -0
- package/dist/metric-testing.d.cts +110 -0
- package/dist/metric-testing.d.ts +110 -0
- package/dist/metric-testing.js +4 -0
- package/dist/metric-testing.js.map +1 -0
- package/dist/metric.cjs +26 -0
- package/dist/metric.cjs.map +1 -0
- package/dist/metric.d.cts +240 -0
- package/dist/metric.d.ts +240 -0
- package/dist/metric.js +9 -0
- package/dist/metric.js.map +1 -0
- package/dist/processors.cjs +17 -0
- package/dist/processors.cjs.map +1 -0
- package/dist/processors.d.cts +1 -0
- package/dist/processors.d.ts +1 -0
- package/dist/processors.js +4 -0
- package/dist/processors.js.map +1 -0
- package/dist/sampling.cjs +40 -0
- package/dist/sampling.cjs.map +1 -0
- package/dist/sampling.d.cts +260 -0
- package/dist/sampling.d.ts +260 -0
- package/dist/sampling.js +7 -0
- package/dist/sampling.js.map +1 -0
- package/dist/semantic-helpers.cjs +35 -0
- package/dist/semantic-helpers.cjs.map +1 -0
- package/dist/semantic-helpers.d.cts +442 -0
- package/dist/semantic-helpers.d.ts +442 -0
- package/dist/semantic-helpers.js +14 -0
- package/dist/semantic-helpers.js.map +1 -0
- package/dist/tail-sampling-processor.cjs +13 -0
- package/dist/tail-sampling-processor.cjs.map +1 -0
- package/dist/tail-sampling-processor.d.cts +27 -0
- package/dist/tail-sampling-processor.d.ts +27 -0
- package/dist/tail-sampling-processor.js +4 -0
- package/dist/tail-sampling-processor.js.map +1 -0
- package/dist/testing.cjs +286 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +291 -0
- package/dist/testing.d.ts +291 -0
- package/dist/testing.js +263 -0
- package/dist/testing.js.map +1 -0
- package/dist/trace-context-DRZdUvVY.d.cts +181 -0
- package/dist/trace-context-DRZdUvVY.d.ts +181 -0
- package/dist/trace-helpers.cjs +54 -0
- package/dist/trace-helpers.cjs.map +1 -0
- package/dist/trace-helpers.d.cts +524 -0
- package/dist/trace-helpers.d.ts +524 -0
- package/dist/trace-helpers.js +5 -0
- package/dist/trace-helpers.js.map +1 -0
- package/dist/tracer-provider.cjs +21 -0
- package/dist/tracer-provider.cjs.map +1 -0
- package/dist/tracer-provider.d.cts +169 -0
- package/dist/tracer-provider.d.ts +169 -0
- package/dist/tracer-provider.js +4 -0
- package/dist/tracer-provider.js.map +1 -0
- package/package.json +280 -0
- package/src/baggage-span-processor.test.ts +202 -0
- package/src/baggage-span-processor.ts +98 -0
- package/src/circuit-breaker.test.ts +341 -0
- package/src/circuit-breaker.ts +184 -0
- package/src/config.test.ts +94 -0
- package/src/config.ts +169 -0
- package/src/db.test.ts +252 -0
- package/src/db.ts +447 -0
- package/src/decorators.test.ts +203 -0
- package/src/decorators.ts +188 -0
- package/src/env-config.test.ts +246 -0
- package/src/env-config.ts +158 -0
- package/src/event-queue.test.ts +222 -0
- package/src/event-queue.ts +203 -0
- package/src/event-subscriber.ts +136 -0
- package/src/event-testing.ts +197 -0
- package/src/event.test.ts +718 -0
- package/src/event.ts +556 -0
- package/src/exporters.ts +96 -0
- package/src/functional.test.ts +1059 -0
- package/src/functional.ts +2295 -0
- package/src/http.test.ts +487 -0
- package/src/http.ts +424 -0
- package/src/index.ts +158 -0
- package/src/init.customization.test.ts +210 -0
- package/src/init.integrations.test.ts +366 -0
- package/src/init.openllmetry.test.ts +282 -0
- package/src/init.protocol.test.ts +215 -0
- package/src/init.ts +1426 -0
- package/src/instrumentation.test.ts +108 -0
- package/src/instrumentation.ts +308 -0
- package/src/logger.test.ts +117 -0
- package/src/logger.ts +246 -0
- package/src/metric-helpers.ts +47 -0
- package/src/metric-testing.ts +197 -0
- package/src/metric.ts +434 -0
- package/src/metrics.test.ts +205 -0
- package/src/operation-context.ts +93 -0
- package/src/processors.ts +106 -0
- package/src/rate-limiter.test.ts +199 -0
- package/src/rate-limiter.ts +98 -0
- package/src/sampling.test.ts +513 -0
- package/src/sampling.ts +428 -0
- package/src/semantic-helpers.test.ts +311 -0
- package/src/semantic-helpers.ts +584 -0
- package/src/shutdown.test.ts +311 -0
- package/src/shutdown.ts +222 -0
- package/src/stub.integration.test.ts +361 -0
- package/src/tail-sampling-processor.test.ts +226 -0
- package/src/tail-sampling-processor.ts +51 -0
- package/src/testing.ts +670 -0
- package/src/trace-context.ts +470 -0
- package/src/trace-helpers.new.test.ts +278 -0
- package/src/trace-helpers.test.ts +242 -0
- package/src/trace-helpers.ts +690 -0
- package/src/tracer-provider.test.ts +183 -0
- package/src/tracer-provider.ts +266 -0
- package/src/track.test.ts +153 -0
- package/src/track.ts +120 -0
- package/src/validation.test.ts +306 -0
- package/src/validation.ts +239 -0
- package/src/variable-name-inference.test.ts +178 -0
- package/src/variable-name-inference.ts +242 -0
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
trace,
|
|
5
|
+
withTracing,
|
|
6
|
+
instrument,
|
|
7
|
+
ctx,
|
|
8
|
+
span,
|
|
9
|
+
withBaggage,
|
|
10
|
+
} from './functional';
|
|
11
|
+
import type { TraceContext } from './trace-helpers';
|
|
12
|
+
import type { TracingOptions } from './functional';
|
|
13
|
+
|
|
14
|
+
function traceFactory<Args extends unknown[], Return>(
|
|
15
|
+
factory: (ctx: TraceContext) => (...args: Args) => Return,
|
|
16
|
+
): (...args: Args) => Return {
|
|
17
|
+
return trace(
|
|
18
|
+
factory as (ctx: TraceContext) => (...args: Args) => Return,
|
|
19
|
+
) as unknown as (...args: Args) => Return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function traceNamedFactory<Args extends unknown[], Return>(
|
|
23
|
+
name: string,
|
|
24
|
+
factory: (ctx: TraceContext) => (...args: Args) => Return,
|
|
25
|
+
): (...args: Args) => Return {
|
|
26
|
+
return trace(
|
|
27
|
+
name,
|
|
28
|
+
factory as (ctx: TraceContext) => (...args: Args) => Return,
|
|
29
|
+
) as unknown as (...args: Args) => Return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function traceOptionsFactory<Args extends unknown[], Return>(
|
|
33
|
+
options: TracingOptions<Args, Return>,
|
|
34
|
+
factory: (ctx: TraceContext) => (...args: Args) => Return,
|
|
35
|
+
): (...args: Args) => Return {
|
|
36
|
+
return trace(
|
|
37
|
+
options,
|
|
38
|
+
factory as (ctx: TraceContext) => (...args: Args) => Return,
|
|
39
|
+
) as unknown as (...args: Args) => Return;
|
|
40
|
+
}
|
|
41
|
+
import { createTraceCollector } from './testing';
|
|
42
|
+
import { AlwaysSampler, NeverSampler } from './sampling';
|
|
43
|
+
import { init } from './init';
|
|
44
|
+
|
|
45
|
+
describe('Functional API', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
// Initialize for all tests
|
|
49
|
+
init({
|
|
50
|
+
service: 'test-service',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('span()', () => {
|
|
55
|
+
it('returns synchronous value when callback is sync', () => {
|
|
56
|
+
const result = span({ name: 'sync-span' }, () => 42);
|
|
57
|
+
expect(result).toBe(42);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns promise when callback is async', async () => {
|
|
61
|
+
const promise = span({ name: 'async-span' }, async () => 84);
|
|
62
|
+
expect(promise).toBeInstanceOf(Promise);
|
|
63
|
+
await expect(promise).resolves.toBe(84);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('trace()', () => {
|
|
68
|
+
it('does not execute sync function during instrumentation', () => {
|
|
69
|
+
let executions = 0;
|
|
70
|
+
const traced = trace(function add(a: number, b: number) {
|
|
71
|
+
executions += 1;
|
|
72
|
+
return a + b;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(executions).toBe(0);
|
|
76
|
+
const result = traced(2, 3);
|
|
77
|
+
expect(result).toBe(5);
|
|
78
|
+
expect(executions).toBe(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('detects ctx factories by parameter name', async () => {
|
|
82
|
+
const collector = createTraceCollector();
|
|
83
|
+
|
|
84
|
+
const traced = trace(
|
|
85
|
+
(_ctx: TraceContext) =>
|
|
86
|
+
async function detected(name: string) {
|
|
87
|
+
_ctx.setAttribute('user.name', name);
|
|
88
|
+
return name;
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
await traced('Alice');
|
|
93
|
+
|
|
94
|
+
const spans = collector.getSpans();
|
|
95
|
+
expect(spans).toHaveLength(1);
|
|
96
|
+
expect(spans[0]!.attributes['user.name']).toBe('Alice');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('overload 1: trace(fn)', () => {
|
|
100
|
+
it('should trace function with inferred name', async () => {
|
|
101
|
+
const collector = createTraceCollector();
|
|
102
|
+
|
|
103
|
+
const createUser = traceFactory(
|
|
104
|
+
(_ctx: TraceContext) =>
|
|
105
|
+
async function inferredName(name: string) {
|
|
106
|
+
return { id: '123', name };
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const result = await createUser('Alice');
|
|
111
|
+
|
|
112
|
+
expect(result).toEqual({ id: '123', name: 'Alice' });
|
|
113
|
+
|
|
114
|
+
const spans = collector.getSpans();
|
|
115
|
+
expect(spans).toHaveLength(1);
|
|
116
|
+
expect(spans[0]!.name).toBe('inferredName');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should infer name from const assignment for factory pattern with arrow functions', async () => {
|
|
120
|
+
const collector = createTraceCollector();
|
|
121
|
+
|
|
122
|
+
// This is the factory pattern that was producing "unknown" trace names
|
|
123
|
+
const processDocuments = traceFactory(
|
|
124
|
+
(_ctx: TraceContext) => async (data: string) => {
|
|
125
|
+
return data.toUpperCase();
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const result = await processDocuments('test');
|
|
130
|
+
|
|
131
|
+
expect(result).toBe('TEST');
|
|
132
|
+
|
|
133
|
+
const spans = collector.getSpans();
|
|
134
|
+
expect(spans).toHaveLength(1);
|
|
135
|
+
// Should infer 'processDocuments' from the const assignment, not 'unknown'
|
|
136
|
+
expect(spans[0]!.name).toBe('processDocuments');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('preserves sync return type for factory functions', () => {
|
|
140
|
+
const collector = createTraceCollector();
|
|
141
|
+
|
|
142
|
+
const add = traceFactory(
|
|
143
|
+
(ctx: TraceContext) =>
|
|
144
|
+
function addSync(a: number, b: number) {
|
|
145
|
+
expect(ctx.traceId).toBeDefined();
|
|
146
|
+
return a + b;
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const result = add(2, 3);
|
|
151
|
+
|
|
152
|
+
expect(result).toBe(5);
|
|
153
|
+
expect(result).not.toBeInstanceOf(Promise);
|
|
154
|
+
|
|
155
|
+
const spans = collector.getSpans();
|
|
156
|
+
expect(spans).toHaveLength(1);
|
|
157
|
+
expect(spans[0]!.name).toBe('addSync');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle errors correctly', async () => {
|
|
161
|
+
const collector = createTraceCollector();
|
|
162
|
+
|
|
163
|
+
const failingFn = traceFactory((_ctx: TraceContext) => async () => {
|
|
164
|
+
throw new Error('Test error');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await expect(failingFn()).rejects.toThrow('Test error');
|
|
168
|
+
|
|
169
|
+
const spans = collector.getSpans();
|
|
170
|
+
expect(spans).toHaveLength(1);
|
|
171
|
+
expect(spans[0]!.status.code).toBe(2); // ERROR
|
|
172
|
+
expect(spans[0]!.attributes['exception.message']).toBe('Test error');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('overload 2: trace(name, fn)', () => {
|
|
177
|
+
it('should use custom name', async () => {
|
|
178
|
+
const collector = createTraceCollector();
|
|
179
|
+
|
|
180
|
+
const createUser = traceNamedFactory(
|
|
181
|
+
'user.create',
|
|
182
|
+
(ctx: TraceContext) => async (name: string) => {
|
|
183
|
+
return { id: '123', name };
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
await createUser('Alice');
|
|
188
|
+
|
|
189
|
+
const spans = collector.getSpans();
|
|
190
|
+
expect(spans).toHaveLength(1);
|
|
191
|
+
expect(spans[0]!.name).toBe('user.create');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('overload 3: trace(options, fn)', () => {
|
|
196
|
+
it('should use options', async () => {
|
|
197
|
+
const collector = createTraceCollector();
|
|
198
|
+
|
|
199
|
+
const createUser = traceOptionsFactory(
|
|
200
|
+
{
|
|
201
|
+
name: 'user.create',
|
|
202
|
+
sampler: new AlwaysSampler(),
|
|
203
|
+
attributesFromArgs: ([name]) => ({ userName: name }),
|
|
204
|
+
},
|
|
205
|
+
(ctx: TraceContext) => async (name: string) => {
|
|
206
|
+
return { id: '123', name };
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
await createUser('Alice');
|
|
211
|
+
|
|
212
|
+
const spans = collector.getSpans();
|
|
213
|
+
expect(spans).toHaveLength(1);
|
|
214
|
+
expect(spans[0]!.name).toBe('user.create');
|
|
215
|
+
expect(spans[0]!.attributes['userName']).toBe('Alice');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should use serviceName to compose span name', async () => {
|
|
219
|
+
const collector = createTraceCollector();
|
|
220
|
+
|
|
221
|
+
const createUser = traceOptionsFactory(
|
|
222
|
+
{ serviceName: 'user' },
|
|
223
|
+
(ctx: TraceContext) =>
|
|
224
|
+
async function serviceNameTest(name: string) {
|
|
225
|
+
return { id: '123', name };
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await createUser('Alice');
|
|
230
|
+
|
|
231
|
+
const spans = collector.getSpans();
|
|
232
|
+
expect(spans).toHaveLength(1);
|
|
233
|
+
expect(spans[0]!.name).toBe('user.serviceNameTest');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should extract result attributes', async () => {
|
|
237
|
+
const collector = createTraceCollector();
|
|
238
|
+
|
|
239
|
+
const createUser = traceOptionsFactory(
|
|
240
|
+
{
|
|
241
|
+
name: 'user.create',
|
|
242
|
+
attributesFromResult: (result) => ({
|
|
243
|
+
userId: (result as unknown as { id: string }).id,
|
|
244
|
+
}),
|
|
245
|
+
},
|
|
246
|
+
(ctx: TraceContext) => async (name: string) => {
|
|
247
|
+
return { id: '456', name };
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
await createUser('Alice');
|
|
252
|
+
|
|
253
|
+
const spans = collector.getSpans();
|
|
254
|
+
expect(spans).toHaveLength(1);
|
|
255
|
+
expect(spans[0]!.attributes['userId']).toBe('456');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should respect NeverSampler', async () => {
|
|
259
|
+
const collector = createTraceCollector();
|
|
260
|
+
|
|
261
|
+
const createUser = traceOptionsFactory(
|
|
262
|
+
{
|
|
263
|
+
name: 'user.create',
|
|
264
|
+
sampler: new NeverSampler(),
|
|
265
|
+
},
|
|
266
|
+
(ctx: TraceContext) => async (name: string) => {
|
|
267
|
+
return { id: '123', name };
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
await createUser('Alice');
|
|
272
|
+
|
|
273
|
+
const spans = collector.getSpans();
|
|
274
|
+
expect(spans).toHaveLength(0);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('withTracing()', () => {
|
|
280
|
+
it('should create reusable wrapper', async () => {
|
|
281
|
+
const collector = createTraceCollector();
|
|
282
|
+
|
|
283
|
+
const trace = withTracing({ serviceName: 'user' });
|
|
284
|
+
|
|
285
|
+
const createUser = trace(
|
|
286
|
+
(_ctx: TraceContext) =>
|
|
287
|
+
async function reusableCreate(name: string) {
|
|
288
|
+
return { id: '123', name };
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const updateUser = trace(
|
|
293
|
+
(_ctx: TraceContext) =>
|
|
294
|
+
async function reusableUpdate(id: string, name: string) {
|
|
295
|
+
return { id, name };
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
await createUser('Alice');
|
|
300
|
+
await updateUser('123', 'Bob');
|
|
301
|
+
|
|
302
|
+
const spans = collector.getSpans();
|
|
303
|
+
expect(spans).toHaveLength(2);
|
|
304
|
+
expect(spans[0]!.name).toBe('user.reusableCreate');
|
|
305
|
+
expect(spans[1]!.name).toBe('user.reusableUpdate');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('preserves sync return values', () => {
|
|
309
|
+
const traceSync = withTracing({ name: 'math.add' });
|
|
310
|
+
const add = traceSync(
|
|
311
|
+
(_ctx: TraceContext) =>
|
|
312
|
+
function addSync(a: number, b: number) {
|
|
313
|
+
return a + b;
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const result = add(4, 5);
|
|
318
|
+
expect(result).toBe(9);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should support explicit name', async () => {
|
|
322
|
+
const collector = createTraceCollector();
|
|
323
|
+
|
|
324
|
+
const createUser = withTracing({ name: 'user.create' })(
|
|
325
|
+
(ctx: TraceContext) => async (name: string) => {
|
|
326
|
+
return { id: '123', name };
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
await createUser('Alice');
|
|
331
|
+
|
|
332
|
+
const spans = collector.getSpans();
|
|
333
|
+
expect(spans).toHaveLength(1);
|
|
334
|
+
expect(spans[0]!.name).toBe('user.create');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should handle errors', async () => {
|
|
338
|
+
const collector = createTraceCollector();
|
|
339
|
+
|
|
340
|
+
const failingFn = withTracing({ name: 'test.fail' })(
|
|
341
|
+
(ctx) => async () => {
|
|
342
|
+
throw new Error('Fail');
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
await expect(failingFn()).rejects.toThrow('Fail');
|
|
347
|
+
|
|
348
|
+
const spans = collector.getSpans();
|
|
349
|
+
expect(spans).toHaveLength(1);
|
|
350
|
+
expect(spans[0]!.status.code).toBe(2); // ERROR
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('instrument()', () => {
|
|
355
|
+
it('should instrument all functions', async () => {
|
|
356
|
+
const collector = createTraceCollector();
|
|
357
|
+
|
|
358
|
+
const userService = instrument({
|
|
359
|
+
functions: {
|
|
360
|
+
createUser: async (name: string) => {
|
|
361
|
+
return { id: '123', name };
|
|
362
|
+
},
|
|
363
|
+
updateUser: async (id: string, name: string) => {
|
|
364
|
+
return { id, name };
|
|
365
|
+
},
|
|
366
|
+
deleteUser: async (id: string) => {
|
|
367
|
+
return { id };
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
serviceName: 'user',
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await userService.createUser('Alice');
|
|
374
|
+
await userService.updateUser('123', 'Bob');
|
|
375
|
+
await userService.deleteUser('123');
|
|
376
|
+
|
|
377
|
+
const spans = collector.getSpans();
|
|
378
|
+
expect(spans).toHaveLength(3);
|
|
379
|
+
expect(spans[0]!.name).toBe('user.createUser');
|
|
380
|
+
expect(spans[1]!.name).toBe('user.updateUser');
|
|
381
|
+
expect(spans[2]!.name).toBe('user.deleteUser');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should skip functions with _ prefix by default', async () => {
|
|
385
|
+
const collector = createTraceCollector();
|
|
386
|
+
|
|
387
|
+
const service = instrument({
|
|
388
|
+
functions: {
|
|
389
|
+
publicFn: async () => 'public',
|
|
390
|
+
_privateFn: async () => 'private',
|
|
391
|
+
},
|
|
392
|
+
serviceName: 'test',
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await service.publicFn();
|
|
396
|
+
await service._privateFn();
|
|
397
|
+
|
|
398
|
+
const spans = collector.getSpans();
|
|
399
|
+
expect(spans).toHaveLength(1);
|
|
400
|
+
expect(spans[0]!.name).toBe('test.publicFn');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should support custom skip rules', async () => {
|
|
404
|
+
const collector = createTraceCollector();
|
|
405
|
+
|
|
406
|
+
const service = instrument({
|
|
407
|
+
functions: {
|
|
408
|
+
publicFn: async () => 'public',
|
|
409
|
+
testFn: async () => 'test',
|
|
410
|
+
debugFn: async () => 'debug',
|
|
411
|
+
},
|
|
412
|
+
serviceName: 'test',
|
|
413
|
+
skip: [
|
|
414
|
+
/^test/, // Skip functions starting with 'test'
|
|
415
|
+
(key) => key.includes('debug'), // Skip functions containing 'debug'
|
|
416
|
+
],
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await service.publicFn();
|
|
420
|
+
await service.testFn();
|
|
421
|
+
await service.debugFn();
|
|
422
|
+
|
|
423
|
+
const spans = collector.getSpans();
|
|
424
|
+
expect(spans).toHaveLength(1);
|
|
425
|
+
expect(spans[0]!.name).toBe('test.publicFn');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should support per-function overrides', async () => {
|
|
429
|
+
const collector = createTraceCollector();
|
|
430
|
+
|
|
431
|
+
const service = instrument({
|
|
432
|
+
functions: {
|
|
433
|
+
createUser: async (name: string) => {
|
|
434
|
+
return { id: '123', name };
|
|
435
|
+
},
|
|
436
|
+
deleteUser: async (id: string) => {
|
|
437
|
+
return { id };
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
serviceName: 'user',
|
|
441
|
+
sampler: new NeverSampler(), // Default: don't sample
|
|
442
|
+
overrides: {
|
|
443
|
+
deleteUser: {
|
|
444
|
+
sampler: new AlwaysSampler(), // Always sample deletes!
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
await service.createUser('Alice');
|
|
450
|
+
await service.deleteUser('123');
|
|
451
|
+
|
|
452
|
+
const spans = collector.getSpans();
|
|
453
|
+
expect(spans).toHaveLength(1);
|
|
454
|
+
expect(spans[0]!.name).toBe('user.deleteUser');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('should preserve function behavior', async () => {
|
|
458
|
+
const service = instrument({
|
|
459
|
+
functions: {
|
|
460
|
+
add: async (a: number, b: number) => a + b,
|
|
461
|
+
subtract: async (a: number, b: number) => a - b,
|
|
462
|
+
},
|
|
463
|
+
serviceName: 'math',
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(await service.add(5, 3)).toBe(8);
|
|
467
|
+
expect(await service.subtract(5, 3)).toBe(2);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should not wrap non-functions', () => {
|
|
471
|
+
const service = instrument({
|
|
472
|
+
functions: {
|
|
473
|
+
fn: async () => 'function',
|
|
474
|
+
value: 42,
|
|
475
|
+
obj: { nested: true },
|
|
476
|
+
},
|
|
477
|
+
serviceName: 'test',
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(typeof service.fn).toBe('function');
|
|
481
|
+
expect(service.value).toBe(42);
|
|
482
|
+
expect(service.obj).toEqual({ nested: true });
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('should preserve this context for methods that rely on it', async () => {
|
|
486
|
+
const collector = createTraceCollector();
|
|
487
|
+
|
|
488
|
+
// Service object with state on 'this'
|
|
489
|
+
const svc = {
|
|
490
|
+
prefix: 'user',
|
|
491
|
+
count: 0,
|
|
492
|
+
build: async function (id: string) {
|
|
493
|
+
return `${this.prefix}-${id}`;
|
|
494
|
+
},
|
|
495
|
+
increment: async function () {
|
|
496
|
+
this.count++;
|
|
497
|
+
return this.count;
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const instrumented = instrument({
|
|
502
|
+
functions: svc,
|
|
503
|
+
serviceName: 'svc',
|
|
504
|
+
}) as typeof svc;
|
|
505
|
+
|
|
506
|
+
// Test that this.prefix is accessible
|
|
507
|
+
const result1 = await instrumented.build('123');
|
|
508
|
+
expect(result1).toBe('user-123'); // Should not be 'undefined-123'
|
|
509
|
+
|
|
510
|
+
// Test that this.count is accessible and modifiable
|
|
511
|
+
const result2 = await instrumented.increment();
|
|
512
|
+
expect(result2).toBe(1);
|
|
513
|
+
const result3 = await instrumented.increment();
|
|
514
|
+
expect(result3).toBe(2);
|
|
515
|
+
|
|
516
|
+
const spans = collector.getSpans();
|
|
517
|
+
expect(spans).toHaveLength(3);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('should not call attributesFromArgs when sampler rejects tracing', async () => {
|
|
521
|
+
const collector = createTraceCollector();
|
|
522
|
+
|
|
523
|
+
// Mock expensive attribute extraction
|
|
524
|
+
const expensiveAttributeExtraction = vi.fn((args: unknown[]) => {
|
|
525
|
+
// Simulate expensive operation (JSON cloning, payload scrubbing, etc.)
|
|
526
|
+
return { arg0: args[0] };
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const service = instrument({
|
|
530
|
+
functions: {
|
|
531
|
+
createUser: async (name: string) => {
|
|
532
|
+
return { id: '123', name };
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
serviceName: 'user',
|
|
536
|
+
sampler: new NeverSampler(), // Never sample
|
|
537
|
+
attributesFromArgs: expensiveAttributeExtraction,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Execute function with NeverSampler
|
|
541
|
+
await service.createUser('Alice');
|
|
542
|
+
|
|
543
|
+
// attributesFromArgs should NOT be called since we're not tracing
|
|
544
|
+
expect(expensiveAttributeExtraction).not.toHaveBeenCalled();
|
|
545
|
+
|
|
546
|
+
// No spans should be created
|
|
547
|
+
const spans = collector.getSpans();
|
|
548
|
+
expect(spans).toHaveLength(0);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should call attributesFromArgs when sampler accepts tracing', async () => {
|
|
552
|
+
const collector = createTraceCollector();
|
|
553
|
+
|
|
554
|
+
// Mock attribute extraction
|
|
555
|
+
const attributeExtraction = vi.fn((args: unknown[]) => {
|
|
556
|
+
return { arg0: args[0] };
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const service = instrument({
|
|
560
|
+
functions: {
|
|
561
|
+
createUser: async (name: string) => {
|
|
562
|
+
return { id: '123', name };
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
serviceName: 'user',
|
|
566
|
+
sampler: new AlwaysSampler(), // Always sample
|
|
567
|
+
attributesFromArgs: attributeExtraction,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Execute function with AlwaysSampler
|
|
571
|
+
await service.createUser('Alice');
|
|
572
|
+
|
|
573
|
+
// attributesFromArgs SHOULD be called since we're tracing
|
|
574
|
+
// Note: args will include context as first element
|
|
575
|
+
expect(attributeExtraction).toHaveBeenCalledTimes(1);
|
|
576
|
+
expect(attributeExtraction).toHaveBeenCalledWith(
|
|
577
|
+
expect.arrayContaining(['Alice']),
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
// Span should be created with attributes
|
|
581
|
+
const spans = collector.getSpans();
|
|
582
|
+
expect(spans).toHaveLength(1);
|
|
583
|
+
expect(spans[0]!.attributes['arg0']).toBe('Alice');
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
describe('Span naming priority', () => {
|
|
588
|
+
it('should prioritize explicit name over serviceName', async () => {
|
|
589
|
+
const collector = createTraceCollector();
|
|
590
|
+
|
|
591
|
+
const fn = traceOptionsFactory(
|
|
592
|
+
{
|
|
593
|
+
name: 'explicit.name',
|
|
594
|
+
serviceName: 'ignored',
|
|
595
|
+
},
|
|
596
|
+
(ctx: TraceContext) => async () => 'result',
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
await fn();
|
|
600
|
+
|
|
601
|
+
const spans = collector.getSpans();
|
|
602
|
+
expect(spans[0]!.name).toBe('explicit.name');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should use serviceName + fnName when no explicit name', async () => {
|
|
606
|
+
const collector = createTraceCollector();
|
|
607
|
+
|
|
608
|
+
const myFunction = traceOptionsFactory(
|
|
609
|
+
{
|
|
610
|
+
serviceName: 'service',
|
|
611
|
+
},
|
|
612
|
+
(ctx: TraceContext) =>
|
|
613
|
+
async function priorityTest() {
|
|
614
|
+
return 'result';
|
|
615
|
+
},
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
await myFunction();
|
|
619
|
+
|
|
620
|
+
const spans = collector.getSpans();
|
|
621
|
+
expect(spans[0]!.name).toBe('service.priorityTest');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should fall back to inferred name', async () => {
|
|
625
|
+
const collector = createTraceCollector();
|
|
626
|
+
|
|
627
|
+
const namedFunction = traceFactory(
|
|
628
|
+
(_ctx: TraceContext) =>
|
|
629
|
+
async function fallbackName() {
|
|
630
|
+
return 'result';
|
|
631
|
+
},
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
await namedFunction();
|
|
635
|
+
|
|
636
|
+
const spans = collector.getSpans();
|
|
637
|
+
expect(spans[0]!.name).toBe('fallbackName');
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
describe('Error handling', () => {
|
|
642
|
+
it('should truncate long error messages', async () => {
|
|
643
|
+
const collector = createTraceCollector();
|
|
644
|
+
|
|
645
|
+
const longError = 'x'.repeat(600);
|
|
646
|
+
const fn = traceFactory((_ctx: TraceContext) => async () => {
|
|
647
|
+
throw new Error(longError);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
await expect(fn()).rejects.toThrow();
|
|
651
|
+
|
|
652
|
+
const spans = collector.getSpans();
|
|
653
|
+
const errorMsg = spans[0]!.attributes['exception.message'] as string;
|
|
654
|
+
expect(errorMsg.length).toBeLessThan(600);
|
|
655
|
+
expect(errorMsg).toContain('(truncated)');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('should record exception type', async () => {
|
|
659
|
+
const collector = createTraceCollector();
|
|
660
|
+
|
|
661
|
+
class CustomError extends Error {
|
|
662
|
+
constructor(message: string) {
|
|
663
|
+
super(message);
|
|
664
|
+
this.name = 'CustomError';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const fn = traceFactory((_ctx: TraceContext) => async () => {
|
|
669
|
+
throw new CustomError('Custom error');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
await expect(fn()).rejects.toThrow();
|
|
673
|
+
|
|
674
|
+
const spans = collector.getSpans();
|
|
675
|
+
expect(spans[0]!.attributes['exception.type']).toBe('CustomError');
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('should include stack trace', async () => {
|
|
679
|
+
const collector = createTraceCollector();
|
|
680
|
+
|
|
681
|
+
const fn = traceFactory((_ctx: TraceContext) => async () => {
|
|
682
|
+
throw new Error('Stack test');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await expect(fn()).rejects.toThrow();
|
|
686
|
+
|
|
687
|
+
const spans = collector.getSpans();
|
|
688
|
+
expect(spans[0]!.attributes['exception.stack']).toBeDefined();
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
describe('Type preservation', () => {
|
|
693
|
+
it('should preserve exact types', async () => {
|
|
694
|
+
interface User {
|
|
695
|
+
id: string;
|
|
696
|
+
name: string;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const createUser = traceFactory(
|
|
700
|
+
(_ctx: TraceContext) =>
|
|
701
|
+
async (name: string): Promise<User> => {
|
|
702
|
+
return { id: '123', name };
|
|
703
|
+
},
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
const result = await createUser('Alice');
|
|
707
|
+
|
|
708
|
+
// TypeScript should know result is User
|
|
709
|
+
expect(result.id).toBe('123');
|
|
710
|
+
expect(result.name).toBe('Alice');
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('should preserve argument types', async () => {
|
|
714
|
+
const fn = traceFactory(
|
|
715
|
+
(ctx: TraceContext) =>
|
|
716
|
+
async (a: number, b: string, c: { x: boolean }): Promise<void> => {
|
|
717
|
+
expect(typeof a).toBe('number');
|
|
718
|
+
expect(typeof b).toBe('string');
|
|
719
|
+
expect(typeof c.x).toBe('boolean');
|
|
720
|
+
},
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
await fn(42, 'hello', { x: true });
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
describe('ctx() helper', () => {
|
|
728
|
+
it('should return trace context when span is active', async () => {
|
|
729
|
+
const collector = createTraceCollector();
|
|
730
|
+
|
|
731
|
+
const createUser = traceFactory(
|
|
732
|
+
(_ctx: TraceContext) => async (name: string) => {
|
|
733
|
+
expect(ctx.traceId).toBeDefined();
|
|
734
|
+
expect(ctx.spanId).toBeDefined();
|
|
735
|
+
expect(ctx.correlationId).toBeDefined();
|
|
736
|
+
return { id: '123', name };
|
|
737
|
+
},
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
const result = await createUser('Alice');
|
|
741
|
+
expect(result).toEqual({ id: '123', name: 'Alice' });
|
|
742
|
+
|
|
743
|
+
const spans = collector.getSpans();
|
|
744
|
+
expect(spans).toHaveLength(1);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should provide span methods on context', async () => {
|
|
748
|
+
const collector = createTraceCollector();
|
|
749
|
+
|
|
750
|
+
const createUser = traceFactory(
|
|
751
|
+
(_ctx: TraceContext) => async (name: string) => {
|
|
752
|
+
if (ctx.traceId) {
|
|
753
|
+
ctx.setAttribute('user.name', name);
|
|
754
|
+
ctx.setAttributes({ 'user.id': '123', 'user.active': true });
|
|
755
|
+
}
|
|
756
|
+
return { id: '123', name };
|
|
757
|
+
},
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
await createUser('Alice');
|
|
761
|
+
|
|
762
|
+
const spans = collector.getSpans();
|
|
763
|
+
expect(spans).toHaveLength(1);
|
|
764
|
+
expect(spans[0]!.attributes['user.name']).toBe('Alice');
|
|
765
|
+
expect(spans[0]!.attributes['user.id']).toBe('123');
|
|
766
|
+
expect(spans[0]!.attributes['user.active']).toBe(true);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('should return undefined properties when no span is active', () => {
|
|
770
|
+
expect(ctx.traceId).toBeUndefined();
|
|
771
|
+
expect(ctx.spanId).toBeUndefined();
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('should record exceptions via context', async () => {
|
|
775
|
+
const collector = createTraceCollector();
|
|
776
|
+
|
|
777
|
+
const failingFn = traceFactory((_ctx: TraceContext) => async () => {
|
|
778
|
+
const error = new Error('Test exception');
|
|
779
|
+
if (ctx.traceId) {
|
|
780
|
+
ctx.recordException(error);
|
|
781
|
+
}
|
|
782
|
+
throw error;
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
await expect(failingFn()).rejects.toThrow('Test exception');
|
|
786
|
+
|
|
787
|
+
const spans = collector.getSpans();
|
|
788
|
+
expect(spans).toHaveLength(1);
|
|
789
|
+
expect(spans[0]!.status.code).toBe(2); // ERROR
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
describe('Immediate execution pattern', () => {
|
|
794
|
+
it('should execute async function immediately with context', async () => {
|
|
795
|
+
const collector = createTraceCollector();
|
|
796
|
+
|
|
797
|
+
const result = await trace(async (ctx: TraceContext) => {
|
|
798
|
+
ctx.setAttribute('test.key', 'value');
|
|
799
|
+
return { data: 'test' };
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
expect(result).toEqual({ data: 'test' });
|
|
803
|
+
|
|
804
|
+
const spans = collector.getSpans();
|
|
805
|
+
expect(spans).toHaveLength(1);
|
|
806
|
+
expect(spans[0]!.attributes['test.key']).toBe('value');
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('should execute sync function immediately with context', () => {
|
|
810
|
+
const collector = createTraceCollector();
|
|
811
|
+
|
|
812
|
+
const result = trace((ctx: TraceContext) => {
|
|
813
|
+
ctx.setAttribute('test.key', 'sync-value');
|
|
814
|
+
return 42;
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
expect(result).toBe(42);
|
|
818
|
+
|
|
819
|
+
const spans = collector.getSpans();
|
|
820
|
+
expect(spans).toHaveLength(1);
|
|
821
|
+
expect(spans[0]!.attributes['test.key']).toBe('sync-value');
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it('should support custom name with immediate execution', async () => {
|
|
825
|
+
const collector = createTraceCollector();
|
|
826
|
+
|
|
827
|
+
const result = await trace(
|
|
828
|
+
'custom.operation',
|
|
829
|
+
async (ctx: TraceContext) => {
|
|
830
|
+
ctx.setAttribute('operation.id', '123');
|
|
831
|
+
return 'success';
|
|
832
|
+
},
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
expect(result).toBe('success');
|
|
836
|
+
|
|
837
|
+
const spans = collector.getSpans();
|
|
838
|
+
expect(spans).toHaveLength(1);
|
|
839
|
+
expect(spans[0]!.name).toBe('custom.operation');
|
|
840
|
+
expect(spans[0]!.attributes['operation.id']).toBe('123');
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('should support options with immediate execution', async () => {
|
|
844
|
+
const collector = createTraceCollector();
|
|
845
|
+
|
|
846
|
+
const result = await trace(
|
|
847
|
+
{ name: 'options.test', withMetrics: true },
|
|
848
|
+
async (ctx: TraceContext) => {
|
|
849
|
+
ctx.setAttribute('test.option', 'enabled');
|
|
850
|
+
return 100;
|
|
851
|
+
},
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
expect(result).toBe(100);
|
|
855
|
+
|
|
856
|
+
const spans = collector.getSpans();
|
|
857
|
+
expect(spans).toHaveLength(1);
|
|
858
|
+
expect(spans[0]!.name).toBe('options.test');
|
|
859
|
+
expect(spans[0]!.attributes['test.option']).toBe('enabled');
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('should distinguish between factory and immediate execution', async () => {
|
|
863
|
+
const collector = createTraceCollector();
|
|
864
|
+
|
|
865
|
+
// Factory pattern - returns a function
|
|
866
|
+
const factory = trace((ctx: TraceContext) => async (name: string) => {
|
|
867
|
+
ctx.setAttribute('user.name', name);
|
|
868
|
+
return { name };
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// Immediate execution - returns result directly
|
|
872
|
+
const immediate = await trace(async (ctx: TraceContext) => {
|
|
873
|
+
ctx.setAttribute('immediate', true);
|
|
874
|
+
return 'done';
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
expect(typeof factory).toBe('function');
|
|
878
|
+
expect(immediate).toBe('done');
|
|
879
|
+
|
|
880
|
+
// Now call the factory
|
|
881
|
+
const factoryResult = await factory('Alice');
|
|
882
|
+
expect(factoryResult).toEqual({ name: 'Alice' });
|
|
883
|
+
|
|
884
|
+
const spans = collector.getSpans();
|
|
885
|
+
expect(spans).toHaveLength(2);
|
|
886
|
+
|
|
887
|
+
// First span is from immediate execution
|
|
888
|
+
expect(spans[0]!.attributes['immediate']).toBe(true);
|
|
889
|
+
|
|
890
|
+
// Second span is from factory call
|
|
891
|
+
expect(spans[1]!.attributes['user.name']).toBe('Alice');
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('should work with wrapper function pattern from feedback', async () => {
|
|
895
|
+
const collector = createTraceCollector();
|
|
896
|
+
|
|
897
|
+
// The exact use case from the feedback
|
|
898
|
+
function timed<T>(
|
|
899
|
+
requestId: string,
|
|
900
|
+
operation: string,
|
|
901
|
+
fn: () => Promise<T>,
|
|
902
|
+
): Promise<T> {
|
|
903
|
+
return trace(operation, async (ctx: TraceContext) => {
|
|
904
|
+
ctx.setAttributes({
|
|
905
|
+
'request.id': requestId,
|
|
906
|
+
'operation.name': operation,
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
const result = await fn();
|
|
910
|
+
return result;
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Test it
|
|
915
|
+
const mockFn = async () => {
|
|
916
|
+
return { userId: '123', status: 'active' };
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
const result = await timed('req-456', 'fetchUser', mockFn);
|
|
920
|
+
|
|
921
|
+
expect(result).toEqual({ userId: '123', status: 'active' });
|
|
922
|
+
|
|
923
|
+
const spans = collector.getSpans();
|
|
924
|
+
expect(spans).toHaveLength(1);
|
|
925
|
+
expect(spans[0]!.name).toBe('fetchUser');
|
|
926
|
+
expect(spans[0]!.attributes['request.id']).toBe('req-456');
|
|
927
|
+
expect(spans[0]!.attributes['operation.name']).toBe('fetchUser');
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
describe('baggage', () => {
|
|
932
|
+
it('should get baggage entry from context', async () => {
|
|
933
|
+
const collector = createTraceCollector();
|
|
934
|
+
const { context, propagation } = await import('@opentelemetry/api');
|
|
935
|
+
|
|
936
|
+
// Create context with baggage
|
|
937
|
+
const activeContext = context.active();
|
|
938
|
+
const baggage = propagation.createBaggage();
|
|
939
|
+
const updatedBaggage = baggage.setEntry('tenant.id', {
|
|
940
|
+
value: 'tenant-123',
|
|
941
|
+
});
|
|
942
|
+
const contextWithBaggage = propagation.setBaggage(
|
|
943
|
+
activeContext,
|
|
944
|
+
updatedBaggage,
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
await context.with(contextWithBaggage, async () => {
|
|
948
|
+
await trace((ctx) => async () => {
|
|
949
|
+
const tenantId = ctx.getBaggage('tenant.id');
|
|
950
|
+
expect(tenantId).toBe('tenant-123');
|
|
951
|
+
return 'done';
|
|
952
|
+
})();
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
expect(collector.getSpans()).toHaveLength(1);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it('withBaggage should set baggage for child spans', async () => {
|
|
959
|
+
const collector = createTraceCollector();
|
|
960
|
+
|
|
961
|
+
await trace((ctx) => async () => {
|
|
962
|
+
return await withBaggage({
|
|
963
|
+
baggage: { 'tenant.id': 'tenant-456', 'user.id': 'user-789' },
|
|
964
|
+
fn: async () => {
|
|
965
|
+
// Check baggage is available
|
|
966
|
+
expect(ctx.getBaggage('tenant.id')).toBe('tenant-456');
|
|
967
|
+
expect(ctx.getBaggage('user.id')).toBe('user-789');
|
|
968
|
+
|
|
969
|
+
// Create child span - should inherit baggage
|
|
970
|
+
await trace((childCtx) => async () => {
|
|
971
|
+
expect(childCtx.getBaggage('tenant.id')).toBe('tenant-456');
|
|
972
|
+
return 'child-done';
|
|
973
|
+
})();
|
|
974
|
+
return 'parent-done';
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
})();
|
|
978
|
+
|
|
979
|
+
const spans = collector.getSpans();
|
|
980
|
+
expect(spans).toHaveLength(2);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it('withBaggage should work with sync functions', () => {
|
|
984
|
+
let capturedBaggage: string | undefined;
|
|
985
|
+
|
|
986
|
+
trace((ctx) => () => {
|
|
987
|
+
return withBaggage({
|
|
988
|
+
baggage: { key: 'value' },
|
|
989
|
+
fn: () => {
|
|
990
|
+
capturedBaggage = ctx.getBaggage('key');
|
|
991
|
+
return 'sync-result';
|
|
992
|
+
},
|
|
993
|
+
});
|
|
994
|
+
})();
|
|
995
|
+
|
|
996
|
+
expect(capturedBaggage).toBe('value');
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('withBaggage should merge with existing baggage', async () => {
|
|
1000
|
+
const collector = createTraceCollector();
|
|
1001
|
+
const { context, propagation } = await import('@opentelemetry/api');
|
|
1002
|
+
|
|
1003
|
+
// Set initial baggage
|
|
1004
|
+
const activeContext = context.active();
|
|
1005
|
+
const baggage = propagation.createBaggage();
|
|
1006
|
+
const updatedBaggage = baggage.setEntry('existing.key', {
|
|
1007
|
+
value: 'existing-value',
|
|
1008
|
+
});
|
|
1009
|
+
const contextWithBaggage = propagation.setBaggage(
|
|
1010
|
+
activeContext,
|
|
1011
|
+
updatedBaggage,
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
await context.with(contextWithBaggage, async () => {
|
|
1015
|
+
await trace((ctx) => async () => {
|
|
1016
|
+
// New baggage should be available
|
|
1017
|
+
expect(ctx.getBaggage('new.key')).toBeUndefined(); // Not set yet
|
|
1018
|
+
|
|
1019
|
+
return await withBaggage({
|
|
1020
|
+
baggage: { 'new.key': 'new-value' },
|
|
1021
|
+
fn: async () => {
|
|
1022
|
+
// New baggage should be available
|
|
1023
|
+
expect(ctx.getBaggage('new.key')).toBe('new-value');
|
|
1024
|
+
// Existing baggage should still be available (if propagator preserves it)
|
|
1025
|
+
return 'done';
|
|
1026
|
+
},
|
|
1027
|
+
});
|
|
1028
|
+
})();
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// Only 1 span created (the outer trace)
|
|
1032
|
+
expect(collector.getSpans()).toHaveLength(1);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
it('ctx.getAllBaggage should return all baggage entries', async () => {
|
|
1036
|
+
const collector = createTraceCollector();
|
|
1037
|
+
const { context, propagation } = await import('@opentelemetry/api');
|
|
1038
|
+
|
|
1039
|
+
// Create context with multiple baggage entries
|
|
1040
|
+
const activeContext = context.active();
|
|
1041
|
+
let baggage = propagation.createBaggage();
|
|
1042
|
+
baggage = baggage.setEntry('key1', { value: 'value1' });
|
|
1043
|
+
baggage = baggage.setEntry('key2', { value: 'value2' });
|
|
1044
|
+
const contextWithBaggage = propagation.setBaggage(activeContext, baggage);
|
|
1045
|
+
|
|
1046
|
+
await context.with(contextWithBaggage, async () => {
|
|
1047
|
+
await trace((ctx) => async () => {
|
|
1048
|
+
const allBaggage = ctx.getAllBaggage();
|
|
1049
|
+
expect(allBaggage.size).toBeGreaterThanOrEqual(2);
|
|
1050
|
+
expect(allBaggage.get('key1')?.value).toBe('value1');
|
|
1051
|
+
expect(allBaggage.get('key2')?.value).toBe('value2');
|
|
1052
|
+
return 'done';
|
|
1053
|
+
})();
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
expect(collector.getSpans()).toHaveLength(1);
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
});
|