autotel 4.1.0 → 4.2.1

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 (253) hide show
  1. package/dist/auto.cjs +5 -3
  2. package/dist/auto.cjs.map +1 -1
  3. package/dist/auto.js +3 -3
  4. package/dist/auto.js.map +1 -1
  5. package/dist/chunk-C_NdSu1c.cjs +34 -0
  6. package/dist/correlation-id.cjs +1 -1
  7. package/dist/correlation-id.d.cts.map +1 -1
  8. package/dist/correlation-id.d.ts.map +1 -1
  9. package/dist/correlation-id.js +1 -1
  10. package/dist/decorators.cjs +1 -1
  11. package/dist/decorators.js +1 -1
  12. package/dist/{event-ByBTV9M2.js → event-531asIM6.js} +4 -4
  13. package/dist/{event-ByBTV9M2.js.map → event-531asIM6.js.map} +1 -1
  14. package/dist/{event-BhHREDJk.cjs → event-CcZYwp50.cjs} +4 -4
  15. package/dist/{event-BhHREDJk.cjs.map → event-CcZYwp50.cjs.map} +1 -1
  16. package/dist/event.cjs +1 -1
  17. package/dist/event.js +1 -1
  18. package/dist/{functional-zpzNLhky.cjs → functional-C8B0Qa7o.cjs} +10 -7
  19. package/dist/functional-C8B0Qa7o.cjs.map +1 -0
  20. package/dist/{functional-DtI0u4vx.js → functional-r-AUIRy_.js} +9 -9
  21. package/dist/functional-r-AUIRy_.js.map +1 -0
  22. package/dist/functional.cjs +1 -1
  23. package/dist/functional.js +1 -1
  24. package/dist/http.cjs +1 -1
  25. package/dist/http.js +1 -1
  26. package/dist/index.cjs +15 -13
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/{init-D-jnNMix.js → init-BS2JVkrL.js} +2 -2
  33. package/dist/{init-D-jnNMix.js.map → init-BS2JVkrL.js.map} +1 -1
  34. package/dist/{init-BX7AmFRl.cjs → init-BXiuPK6j.cjs} +3 -3
  35. package/dist/{init-BX7AmFRl.cjs.map → init-BXiuPK6j.cjs.map} +1 -1
  36. package/dist/instrumentation.cjs +2 -2
  37. package/dist/instrumentation.js +2 -2
  38. package/dist/logger.cjs +236 -8
  39. package/dist/logger.cjs.map +1 -0
  40. package/dist/messaging.cjs +1 -1
  41. package/dist/messaging.js +1 -1
  42. package/dist/{node-require-DF5QBX6z.cjs → node-require-CZ_PU448.cjs} +6 -4
  43. package/dist/node-require-CZ_PU448.cjs.map +1 -0
  44. package/dist/{node-require-Db1oDpLj.js → node-require-vROmTeJ8.js} +5 -5
  45. package/dist/node-require-vROmTeJ8.js.map +1 -0
  46. package/dist/{operation-context-C-2hmmtP.js → operation-context-CKBoA4Qy.js} +3 -3
  47. package/dist/operation-context-CKBoA4Qy.js.map +1 -0
  48. package/dist/{operation-context-n4_obUwq.cjs → operation-context-D6LDf4W_.cjs} +3 -1
  49. package/dist/operation-context-D6LDf4W_.cjs.map +1 -0
  50. package/dist/register.cjs +3 -1
  51. package/dist/register.cjs.map +1 -1
  52. package/dist/register.js +2 -2
  53. package/dist/register.js.map +1 -1
  54. package/dist/semantic-helpers.cjs +1 -1
  55. package/dist/semantic-helpers.js +1 -1
  56. package/dist/{stable-hash-Cg5cT34Q.js → stable-hash-ChFBIhNt.js} +3 -3
  57. package/dist/stable-hash-ChFBIhNt.js.map +1 -0
  58. package/dist/{stable-hash-BNTMrmdB.cjs → stable-hash-brKISGf1.cjs} +4 -2
  59. package/dist/stable-hash-brKISGf1.cjs.map +1 -0
  60. package/dist/trace-context-Cijqoi6e.d.cts.map +1 -1
  61. package/dist/trace-context-Cijqoi6e.d.ts.map +1 -1
  62. package/dist/trace-helpers.cjs +1 -1
  63. package/dist/trace-helpers.js +1 -1
  64. package/dist/{track-wc0HafS_.js → track-COUuU48p.js} +5 -5
  65. package/dist/track-COUuU48p.js.map +1 -0
  66. package/dist/{track-D59FfpL0.cjs → track-Cb3Q4QmS.cjs} +4 -2
  67. package/dist/track-Cb3Q4QmS.cjs.map +1 -0
  68. package/dist/validate.cjs +1 -1
  69. package/dist/validate.js +1 -1
  70. package/dist/webhook.cjs +1 -1
  71. package/dist/webhook.js +1 -1
  72. package/dist/workflow-distributed.cjs +1 -1
  73. package/dist/workflow-distributed.js +1 -1
  74. package/dist/workflow.cjs +3 -1
  75. package/dist/workflow.cjs.map +1 -1
  76. package/dist/workflow.d.cts.map +1 -1
  77. package/dist/workflow.d.ts.map +1 -1
  78. package/dist/workflow.js +3 -3
  79. package/dist/workflow.js.map +1 -1
  80. package/dist/yaml-config.cjs +233 -4
  81. package/dist/yaml-config.cjs.map +1 -0
  82. package/dist/yaml-config.d.cts.map +1 -1
  83. package/dist/yaml-config.d.ts.map +1 -1
  84. package/dist/yaml-config.js +8 -7
  85. package/dist/yaml-config.js.map +1 -1
  86. package/package.json +1 -2
  87. package/dist/functional-DtI0u4vx.js.map +0 -1
  88. package/dist/functional-zpzNLhky.cjs.map +0 -1
  89. package/dist/logger-thMPLpOG.cjs +0 -487
  90. package/dist/logger-thMPLpOG.cjs.map +0 -1
  91. package/dist/node-require-DF5QBX6z.cjs.map +0 -1
  92. package/dist/node-require-Db1oDpLj.js.map +0 -1
  93. package/dist/operation-context-C-2hmmtP.js.map +0 -1
  94. package/dist/operation-context-n4_obUwq.cjs.map +0 -1
  95. package/dist/stable-hash-BNTMrmdB.cjs.map +0 -1
  96. package/dist/stable-hash-Cg5cT34Q.js.map +0 -1
  97. package/dist/track-D59FfpL0.cjs.map +0 -1
  98. package/dist/track-wc0HafS_.js.map +0 -1
  99. package/dist/yaml-config-Ck2uB0Dp.cjs +0 -273
  100. package/dist/yaml-config-Ck2uB0Dp.cjs.map +0 -1
  101. package/src/attribute-redacting-processor.test.ts +0 -763
  102. package/src/attribute-redacting-processor.ts +0 -621
  103. package/src/attributes/attachers.ts +0 -161
  104. package/src/attributes/builders.ts +0 -529
  105. package/src/attributes/domains.ts +0 -42
  106. package/src/attributes/index.ts +0 -81
  107. package/src/attributes/registry.ts +0 -323
  108. package/src/attributes/types.ts +0 -211
  109. package/src/attributes/utils.ts +0 -64
  110. package/src/attributes/validators.ts +0 -266
  111. package/src/attributes.test.ts +0 -292
  112. package/src/auto.ts +0 -67
  113. package/src/autotel-logger.test.ts +0 -548
  114. package/src/autotel-logger.ts +0 -364
  115. package/src/baggage-span-processor.test.ts +0 -202
  116. package/src/baggage-span-processor.ts +0 -100
  117. package/src/business-baggage.test.ts +0 -500
  118. package/src/business-baggage.ts +0 -669
  119. package/src/circuit-breaker.test.ts +0 -341
  120. package/src/circuit-breaker.ts +0 -184
  121. package/src/config.test.ts +0 -94
  122. package/src/config.ts +0 -172
  123. package/src/correlated-events.test.ts +0 -151
  124. package/src/correlated-events.ts +0 -47
  125. package/src/correlation-id.test.ts +0 -163
  126. package/src/correlation-id.ts +0 -206
  127. package/src/db.test.ts +0 -252
  128. package/src/db.ts +0 -447
  129. package/src/decorators.test.ts +0 -153
  130. package/src/decorators.ts +0 -188
  131. package/src/define-event.test.ts +0 -41
  132. package/src/define-event.ts +0 -58
  133. package/src/devtools.ts +0 -60
  134. package/src/drain-pipeline.test.ts +0 -68
  135. package/src/drain-pipeline.ts +0 -199
  136. package/src/drain-toolkit.test.ts +0 -113
  137. package/src/drain-toolkit.ts +0 -129
  138. package/src/enricher-toolkit.test.ts +0 -67
  139. package/src/enricher-toolkit.ts +0 -79
  140. package/src/enrichers.test.ts +0 -150
  141. package/src/enrichers.ts +0 -145
  142. package/src/env-config.test.ts +0 -323
  143. package/src/env-config.ts +0 -309
  144. package/src/error-catalog.test.ts +0 -133
  145. package/src/error-catalog.ts +0 -262
  146. package/src/event-queue.test.ts +0 -864
  147. package/src/event-queue.ts +0 -699
  148. package/src/event-subscriber.ts +0 -262
  149. package/src/event-testing.ts +0 -197
  150. package/src/event.test.ts +0 -1104
  151. package/src/event.ts +0 -988
  152. package/src/events-config.ts +0 -235
  153. package/src/exporters.ts +0 -165
  154. package/src/filtering-span-processor.test.ts +0 -281
  155. package/src/filtering-span-processor.ts +0 -111
  156. package/src/flatten-attributes.test.ts +0 -76
  157. package/src/flatten-attributes.ts +0 -80
  158. package/src/functional.strict-types.typecheck.ts +0 -53
  159. package/src/functional.test.ts +0 -1464
  160. package/src/functional.ts +0 -2539
  161. package/src/functional.types.test.ts +0 -135
  162. package/src/hook.mjs +0 -15
  163. package/src/http.test.ts +0 -485
  164. package/src/http.ts +0 -424
  165. package/src/index.ts +0 -433
  166. package/src/init-auto-redactor.test.ts +0 -53
  167. package/src/init-redactor.test.ts +0 -8
  168. package/src/init.customization.test.ts +0 -665
  169. package/src/init.integrations.test.ts +0 -399
  170. package/src/init.openllmetry.test.ts +0 -194
  171. package/src/init.protocol.test.ts +0 -215
  172. package/src/init.ts +0 -2439
  173. package/src/instrumentation.test.ts +0 -108
  174. package/src/instrumentation.ts +0 -319
  175. package/src/logger.test.ts +0 -125
  176. package/src/logger.ts +0 -341
  177. package/src/messaging-adapters.test.ts +0 -595
  178. package/src/messaging-adapters.ts +0 -583
  179. package/src/messaging-testing.test.ts +0 -573
  180. package/src/messaging-testing.ts +0 -935
  181. package/src/messaging.test.ts +0 -1646
  182. package/src/messaging.ts +0 -2245
  183. package/src/metric-helpers.ts +0 -47
  184. package/src/metric-testing.ts +0 -197
  185. package/src/metric.ts +0 -446
  186. package/src/metrics.test.ts +0 -241
  187. package/src/node-require.ts +0 -123
  188. package/src/operation-context.ts +0 -93
  189. package/src/parse-error.test.ts +0 -73
  190. package/src/parse-error.ts +0 -112
  191. package/src/posthog-logs.test.ts +0 -115
  192. package/src/posthog-logs.ts +0 -77
  193. package/src/pretty-console-exporter.test.ts +0 -545
  194. package/src/pretty-console-exporter.ts +0 -413
  195. package/src/pretty-log-formatter.test.ts +0 -123
  196. package/src/pretty-log-formatter.ts +0 -210
  197. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  198. package/src/processors/canonical-log-line-processor.ts +0 -396
  199. package/src/processors.ts +0 -152
  200. package/src/rate-limiter.test.ts +0 -199
  201. package/src/rate-limiter.ts +0 -98
  202. package/src/redact-values.test.ts +0 -90
  203. package/src/redact-values.ts +0 -34
  204. package/src/register.ts +0 -37
  205. package/src/request-logger.test.ts +0 -545
  206. package/src/request-logger.ts +0 -342
  207. package/src/sampling.test.ts +0 -1060
  208. package/src/sampling.ts +0 -737
  209. package/src/security-schema.test.ts +0 -45
  210. package/src/security-schema.ts +0 -107
  211. package/src/semantic-conventions.ts +0 -15
  212. package/src/semantic-helpers.test.ts +0 -226
  213. package/src/semantic-helpers.ts +0 -438
  214. package/src/shutdown.test.ts +0 -364
  215. package/src/shutdown.ts +0 -246
  216. package/src/span-name-normalizer.test.ts +0 -377
  217. package/src/span-name-normalizer.ts +0 -213
  218. package/src/stable-hash.ts +0 -27
  219. package/src/structured-error.test.ts +0 -191
  220. package/src/structured-error.ts +0 -157
  221. package/src/stub.integration.test.ts +0 -361
  222. package/src/tail-sampling-processor.test.ts +0 -230
  223. package/src/tail-sampling-processor.ts +0 -55
  224. package/src/test-span-collector.test.ts +0 -234
  225. package/src/test-span-collector.ts +0 -150
  226. package/src/testing.ts +0 -705
  227. package/src/trace-context.test.ts +0 -73
  228. package/src/trace-context.ts +0 -567
  229. package/src/trace-helpers.new.test.ts +0 -278
  230. package/src/trace-helpers.test.ts +0 -290
  231. package/src/trace-helpers.ts +0 -710
  232. package/src/trace-hybrid.test.ts +0 -42
  233. package/src/trace-hybrid.ts +0 -37
  234. package/src/tracer-provider.test.ts +0 -183
  235. package/src/tracer-provider.ts +0 -266
  236. package/src/track.test.ts +0 -154
  237. package/src/track.ts +0 -216
  238. package/src/validate.test.ts +0 -287
  239. package/src/validate.ts +0 -307
  240. package/src/validation-attributes.ts +0 -43
  241. package/src/validation.test.ts +0 -330
  242. package/src/validation.ts +0 -246
  243. package/src/variable-name-inference.test.ts +0 -178
  244. package/src/variable-name-inference.ts +0 -242
  245. package/src/webhook.test.ts +0 -649
  246. package/src/webhook.ts +0 -637
  247. package/src/workflow-distributed.test.ts +0 -786
  248. package/src/workflow-distributed.ts +0 -916
  249. package/src/workflow.async-safety.integration.test.ts +0 -345
  250. package/src/workflow.test.ts +0 -647
  251. package/src/workflow.ts +0 -810
  252. package/src/yaml-config.test.ts +0 -373
  253. package/src/yaml-config.ts +0 -351
@@ -1,1464 +0,0 @@
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
- markAsImmediate,
11
- } from './functional';
12
- import type { TraceContext } from './trace-helpers';
13
- import type { TracingOptions } from './functional';
14
-
15
- function traceFactory<Args extends unknown[], Return>(
16
- factory: (ctx: TraceContext) => (...args: Args) => Return,
17
- ): (...args: Args) => Return {
18
- return trace(
19
- factory as (ctx: TraceContext) => (...args: Args) => Return,
20
- ) as unknown as (...args: Args) => Return;
21
- }
22
-
23
- function traceNamedFactory<Args extends unknown[], Return>(
24
- name: string,
25
- factory: (ctx: TraceContext) => (...args: Args) => Return,
26
- ): (...args: Args) => Return {
27
- return trace(
28
- name,
29
- factory as (ctx: TraceContext) => (...args: Args) => Return,
30
- ) as unknown as (...args: Args) => Return;
31
- }
32
-
33
- function traceOptionsFactory<Args extends unknown[], Return>(
34
- options: TracingOptions<Args, Return>,
35
- factory: (ctx: TraceContext) => (...args: Args) => Return,
36
- ): (...args: Args) => Return {
37
- return trace(
38
- options,
39
- factory as (ctx: TraceContext) => (...args: Args) => Return,
40
- ) as unknown as (...args: Args) => Return;
41
- }
42
- import { createTraceCollector } from './testing';
43
- import { AlwaysSampler, NeverSampler } from './sampling';
44
- import { init } from './init';
45
-
46
- describe('Functional API', () => {
47
- beforeEach(() => {
48
- vi.clearAllMocks();
49
- // Initialize for all tests
50
- init({
51
- service: 'test-service',
52
- });
53
- });
54
-
55
- describe('span()', () => {
56
- it('returns synchronous value when callback is sync', () => {
57
- const result = span({ name: 'sync-span' }, () => 42);
58
- expect(result).toBe(42);
59
- });
60
-
61
- it('returns promise when callback is async', async () => {
62
- const promise = span({ name: 'async-span' }, async () => 84);
63
- expect(promise).toBeInstanceOf(Promise);
64
- await expect(promise).resolves.toBe(84);
65
- });
66
-
67
- it('accepts a string name as first argument (sync)', () => {
68
- const result = span('sync-name-shorthand', () => 'ok');
69
- expect(result).toBe('ok');
70
- });
71
-
72
- it('accepts a string name as first argument (async)', async () => {
73
- await expect(
74
- span('async-name-shorthand', async () => 'ok'),
75
- ).resolves.toBe('ok');
76
- });
77
-
78
- it('records spans created via the string-name shorthand', async () => {
79
- const collector = createTraceCollector();
80
- await span('shorthand.recorded', async () => undefined);
81
- const names = collector.getSpans().map((s) => s.name);
82
- expect(names).toContain('shorthand.recorded');
83
- });
84
- });
85
-
86
- describe('trace()', () => {
87
- it('does not execute sync function during instrumentation', () => {
88
- let executions = 0;
89
- const traced = trace(function add(a: number, b: number) {
90
- executions += 1;
91
- return a + b;
92
- });
93
-
94
- expect(executions).toBe(0);
95
- const result = traced(2, 3);
96
- expect(result).toBe(5);
97
- expect(executions).toBe(1);
98
- });
99
-
100
- it('detects ctx factories by parameter name', async () => {
101
- const collector = createTraceCollector();
102
-
103
- const traced = trace(
104
- (_ctx: TraceContext) =>
105
- async function detected(name: string) {
106
- _ctx.setAttribute('user.name', name);
107
- return name;
108
- },
109
- );
110
-
111
- await traced('Alice');
112
-
113
- const spans = collector.getSpans();
114
- expect(spans).toHaveLength(1);
115
- expect(spans[0]!.attributes['user.name']).toBe('Alice');
116
- });
117
-
118
- describe('overload 1: trace(fn)', () => {
119
- it('should trace function with inferred name', async () => {
120
- const collector = createTraceCollector();
121
-
122
- const createUser = traceFactory(
123
- (_ctx: TraceContext) =>
124
- async function inferredName(name: string) {
125
- return { id: '123', name };
126
- },
127
- );
128
-
129
- const result = await createUser('Alice');
130
-
131
- expect(result).toEqual({ id: '123', name: 'Alice' });
132
-
133
- const spans = collector.getSpans();
134
- expect(spans).toHaveLength(1);
135
- expect(spans[0]!.name).toBe('inferredName');
136
- });
137
-
138
- it('should infer name from const assignment for factory pattern with arrow functions', async () => {
139
- const collector = createTraceCollector();
140
-
141
- // This is the factory pattern that was producing "unknown" trace names
142
- const processDocuments = traceFactory(
143
- (_ctx: TraceContext) => async (data: string) => {
144
- return data.toUpperCase();
145
- },
146
- );
147
-
148
- const result = await processDocuments('test');
149
-
150
- expect(result).toBe('TEST');
151
-
152
- const spans = collector.getSpans();
153
- expect(spans).toHaveLength(1);
154
- // Should infer 'processDocuments' from the const assignment, not 'unknown'
155
- expect(spans[0]!.name).toBe('processDocuments');
156
- });
157
-
158
- it('preserves sync return type for factory functions', () => {
159
- const collector = createTraceCollector();
160
-
161
- const add = traceFactory(
162
- (ctx: TraceContext) =>
163
- function addSync(a: number, b: number) {
164
- expect(ctx.traceId).toBeDefined();
165
- return a + b;
166
- },
167
- );
168
-
169
- const result = add(2, 3);
170
-
171
- expect(result).toBe(5);
172
- expect(result).not.toBeInstanceOf(Promise);
173
-
174
- const spans = collector.getSpans();
175
- expect(spans).toHaveLength(1);
176
- expect(spans[0]!.name).toBe('addSync');
177
- });
178
-
179
- it('should handle errors correctly', async () => {
180
- const collector = createTraceCollector();
181
-
182
- const failingFn = traceFactory((_ctx: TraceContext) => async () => {
183
- throw new Error('Test error');
184
- });
185
-
186
- await expect(failingFn()).rejects.toThrow('Test error');
187
-
188
- const spans = collector.getSpans();
189
- expect(spans).toHaveLength(1);
190
- expect(spans[0]!.status.code).toBe(2); // ERROR
191
- expect(spans[0]!.attributes['exception.message']).toBe('Test error');
192
- });
193
- });
194
-
195
- describe('zero-arg factory pattern (no ctx parameter)', () => {
196
- it('should detect zero-arg sync factory and execute inner function', () => {
197
- const collector = createTraceCollector();
198
-
199
- const addOne = trace(() => (i: number) => {
200
- return i + 1;
201
- });
202
-
203
- const result = addOne(1);
204
-
205
- expect(result).toBe(2);
206
- expect(result).not.toBeInstanceOf(Promise);
207
-
208
- const spans = collector.getSpans();
209
- expect(spans).toHaveLength(1);
210
- });
211
-
212
- it('should detect zero-arg async factory and execute inner function', async () => {
213
- const collector = createTraceCollector();
214
-
215
- const fetchData = trace(() => async (query: string) => {
216
- return query.toUpperCase();
217
- });
218
-
219
- const result = await fetchData('test');
220
-
221
- expect(result).toBe('TEST');
222
-
223
- const spans = collector.getSpans();
224
- expect(spans).toHaveLength(1);
225
- });
226
-
227
- it('should work with named zero-arg factory', () => {
228
- const collector = createTraceCollector();
229
-
230
- const addOne = trace('addOne', () => (i: number) => {
231
- return i + 1;
232
- });
233
-
234
- const result = addOne(1);
235
-
236
- expect(result).toBe(2);
237
-
238
- const spans = collector.getSpans();
239
- expect(spans).toHaveLength(1);
240
- expect(spans[0]!.name).toBe('addOne');
241
- });
242
-
243
- it('should handle multiple zero-arg factories combined', () => {
244
- const collector = createTraceCollector();
245
-
246
- const addOne = trace('addOne', () => (i: number) => i + 1);
247
- const addTwo = trace('addTwo', () => (i: number) => i + 2);
248
-
249
- const result = addOne(1) + addTwo(1);
250
-
251
- expect(result).toBe(5);
252
-
253
- const spans = collector.getSpans();
254
- expect(spans).toHaveLength(2);
255
- });
256
- });
257
-
258
- describe('overload 2: trace(name, fn)', () => {
259
- it('should use custom name', async () => {
260
- const collector = createTraceCollector();
261
-
262
- const createUser = traceNamedFactory(
263
- 'user.create',
264
- (ctx: TraceContext) => async (name: string) => {
265
- return { id: '123', name };
266
- },
267
- );
268
-
269
- await createUser('Alice');
270
-
271
- const spans = collector.getSpans();
272
- expect(spans).toHaveLength(1);
273
- expect(spans[0]!.name).toBe('user.create');
274
- });
275
- });
276
-
277
- describe('overload 3: trace(options, fn)', () => {
278
- it('should use options', async () => {
279
- const collector = createTraceCollector();
280
-
281
- const createUser = traceOptionsFactory(
282
- {
283
- name: 'user.create',
284
- sampler: new AlwaysSampler(),
285
- attributesFromArgs: ([name]) => ({ userName: name }),
286
- },
287
- (ctx: TraceContext) => async (name: string) => {
288
- return { id: '123', name };
289
- },
290
- );
291
-
292
- await createUser('Alice');
293
-
294
- const spans = collector.getSpans();
295
- expect(spans).toHaveLength(1);
296
- expect(spans[0]!.name).toBe('user.create');
297
- expect(spans[0]!.attributes['userName']).toBe('Alice');
298
- });
299
-
300
- it('should use serviceName to compose span name', async () => {
301
- const collector = createTraceCollector();
302
-
303
- const createUser = traceOptionsFactory(
304
- { serviceName: 'user' },
305
- (ctx: TraceContext) =>
306
- async function serviceNameTest(name: string) {
307
- return { id: '123', name };
308
- },
309
- );
310
-
311
- await createUser('Alice');
312
-
313
- const spans = collector.getSpans();
314
- expect(spans).toHaveLength(1);
315
- expect(spans[0]!.name).toBe('user.serviceNameTest');
316
- });
317
-
318
- it('should extract result attributes', async () => {
319
- const collector = createTraceCollector();
320
-
321
- const createUser = traceOptionsFactory(
322
- {
323
- name: 'user.create',
324
- attributesFromResult: (result) => ({
325
- userId: (result as unknown as { id: string }).id,
326
- }),
327
- },
328
- (ctx: TraceContext) => async (name: string) => {
329
- return { id: '456', name };
330
- },
331
- );
332
-
333
- await createUser('Alice');
334
-
335
- const spans = collector.getSpans();
336
- expect(spans).toHaveLength(1);
337
- expect(spans[0]!.attributes['userId']).toBe('456');
338
- });
339
-
340
- it('captures input/output as autotel.input/output when opted in', async () => {
341
- const collector = createTraceCollector();
342
-
343
- const calc = traceOptionsFactory(
344
- { name: 'calc', captureInput: true, captureOutput: true },
345
- (_ctx: TraceContext) => async (a: number, b: number) => a + b,
346
- );
347
-
348
- await calc(2, 3);
349
-
350
- const span = collector.getSpans()[0]!;
351
- // Multiple args captured as an array; single value would be captured directly.
352
- expect(span.attributes['autotel.input']).toBe('[2,3]');
353
- expect(span.attributes['autotel.output']).toBe('5');
354
- });
355
-
356
- it('does not capture input/output by default', async () => {
357
- const collector = createTraceCollector();
358
-
359
- const calc = traceOptionsFactory(
360
- { name: 'calc-default' },
361
- (_ctx: TraceContext) => async (a: number, b: number) => a + b,
362
- );
363
-
364
- await calc(2, 3);
365
-
366
- const span = collector.getSpans()[0]!;
367
- expect(span.attributes['autotel.input']).toBeUndefined();
368
- expect(span.attributes['autotel.output']).toBeUndefined();
369
- });
370
-
371
- it('captures a single argument directly (not wrapped in an array)', async () => {
372
- const collector = createTraceCollector();
373
-
374
- const load = traceOptionsFactory(
375
- { name: 'load', captureInput: true, captureOutput: true },
376
- (_ctx: TraceContext) => async (req: { userId: string }) => ({
377
- holdings: 3,
378
- userId: req.userId,
379
- }),
380
- );
381
-
382
- await load({ userId: 'anon' });
383
-
384
- const span = collector.getSpans()[0]!;
385
- expect(span.attributes['autotel.input']).toBe('{"userId":"anon"}');
386
- expect(span.attributes['autotel.output']).toBe(
387
- '{"holdings":3,"userId":"anon"}',
388
- );
389
- });
390
-
391
- it('should respect NeverSampler', async () => {
392
- const collector = createTraceCollector();
393
-
394
- const createUser = traceOptionsFactory(
395
- {
396
- name: 'user.create',
397
- sampler: new NeverSampler(),
398
- },
399
- (ctx: TraceContext) => async (name: string) => {
400
- return { id: '123', name };
401
- },
402
- );
403
-
404
- await createUser('Alice');
405
-
406
- const spans = collector.getSpans();
407
- expect(spans).toHaveLength(0);
408
- });
409
- });
410
- });
411
-
412
- describe('withTracing()', () => {
413
- it('should create reusable wrapper', async () => {
414
- const collector = createTraceCollector();
415
-
416
- const trace = withTracing({ serviceName: 'user' });
417
-
418
- const createUser = trace(
419
- (_ctx: TraceContext) =>
420
- async function reusableCreate(name: string) {
421
- return { id: '123', name };
422
- },
423
- );
424
-
425
- const updateUser = trace(
426
- (_ctx: TraceContext) =>
427
- async function reusableUpdate(id: string, name: string) {
428
- return { id, name };
429
- },
430
- );
431
-
432
- await createUser('Alice');
433
- await updateUser('123', 'Bob');
434
-
435
- const spans = collector.getSpans();
436
- expect(spans).toHaveLength(2);
437
- expect(spans[0]!.name).toBe('user.reusableCreate');
438
- expect(spans[1]!.name).toBe('user.reusableUpdate');
439
- });
440
-
441
- it('preserves sync return values', () => {
442
- const traceSync = withTracing({ name: 'math.add' });
443
- const add = traceSync(
444
- (_ctx: TraceContext) =>
445
- function addSync(a: number, b: number) {
446
- return a + b;
447
- },
448
- );
449
-
450
- const result = add(4, 5);
451
- expect(result).toBe(9);
452
- });
453
-
454
- it('should support explicit name', async () => {
455
- const collector = createTraceCollector();
456
-
457
- const createUser = withTracing({ name: 'user.create' })(
458
- (ctx: TraceContext) => async (name: string) => {
459
- return { id: '123', name };
460
- },
461
- );
462
-
463
- await createUser('Alice');
464
-
465
- const spans = collector.getSpans();
466
- expect(spans).toHaveLength(1);
467
- expect(spans[0]!.name).toBe('user.create');
468
- });
469
-
470
- it('should handle errors', async () => {
471
- const collector = createTraceCollector();
472
-
473
- const failingFn = withTracing({ name: 'test.fail' })(
474
- (ctx) => async () => {
475
- throw new Error('Fail');
476
- },
477
- );
478
-
479
- await expect(failingFn()).rejects.toThrow('Fail');
480
-
481
- const spans = collector.getSpans();
482
- expect(spans).toHaveLength(1);
483
- expect(spans[0]!.status.code).toBe(2); // ERROR
484
- });
485
- });
486
-
487
- describe('instrument()', () => {
488
- it('should instrument all functions', async () => {
489
- const collector = createTraceCollector();
490
-
491
- const userService = instrument({
492
- functions: {
493
- createUser: async (name: string) => {
494
- return { id: '123', name };
495
- },
496
- updateUser: async (id: string, name: string) => {
497
- return { id, name };
498
- },
499
- deleteUser: async (id: string) => {
500
- return { id };
501
- },
502
- },
503
- serviceName: 'user',
504
- });
505
-
506
- await userService.createUser('Alice');
507
- await userService.updateUser('123', 'Bob');
508
- await userService.deleteUser('123');
509
-
510
- const spans = collector.getSpans();
511
- expect(spans).toHaveLength(3);
512
- expect(spans[0]!.name).toBe('user.createUser');
513
- expect(spans[1]!.name).toBe('user.updateUser');
514
- expect(spans[2]!.name).toBe('user.deleteUser');
515
- });
516
-
517
- it('should skip functions with _ prefix by default', async () => {
518
- const collector = createTraceCollector();
519
-
520
- const service = instrument({
521
- functions: {
522
- publicFn: async () => 'public',
523
- _privateFn: async () => 'private',
524
- },
525
- serviceName: 'test',
526
- });
527
-
528
- await service.publicFn();
529
- await service._privateFn();
530
-
531
- const spans = collector.getSpans();
532
- expect(spans).toHaveLength(1);
533
- expect(spans[0]!.name).toBe('test.publicFn');
534
- });
535
-
536
- it('should support custom skip rules', async () => {
537
- const collector = createTraceCollector();
538
-
539
- const service = instrument({
540
- functions: {
541
- publicFn: async () => 'public',
542
- testFn: async () => 'test',
543
- debugFn: async () => 'debug',
544
- },
545
- serviceName: 'test',
546
- skip: [
547
- /^test/, // Skip functions starting with 'test'
548
- (key) => key.includes('debug'), // Skip functions containing 'debug'
549
- ],
550
- });
551
-
552
- await service.publicFn();
553
- await service.testFn();
554
- await service.debugFn();
555
-
556
- const spans = collector.getSpans();
557
- expect(spans).toHaveLength(1);
558
- expect(spans[0]!.name).toBe('test.publicFn');
559
- });
560
-
561
- it('should support per-function overrides', async () => {
562
- const collector = createTraceCollector();
563
-
564
- const service = instrument({
565
- functions: {
566
- createUser: async (name: string) => {
567
- return { id: '123', name };
568
- },
569
- deleteUser: async (id: string) => {
570
- return { id };
571
- },
572
- },
573
- serviceName: 'user',
574
- sampler: new NeverSampler(), // Default: don't sample
575
- overrides: {
576
- deleteUser: {
577
- sampler: new AlwaysSampler(), // Always sample deletes!
578
- },
579
- },
580
- });
581
-
582
- await service.createUser('Alice');
583
- await service.deleteUser('123');
584
-
585
- const spans = collector.getSpans();
586
- expect(spans).toHaveLength(1);
587
- expect(spans[0]!.name).toBe('user.deleteUser');
588
- });
589
-
590
- it('should preserve function behavior', async () => {
591
- const service = instrument({
592
- functions: {
593
- add: async (a: number, b: number) => a + b,
594
- subtract: async (a: number, b: number) => a - b,
595
- },
596
- serviceName: 'math',
597
- });
598
-
599
- expect(await service.add(5, 3)).toBe(8);
600
- expect(await service.subtract(5, 3)).toBe(2);
601
- });
602
-
603
- it('should not wrap non-functions', () => {
604
- const service = instrument({
605
- functions: {
606
- fn: async () => 'function',
607
- value: 42,
608
- obj: { nested: true },
609
- },
610
- serviceName: 'test',
611
- });
612
-
613
- expect(typeof service.fn).toBe('function');
614
- expect(service.value).toBe(42);
615
- expect(service.obj).toEqual({ nested: true });
616
- });
617
-
618
- it('should preserve this context for methods that rely on it', async () => {
619
- const collector = createTraceCollector();
620
-
621
- // Service object with state on 'this'
622
- const svc = {
623
- prefix: 'user',
624
- count: 0,
625
- build: async function (id: string) {
626
- return `${this.prefix}-${id}`;
627
- },
628
- increment: async function () {
629
- this.count++;
630
- return this.count;
631
- },
632
- };
633
-
634
- const instrumented = instrument({
635
- functions: svc,
636
- serviceName: 'svc',
637
- }) as typeof svc;
638
-
639
- // Test that this.prefix is accessible
640
- const result1 = await instrumented.build('123');
641
- expect(result1).toBe('user-123'); // Should not be 'undefined-123'
642
-
643
- // Test that this.count is accessible and modifiable
644
- const result2 = await instrumented.increment();
645
- expect(result2).toBe(1);
646
- const result3 = await instrumented.increment();
647
- expect(result3).toBe(2);
648
-
649
- const spans = collector.getSpans();
650
- expect(spans).toHaveLength(3);
651
- });
652
-
653
- it('should not call attributesFromArgs when sampler rejects tracing', async () => {
654
- const collector = createTraceCollector();
655
-
656
- // Mock expensive attribute extraction
657
- const expensiveAttributeExtraction = vi.fn((args: unknown[]) => {
658
- // Simulate expensive operation (JSON cloning, payload scrubbing, etc.)
659
- return { arg0: args[0] };
660
- });
661
-
662
- const service = instrument({
663
- functions: {
664
- createUser: async (name: string) => {
665
- return { id: '123', name };
666
- },
667
- },
668
- serviceName: 'user',
669
- sampler: new NeverSampler(), // Never sample
670
- attributesFromArgs: expensiveAttributeExtraction,
671
- });
672
-
673
- // Execute function with NeverSampler
674
- await service.createUser('Alice');
675
-
676
- // attributesFromArgs should NOT be called since we're not tracing
677
- expect(expensiveAttributeExtraction).not.toHaveBeenCalled();
678
-
679
- // No spans should be created
680
- const spans = collector.getSpans();
681
- expect(spans).toHaveLength(0);
682
- });
683
-
684
- it('should call attributesFromArgs when sampler accepts tracing', async () => {
685
- const collector = createTraceCollector();
686
-
687
- // Mock attribute extraction
688
- const attributeExtraction = vi.fn((args: unknown[]) => {
689
- return { arg0: args[0] };
690
- });
691
-
692
- const service = instrument({
693
- functions: {
694
- createUser: async (name: string) => {
695
- return { id: '123', name };
696
- },
697
- },
698
- serviceName: 'user',
699
- sampler: new AlwaysSampler(), // Always sample
700
- attributesFromArgs: attributeExtraction,
701
- });
702
-
703
- // Execute function with AlwaysSampler
704
- await service.createUser('Alice');
705
-
706
- // attributesFromArgs SHOULD be called since we're tracing
707
- // Note: args will include context as first element
708
- expect(attributeExtraction).toHaveBeenCalledTimes(1);
709
- expect(attributeExtraction).toHaveBeenCalledWith(
710
- expect.arrayContaining(['Alice']),
711
- );
712
-
713
- // Span should be created with attributes
714
- const spans = collector.getSpans();
715
- expect(spans).toHaveLength(1);
716
- expect(spans[0]!.attributes['arg0']).toBe('Alice');
717
- });
718
- });
719
-
720
- describe('Span naming priority', () => {
721
- it('should prioritize explicit name over serviceName', async () => {
722
- const collector = createTraceCollector();
723
-
724
- const fn = traceOptionsFactory(
725
- {
726
- name: 'explicit.name',
727
- serviceName: 'ignored',
728
- },
729
- (ctx: TraceContext) => async () => 'result',
730
- );
731
-
732
- await fn();
733
-
734
- const spans = collector.getSpans();
735
- expect(spans[0]!.name).toBe('explicit.name');
736
- });
737
-
738
- it('should use serviceName + fnName when no explicit name', async () => {
739
- const collector = createTraceCollector();
740
-
741
- const myFunction = traceOptionsFactory(
742
- {
743
- serviceName: 'service',
744
- },
745
- (ctx: TraceContext) =>
746
- async function priorityTest() {
747
- return 'result';
748
- },
749
- );
750
-
751
- await myFunction();
752
-
753
- const spans = collector.getSpans();
754
- expect(spans[0]!.name).toBe('service.priorityTest');
755
- });
756
-
757
- it('should fall back to inferred name', async () => {
758
- const collector = createTraceCollector();
759
-
760
- const namedFunction = traceFactory(
761
- (_ctx: TraceContext) =>
762
- async function fallbackName() {
763
- return 'result';
764
- },
765
- );
766
-
767
- await namedFunction();
768
-
769
- const spans = collector.getSpans();
770
- expect(spans[0]!.name).toBe('fallbackName');
771
- });
772
- });
773
-
774
- describe('Error handling', () => {
775
- it('should truncate long error messages', async () => {
776
- const collector = createTraceCollector();
777
-
778
- const longError = 'x'.repeat(600);
779
- const fn = traceFactory((_ctx: TraceContext) => async () => {
780
- throw new Error(longError);
781
- });
782
-
783
- await expect(fn()).rejects.toThrow();
784
-
785
- const spans = collector.getSpans();
786
- const errorMsg = spans[0]!.attributes['exception.message'] as string;
787
- expect(errorMsg.length).toBeLessThan(600);
788
- expect(errorMsg).toContain('(truncated)');
789
- });
790
-
791
- it('should record exception type', async () => {
792
- const collector = createTraceCollector();
793
-
794
- class CustomError extends Error {
795
- constructor(message: string) {
796
- super(message);
797
- this.name = 'CustomError';
798
- }
799
- }
800
-
801
- const fn = traceFactory((_ctx: TraceContext) => async () => {
802
- throw new CustomError('Custom error');
803
- });
804
-
805
- await expect(fn()).rejects.toThrow();
806
-
807
- const spans = collector.getSpans();
808
- expect(spans[0]!.attributes['exception.type']).toBe('CustomError');
809
- });
810
-
811
- it('should include stack trace', async () => {
812
- const collector = createTraceCollector();
813
-
814
- const fn = traceFactory((_ctx: TraceContext) => async () => {
815
- throw new Error('Stack test');
816
- });
817
-
818
- await expect(fn()).rejects.toThrow();
819
-
820
- const spans = collector.getSpans();
821
- expect(spans[0]!.attributes['exception.stack']).toBeDefined();
822
- });
823
- });
824
-
825
- describe('Type preservation', () => {
826
- it('should preserve exact types', async () => {
827
- interface User {
828
- id: string;
829
- name: string;
830
- }
831
-
832
- const createUser = traceFactory(
833
- (_ctx: TraceContext) =>
834
- async (name: string): Promise<User> => {
835
- return { id: '123', name };
836
- },
837
- );
838
-
839
- const result = await createUser('Alice');
840
-
841
- // TypeScript should know result is User
842
- expect(result.id).toBe('123');
843
- expect(result.name).toBe('Alice');
844
- });
845
-
846
- it('should preserve argument types', async () => {
847
- const fn = traceFactory(
848
- (ctx: TraceContext) =>
849
- async (a: number, b: string, c: { x: boolean }): Promise<void> => {
850
- expect(typeof a).toBe('number');
851
- expect(typeof b).toBe('string');
852
- expect(typeof c.x).toBe('boolean');
853
- },
854
- );
855
-
856
- await fn(42, 'hello', { x: true });
857
- });
858
- });
859
-
860
- describe('ctx() helper', () => {
861
- it('should return trace context when span is active', async () => {
862
- const collector = createTraceCollector();
863
-
864
- const createUser = traceFactory(
865
- (_ctx: TraceContext) => async (name: string) => {
866
- expect(ctx.traceId).toBeDefined();
867
- expect(ctx.spanId).toBeDefined();
868
- expect(ctx.correlationId).toBeDefined();
869
- return { id: '123', name };
870
- },
871
- );
872
-
873
- const result = await createUser('Alice');
874
- expect(result).toEqual({ id: '123', name: 'Alice' });
875
-
876
- const spans = collector.getSpans();
877
- expect(spans).toHaveLength(1);
878
- });
879
-
880
- it('should provide span methods on context', async () => {
881
- const collector = createTraceCollector();
882
-
883
- const createUser = traceFactory(
884
- (_ctx: TraceContext) => async (name: string) => {
885
- if (ctx.traceId) {
886
- ctx.setAttribute('user.name', name);
887
- ctx.setAttributes({ 'user.id': '123', 'user.active': true });
888
- }
889
- return { id: '123', name };
890
- },
891
- );
892
-
893
- await createUser('Alice');
894
-
895
- const spans = collector.getSpans();
896
- expect(spans).toHaveLength(1);
897
- expect(spans[0]!.attributes['user.name']).toBe('Alice');
898
- expect(spans[0]!.attributes['user.id']).toBe('123');
899
- expect(spans[0]!.attributes['user.active']).toBe(true);
900
- });
901
-
902
- it('should return undefined properties when no span is active', () => {
903
- expect(ctx.traceId).toBeUndefined();
904
- expect(ctx.spanId).toBeUndefined();
905
- });
906
-
907
- it('should record exceptions via context', async () => {
908
- const collector = createTraceCollector();
909
-
910
- const failingFn = traceFactory((_ctx: TraceContext) => async () => {
911
- const error = new Error('Test exception');
912
- if (ctx.traceId) {
913
- ctx.recordException(error);
914
- }
915
- throw error;
916
- });
917
-
918
- await expect(failingFn()).rejects.toThrow('Test exception');
919
-
920
- const spans = collector.getSpans();
921
- expect(spans).toHaveLength(1);
922
- expect(spans[0]!.status.code).toBe(2); // ERROR
923
- });
924
- });
925
-
926
- describe('Immediate execution pattern', () => {
927
- it('should execute async function immediately with context', async () => {
928
- const collector = createTraceCollector();
929
-
930
- const result = await trace(async (ctx: TraceContext) => {
931
- ctx.setAttribute('test.key', 'value');
932
- return { data: 'test' };
933
- });
934
-
935
- expect(result).toEqual({ data: 'test' });
936
-
937
- const spans = collector.getSpans();
938
- expect(spans).toHaveLength(1);
939
- expect(spans[0]!.attributes['test.key']).toBe('value');
940
- });
941
-
942
- it('should execute sync function immediately with context', () => {
943
- const collector = createTraceCollector();
944
-
945
- const result = trace((ctx: TraceContext) => {
946
- ctx.setAttribute('test.key', 'sync-value');
947
- return 42;
948
- });
949
-
950
- expect(result).toBe(42);
951
-
952
- const spans = collector.getSpans();
953
- expect(spans).toHaveLength(1);
954
- expect(spans[0]!.attributes['test.key']).toBe('sync-value');
955
- });
956
-
957
- it('should support custom name with immediate execution', async () => {
958
- const collector = createTraceCollector();
959
-
960
- const result = await trace(
961
- 'custom.operation',
962
- async (ctx: TraceContext) => {
963
- ctx.setAttribute('operation.id', '123');
964
- return 'success';
965
- },
966
- );
967
-
968
- expect(result).toBe('success');
969
-
970
- const spans = collector.getSpans();
971
- expect(spans).toHaveLength(1);
972
- expect(spans[0]!.name).toBe('custom.operation');
973
- expect(spans[0]!.attributes['operation.id']).toBe('123');
974
- });
975
-
976
- // Regression: when esbuild/terser minifies the inner function, the
977
- // `ctx` parameter gets renamed to a single letter and the name-allowlist
978
- // in looksLikeTraceFactory stops matching. Library authors who wrap user
979
- // handlers (e.g. autotel-aws/lambda's wrapHandler) should use
980
- // `markAsImmediate` to opt the inner function into immediate execution
981
- // regardless of parameter naming.
982
- it('honors markAsImmediate so dispatch survives minified parameter names', async () => {
983
- const collector = createTraceCollector();
984
-
985
- // Parameter named `d` simulates the post-minification shape.
986
- const inner = markAsImmediate(async (d: TraceContext) => {
987
- d.setAttribute('test.minified', true);
988
- return 'ok';
989
- });
990
- const result = await trace('minified.handler', inner);
991
-
992
- expect(result).toBe('ok');
993
-
994
- const spans = collector.getSpans();
995
- expect(spans).toHaveLength(1);
996
- expect(spans[0]!.name).toBe('minified.handler');
997
- expect(spans[0]!.attributes['test.minified']).toBe(true);
998
- });
999
-
1000
- it('should support options with immediate execution', async () => {
1001
- const collector = createTraceCollector();
1002
-
1003
- const result = await trace(
1004
- { name: 'options.test', withMetrics: true },
1005
- async (ctx: TraceContext) => {
1006
- ctx.setAttribute('test.option', 'enabled');
1007
- return 100;
1008
- },
1009
- );
1010
-
1011
- expect(result).toBe(100);
1012
-
1013
- const spans = collector.getSpans();
1014
- expect(spans).toHaveLength(1);
1015
- expect(spans[0]!.name).toBe('options.test');
1016
- expect(spans[0]!.attributes['test.option']).toBe('enabled');
1017
- });
1018
-
1019
- it('should distinguish between factory and immediate execution', async () => {
1020
- const collector = createTraceCollector();
1021
-
1022
- // Factory pattern - returns a function
1023
- const factory = trace((ctx: TraceContext) => async (name: string) => {
1024
- ctx.setAttribute('user.name', name);
1025
- return { name };
1026
- });
1027
-
1028
- // Immediate execution - returns result directly
1029
- const immediate = await trace(async (ctx: TraceContext) => {
1030
- ctx.setAttribute('immediate', true);
1031
- return 'done';
1032
- });
1033
-
1034
- expect(typeof factory).toBe('function');
1035
- expect(immediate).toBe('done');
1036
-
1037
- // Now call the factory
1038
- const factoryResult = await factory('Alice');
1039
- expect(factoryResult).toEqual({ name: 'Alice' });
1040
-
1041
- const spans = collector.getSpans();
1042
- expect(spans).toHaveLength(2);
1043
-
1044
- // First span is from immediate execution
1045
- expect(spans[0]!.attributes['immediate']).toBe(true);
1046
-
1047
- // Second span is from factory call
1048
- expect(spans[1]!.attributes['user.name']).toBe('Alice');
1049
- });
1050
-
1051
- it('should work with wrapper function pattern from feedback', async () => {
1052
- const collector = createTraceCollector();
1053
-
1054
- // The exact use case from the feedback
1055
- function timed<T>(
1056
- requestId: string,
1057
- operation: string,
1058
- fn: () => Promise<T>,
1059
- ): Promise<T> {
1060
- return trace(operation, async (ctx: TraceContext) => {
1061
- ctx.setAttributes({
1062
- 'request.id': requestId,
1063
- 'operation.name': operation,
1064
- });
1065
-
1066
- const result = await fn();
1067
- return result;
1068
- });
1069
- }
1070
-
1071
- // Test it
1072
- const mockFn = async () => {
1073
- return { userId: '123', status: 'active' };
1074
- };
1075
-
1076
- const result = await timed('req-456', 'fetchUser', mockFn);
1077
-
1078
- expect(result).toEqual({ userId: '123', status: 'active' });
1079
-
1080
- const spans = collector.getSpans();
1081
- expect(spans).toHaveLength(1);
1082
- expect(spans[0]!.name).toBe('fetchUser');
1083
- expect(spans[0]!.attributes['request.id']).toBe('req-456');
1084
- expect(spans[0]!.attributes['operation.name']).toBe('fetchUser');
1085
- });
1086
-
1087
- it('should not create orphan spans when nesting span() inside trace() immediate execution', async () => {
1088
- const collector = createTraceCollector();
1089
-
1090
- // This was causing a bug where span() was called during pattern detection,
1091
- // creating an orphan span outside of the trace() context
1092
- await trace('user-request-trace', async (ctx: TraceContext) => {
1093
- ctx.setAttribute('input.query', 'What is the capital of France?');
1094
-
1095
- // Nested span should be a child of user-request-trace
1096
- await span(
1097
- {
1098
- name: 'llm-call',
1099
- attributes: { model: 'gpt-4' },
1100
- },
1101
- async () => {
1102
- // Simulate LLM call
1103
- return 'The capital of France is Paris.';
1104
- },
1105
- );
1106
-
1107
- ctx.setAttribute('output', 'Successfully answered.');
1108
- });
1109
-
1110
- const spans = collector.getSpans();
1111
-
1112
- // KEY ASSERTION: Should have exactly 2 spans, NOT 3
1113
- // Before the fix, there would be 3 spans:
1114
- // 1. An orphan llm-call (created during pattern detection)
1115
- // 2. user-request-trace (the parent)
1116
- // 3. llm-call (proper child)
1117
- expect(spans).toHaveLength(2);
1118
-
1119
- // Verify we have the correct span names
1120
- const spanNames = spans.map((s) => s.name).toSorted();
1121
- expect(spanNames).toEqual(['llm-call', 'user-request-trace']);
1122
-
1123
- // Verify attributes on each span
1124
- const parentSpan = spans.find((s) => s.name === 'user-request-trace');
1125
- const childSpan = spans.find((s) => s.name === 'llm-call');
1126
-
1127
- expect(parentSpan).toBeDefined();
1128
- expect(childSpan).toBeDefined();
1129
-
1130
- expect(parentSpan!.attributes['input.query']).toBe(
1131
- 'What is the capital of France?',
1132
- );
1133
- expect(parentSpan!.attributes['output']).toBe('Successfully answered.');
1134
- expect(childSpan!.attributes['model']).toBe('gpt-4');
1135
- });
1136
-
1137
- it('should not execute async function during pattern detection', async () => {
1138
- const collector = createTraceCollector();
1139
- let executionCount = 0;
1140
-
1141
- // This async function should only be executed ONCE, not twice
1142
- // (once during pattern detection + once for actual execution = BUG)
1143
- await trace('single-execution', async (ctx: TraceContext) => {
1144
- executionCount++;
1145
- ctx.setAttribute('execution.count', executionCount);
1146
- return 'done';
1147
- });
1148
-
1149
- // Function should have been executed exactly once
1150
- expect(executionCount).toBe(1);
1151
-
1152
- const spans = collector.getSpans();
1153
- expect(spans).toHaveLength(1);
1154
- expect(spans[0]!.attributes['execution.count']).toBe(1);
1155
- });
1156
- });
1157
-
1158
- describe('baggage', () => {
1159
- it('should get baggage entry from context', async () => {
1160
- const collector = createTraceCollector();
1161
- const { context, propagation } = await import('@opentelemetry/api');
1162
-
1163
- // Create context with baggage
1164
- const activeContext = context.active();
1165
- const baggage = propagation.createBaggage();
1166
- const updatedBaggage = baggage.setEntry('tenant.id', {
1167
- value: 'tenant-123',
1168
- });
1169
- const contextWithBaggage = propagation.setBaggage(
1170
- activeContext,
1171
- updatedBaggage,
1172
- );
1173
-
1174
- await context.with(contextWithBaggage, async () => {
1175
- await trace((ctx) => async () => {
1176
- const tenantId = ctx.getBaggage('tenant.id');
1177
- expect(tenantId).toBe('tenant-123');
1178
- return 'done';
1179
- })();
1180
- });
1181
-
1182
- expect(collector.getSpans()).toHaveLength(1);
1183
- });
1184
-
1185
- it('withBaggage should set baggage for child spans', async () => {
1186
- const collector = createTraceCollector();
1187
-
1188
- await trace((ctx) => async () => {
1189
- return await withBaggage({
1190
- baggage: { 'tenant.id': 'tenant-456', 'user.id': 'user-789' },
1191
- fn: async () => {
1192
- // Check baggage is available
1193
- expect(ctx.getBaggage('tenant.id')).toBe('tenant-456');
1194
- expect(ctx.getBaggage('user.id')).toBe('user-789');
1195
-
1196
- // Create child span - should inherit baggage
1197
- await trace((childCtx) => async () => {
1198
- expect(childCtx.getBaggage('tenant.id')).toBe('tenant-456');
1199
- return 'child-done';
1200
- })();
1201
- return 'parent-done';
1202
- },
1203
- });
1204
- })();
1205
-
1206
- const spans = collector.getSpans();
1207
- expect(spans).toHaveLength(2);
1208
- });
1209
-
1210
- it('withBaggage should work with sync functions', () => {
1211
- let capturedBaggage: string | undefined;
1212
-
1213
- trace((ctx) => () => {
1214
- return withBaggage({
1215
- baggage: { key: 'value' },
1216
- fn: () => {
1217
- capturedBaggage = ctx.getBaggage('key');
1218
- return 'sync-result';
1219
- },
1220
- });
1221
- })();
1222
-
1223
- expect(capturedBaggage).toBe('value');
1224
- });
1225
-
1226
- it('withBaggage should merge with existing baggage', async () => {
1227
- const collector = createTraceCollector();
1228
- const { context, propagation } = await import('@opentelemetry/api');
1229
-
1230
- // Set initial baggage
1231
- const activeContext = context.active();
1232
- const baggage = propagation.createBaggage();
1233
- const updatedBaggage = baggage.setEntry('existing.key', {
1234
- value: 'existing-value',
1235
- });
1236
- const contextWithBaggage = propagation.setBaggage(
1237
- activeContext,
1238
- updatedBaggage,
1239
- );
1240
-
1241
- await context.with(contextWithBaggage, async () => {
1242
- await trace((ctx) => async () => {
1243
- // New baggage should be available
1244
- expect(ctx.getBaggage('new.key')).toBeUndefined(); // Not set yet
1245
-
1246
- return await withBaggage({
1247
- baggage: { 'new.key': 'new-value' },
1248
- fn: async () => {
1249
- // New baggage should be available
1250
- expect(ctx.getBaggage('new.key')).toBe('new-value');
1251
- // Existing baggage should still be available (if propagator preserves it)
1252
- return 'done';
1253
- },
1254
- });
1255
- })();
1256
- });
1257
-
1258
- // Only 1 span created (the outer trace)
1259
- expect(collector.getSpans()).toHaveLength(1);
1260
- });
1261
-
1262
- it('withBaggage should not leak baggage after callback completes', async () => {
1263
- const collector = createTraceCollector();
1264
-
1265
- await trace((ctx) => async () => {
1266
- expect(ctx.getBaggage('tenant.id')).toBeUndefined();
1267
-
1268
- await withBaggage({
1269
- baggage: { 'tenant.id': 'tenant-456' },
1270
- fn: async () => {
1271
- expect(ctx.getBaggage('tenant.id')).toBe('tenant-456');
1272
- },
1273
- });
1274
-
1275
- // Child spans created after withBaggage must not inherit scoped baggage.
1276
- // (Same-ctx after await may still see baggage due to async context propagation.)
1277
- await trace((childCtx) => async () => {
1278
- expect(childCtx.getBaggage('tenant.id')).toBeUndefined();
1279
- })();
1280
- })();
1281
-
1282
- expect(collector.getSpans()).toHaveLength(2);
1283
- });
1284
-
1285
- it('ctx.getAllBaggage should return all baggage entries', async () => {
1286
- const collector = createTraceCollector();
1287
- const { context, propagation } = await import('@opentelemetry/api');
1288
-
1289
- // Create context with multiple baggage entries
1290
- const activeContext = context.active();
1291
- let baggage = propagation.createBaggage();
1292
- baggage = baggage.setEntry('key1', { value: 'value1' });
1293
- baggage = baggage.setEntry('key2', { value: 'value2' });
1294
- const contextWithBaggage = propagation.setBaggage(activeContext, baggage);
1295
-
1296
- await context.with(contextWithBaggage, async () => {
1297
- await trace((ctx) => async () => {
1298
- const allBaggage = ctx.getAllBaggage();
1299
- expect(allBaggage.size).toBeGreaterThanOrEqual(2);
1300
- expect(allBaggage.get('key1')?.value).toBe('value1');
1301
- expect(allBaggage.get('key2')?.value).toBe('value2');
1302
- return 'done';
1303
- })();
1304
- });
1305
-
1306
- expect(collector.getSpans()).toHaveLength(1);
1307
- });
1308
- });
1309
-
1310
- describe('Array attribute support', () => {
1311
- it('should support string array attributes', async () => {
1312
- const collector = createTraceCollector();
1313
-
1314
- await trace(async (ctx: TraceContext) => {
1315
- ctx.setAttribute('tags', ['qa', 'test', 'automated']);
1316
- return 'done';
1317
- });
1318
-
1319
- const spans = collector.getSpans();
1320
- expect(spans).toHaveLength(1);
1321
- expect(spans[0]!.attributes['tags']).toEqual(['qa', 'test', 'automated']);
1322
- });
1323
-
1324
- it('should support number array attributes', async () => {
1325
- const collector = createTraceCollector();
1326
-
1327
- await trace(async (ctx: TraceContext) => {
1328
- ctx.setAttribute('scores', [95, 87, 92]);
1329
- return 'done';
1330
- });
1331
-
1332
- const spans = collector.getSpans();
1333
- expect(spans).toHaveLength(1);
1334
- expect(spans[0]!.attributes['scores']).toEqual([95, 87, 92]);
1335
- });
1336
-
1337
- it('should support boolean array attributes', async () => {
1338
- const collector = createTraceCollector();
1339
-
1340
- await trace(async (ctx: TraceContext) => {
1341
- ctx.setAttribute('flags', [true, false, true]);
1342
- return 'done';
1343
- });
1344
-
1345
- const spans = collector.getSpans();
1346
- expect(spans).toHaveLength(1);
1347
- expect(spans[0]!.attributes['flags']).toEqual([true, false, true]);
1348
- });
1349
-
1350
- it('should support mixed attributes including arrays via setAttributes', async () => {
1351
- const collector = createTraceCollector();
1352
-
1353
- await trace(async (ctx: TraceContext) => {
1354
- ctx.setAttributes({
1355
- 'user.id': 'user_123',
1356
- environment: 'development',
1357
- version: '1.0.0',
1358
- tags: ['qa', 'test'],
1359
- scores: [1, 2, 3],
1360
- });
1361
- return 'done';
1362
- });
1363
-
1364
- const spans = collector.getSpans();
1365
- expect(spans).toHaveLength(1);
1366
- expect(spans[0]!.attributes['user.id']).toBe('user_123');
1367
- expect(spans[0]!.attributes['environment']).toBe('development');
1368
- expect(spans[0]!.attributes['tags']).toEqual(['qa', 'test']);
1369
- expect(spans[0]!.attributes['scores']).toEqual([1, 2, 3]);
1370
- });
1371
- });
1372
-
1373
- describe('Full OTel Span API', () => {
1374
- it('should support addEvent for span events', async () => {
1375
- const collector = createTraceCollector();
1376
-
1377
- // Verify the method can be called without error
1378
- const result = await trace(async (ctx: TraceContext) => {
1379
- ctx.addEvent('order.started', { 'order.id': '123' });
1380
- ctx.addEvent('items.fetched', { 'item.count': 5 });
1381
- return 'done';
1382
- });
1383
-
1384
- expect(result).toBe('done');
1385
- expect(collector.getSpans()).toHaveLength(1);
1386
- });
1387
-
1388
- it('should support updateName for dynamic span naming', async () => {
1389
- const collector = createTraceCollector();
1390
-
1391
- await trace('initial.name', async (ctx: TraceContext) => {
1392
- ctx.updateName('updated.name');
1393
- return 'done';
1394
- });
1395
-
1396
- const spans = collector.getSpans();
1397
- expect(spans).toHaveLength(1);
1398
- expect(spans[0]!.name).toBe('updated.name');
1399
- });
1400
-
1401
- it('should support isRecording', async () => {
1402
- const collector = createTraceCollector();
1403
- let wasRecording = false;
1404
-
1405
- await trace(async (ctx: TraceContext) => {
1406
- wasRecording = ctx.isRecording();
1407
- return 'done';
1408
- });
1409
-
1410
- expect(wasRecording).toBe(true);
1411
- expect(collector.getSpans()).toHaveLength(1);
1412
- });
1413
-
1414
- it('should support addLink for span links', async () => {
1415
- const collector = createTraceCollector();
1416
-
1417
- // Create a mock span context to link to
1418
- const linkContext = {
1419
- traceId: '0af7651916cd43dd8448eb211c80319c',
1420
- spanId: 'b7ad6b7169203331',
1421
- traceFlags: 1,
1422
- };
1423
-
1424
- // Verify the method can be called without error
1425
- const result = await trace(async (ctx: TraceContext) => {
1426
- ctx.addLink({ context: linkContext });
1427
- return 'done';
1428
- });
1429
-
1430
- expect(result).toBe('done');
1431
- expect(collector.getSpans()).toHaveLength(1);
1432
- });
1433
-
1434
- it('should support addLinks for multiple span links', async () => {
1435
- const collector = createTraceCollector();
1436
-
1437
- const links = [
1438
- {
1439
- context: {
1440
- traceId: '0af7651916cd43dd8448eb211c80319c',
1441
- spanId: 'b7ad6b7169203331',
1442
- traceFlags: 1,
1443
- },
1444
- },
1445
- {
1446
- context: {
1447
- traceId: '0af7651916cd43dd8448eb211c80319d',
1448
- spanId: 'b7ad6b7169203332',
1449
- traceFlags: 1,
1450
- },
1451
- },
1452
- ];
1453
-
1454
- // Verify the method can be called without error
1455
- const result = await trace(async (ctx: TraceContext) => {
1456
- ctx.addLinks(links);
1457
- return 'done';
1458
- });
1459
-
1460
- expect(result).toBe('done');
1461
- expect(collector.getSpans()).toHaveLength(1);
1462
- });
1463
- });
1464
- });