autotel-edge 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/chunk-F32WSLNX.js +309 -0
  4. package/dist/chunk-F32WSLNX.js.map +1 -0
  5. package/dist/events.d.ts +86 -0
  6. package/dist/events.js +157 -0
  7. package/dist/events.js.map +1 -0
  8. package/dist/index.d.ts +326 -0
  9. package/dist/index.js +921 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/logger.d.ts +89 -0
  12. package/dist/logger.js +81 -0
  13. package/dist/logger.js.map +1 -0
  14. package/dist/sampling.d.ts +166 -0
  15. package/dist/sampling.js +108 -0
  16. package/dist/sampling.js.map +1 -0
  17. package/dist/testing.d.ts +2 -0
  18. package/dist/testing.js +3 -0
  19. package/dist/testing.js.map +1 -0
  20. package/dist/types-Dj85cPUj.d.ts +182 -0
  21. package/package.json +88 -0
  22. package/src/api/logger.test.ts +367 -0
  23. package/src/api/logger.ts +197 -0
  24. package/src/compose.ts +243 -0
  25. package/src/core/buffer.ts +16 -0
  26. package/src/core/config.test.ts +388 -0
  27. package/src/core/config.ts +167 -0
  28. package/src/core/context.ts +224 -0
  29. package/src/core/exporter.ts +99 -0
  30. package/src/core/provider.ts +45 -0
  31. package/src/core/span.ts +222 -0
  32. package/src/core/spanprocessor.test.ts +521 -0
  33. package/src/core/spanprocessor.ts +232 -0
  34. package/src/core/trace-context.ts +66 -0
  35. package/src/core/tracer.test.ts +123 -0
  36. package/src/core/tracer.ts +216 -0
  37. package/src/events/index.test.ts +242 -0
  38. package/src/events/index.ts +338 -0
  39. package/src/events.ts +6 -0
  40. package/src/functional.test.ts +702 -0
  41. package/src/functional.ts +846 -0
  42. package/src/index.ts +81 -0
  43. package/src/logger.ts +13 -0
  44. package/src/sampling/index.test.ts +297 -0
  45. package/src/sampling/index.ts +276 -0
  46. package/src/sampling.ts +6 -0
  47. package/src/testing/index.ts +9 -0
  48. package/src/testing.ts +6 -0
  49. package/src/types.ts +267 -0
@@ -0,0 +1,846 @@
1
+ /**
2
+ * Functional API for autotel-edge
3
+ *
4
+ * Provides zero-boilerplate tracing helpers that mirror the Node.js runtime
5
+ * implementation while staying optimized for edge environments.
6
+ */
7
+
8
+ import {
9
+ trace as otelTrace,
10
+ SpanStatusCode,
11
+ type Span,
12
+ type AttributeValue,
13
+ } from '@opentelemetry/api';
14
+ import type { Sampler } from '@opentelemetry/sdk-trace-base';
15
+ import type { TraceContext } from './core/trace-context';
16
+ import { createTraceContext, setSpanName } from './core/trace-context';
17
+
18
+ // Re-export for convenience
19
+ export type { TraceContext } from './core/trace-context';
20
+
21
+ type AnyFn = (...args: any[]) => any;
22
+
23
+ const TRACE_FACTORY_SYMBOL = Symbol.for('autotel.edge.functional.factory');
24
+ const FACTORY_NAME_HINTS = new Set(['ctx', '_ctx', 'context', 'tracecontext', 'tracectx']);
25
+
26
+ const SINGLE_LINE_COMMENT_REGEX = /\/\/.*$/gm;
27
+ const MULTI_LINE_COMMENT_REGEX = /\/\*[\s\S]*?\*\//gm;
28
+ const PARAM_TOKEN_SANITIZE_REGEX = new RegExp(String.raw`[{}\[\]\s]`, 'g');
29
+
30
+ interface TraceFactoryMarked {
31
+ [TRACE_FACTORY_SYMBOL]?: true;
32
+ }
33
+
34
+ function markAsTraceFactory(fn: AnyFn): void {
35
+ try {
36
+ Object.defineProperty(fn, TRACE_FACTORY_SYMBOL, {
37
+ value: true,
38
+ configurable: true,
39
+ });
40
+ } catch {
41
+ (fn as TraceFactoryMarked)[TRACE_FACTORY_SYMBOL] = true;
42
+ }
43
+ }
44
+
45
+ function hasFactoryMark(fn: AnyFn): boolean {
46
+ return Boolean((fn as TraceFactoryMarked)[TRACE_FACTORY_SYMBOL]);
47
+ }
48
+
49
+ function sanitizeParameterToken(token: string): string {
50
+ const [firstToken] = token.split('=');
51
+ return (firstToken ?? '').replaceAll(PARAM_TOKEN_SANITIZE_REGEX, '').trim();
52
+ }
53
+
54
+ function getFirstParameterToken(fn: AnyFn): string | null {
55
+ let source = Function.prototype.toString.call(fn);
56
+ source = source
57
+ .replaceAll(MULTI_LINE_COMMENT_REGEX, '')
58
+ .replaceAll(SINGLE_LINE_COMMENT_REGEX, '')
59
+ .trim();
60
+
61
+ const arrowMatch = source.match(/^(?:async\s*)?(?:\(([^)]*)\)|([^=()]+))\s*=>/);
62
+ if (arrowMatch) {
63
+ const params = (arrowMatch[1] ?? arrowMatch[2] ?? '').split(',');
64
+ const first = params[0]?.trim();
65
+ if (first) {
66
+ return sanitizeParameterToken(first);
67
+ }
68
+ return null;
69
+ }
70
+
71
+ const functionMatch = source.match(/^[^(]*\(([^)]*)\)/);
72
+ if (functionMatch) {
73
+ const params = functionMatch[1]?.split(',');
74
+ const first = params?.[0]?.trim();
75
+ if (first) {
76
+ return sanitizeParameterToken(first);
77
+ }
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ function looksLikeTraceFactory(fn: AnyFn): boolean {
84
+ if (hasFactoryMark(fn)) {
85
+ return true;
86
+ }
87
+
88
+ if (fn.length === 0) {
89
+ return false;
90
+ }
91
+
92
+ const firstParam = getFirstParameterToken(fn);
93
+ if (!firstParam) {
94
+ return false;
95
+ }
96
+
97
+ const normalized = firstParam.toLowerCase();
98
+ if (
99
+ FACTORY_NAME_HINTS.has(normalized) ||
100
+ normalized.startsWith('ctx') ||
101
+ normalized.startsWith('_ctx') ||
102
+ normalized.startsWith('trace')
103
+ ) {
104
+ return true;
105
+ }
106
+
107
+ return false;
108
+ }
109
+
110
+ /**
111
+ * Check if a function that takes ctx returns another function (factory pattern)
112
+ * vs returning a value directly (immediate execution pattern)
113
+ */
114
+ function isFactoryReturningFunction(
115
+ fnWithCtx: (ctx: TraceContext) => unknown,
116
+ ): boolean {
117
+ try {
118
+ const result = fnWithCtx(createDummyCtx());
119
+ return typeof result === 'function';
120
+ } catch {
121
+ // If the function throws when called with dummy ctx, assume it's immediate execution
122
+ // since factory functions typically just return a function and don't execute logic
123
+ return false;
124
+ }
125
+ }
126
+
127
+ function isTraceFactoryFunction<TArgs extends any[], TReturn>(
128
+ fn:
129
+ | ((...args: TArgs) => TReturn | Promise<TReturn>)
130
+ | ((ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn>),
131
+ ): fn is (ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn> {
132
+ if (typeof fn !== 'function') {
133
+ return false;
134
+ }
135
+
136
+ if (hasFactoryMark(fn as AnyFn)) {
137
+ return true;
138
+ }
139
+
140
+ if (looksLikeTraceFactory(fn as AnyFn)) {
141
+ markAsTraceFactory(fn as AnyFn);
142
+ return true;
143
+ }
144
+
145
+ return false;
146
+ }
147
+
148
+ function ensureTraceFactory<TArgs extends any[], TReturn>(
149
+ fnOrFactory:
150
+ | ((...args: TArgs) => TReturn | Promise<TReturn>)
151
+ | ((ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn>),
152
+ ): (ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn> {
153
+ if (isTraceFactoryFunction(fnOrFactory)) {
154
+ return fnOrFactory;
155
+ }
156
+
157
+ const plainFn = fnOrFactory as (...args: TArgs) => TReturn | Promise<TReturn>;
158
+ const factory = (ctx: TraceContext) => {
159
+ void ctx;
160
+ return plainFn;
161
+ };
162
+ markAsTraceFactory(factory);
163
+ return factory;
164
+ }
165
+
166
+ type ExtractFunctionSignature<T> = T extends (ctx: TraceContext) => infer F
167
+ ? F extends (...args: infer Args) => infer Return
168
+ ? (...args: Args) => Return
169
+ : never
170
+ : never;
171
+
172
+ type WrappedFunction<TArgs extends any[], TReturn> = (...args: TArgs) => TReturn | Promise<TReturn>;
173
+
174
+ /**
175
+ * trace function options
176
+ */
177
+ export interface traceOptions<TArgs extends any[] = any[], TReturn = any> {
178
+ name?: string;
179
+ serviceName?: string;
180
+ sampler?: Sampler;
181
+ attributesFromArgs?: (args: TArgs) => Record<string, unknown>;
182
+ attributesFromResult?: (result: TReturn) => Record<string, unknown>;
183
+ attributes?: Record<string, unknown>;
184
+ }
185
+
186
+ const MAX_ERROR_MESSAGE_LENGTH = 500;
187
+
188
+ function createDummyCtx(): TraceContext {
189
+ return {
190
+ traceId: '',
191
+ spanId: '',
192
+ correlationId: '',
193
+ setAttribute: () => {},
194
+ setAttributes: () => {},
195
+ setStatus: () => {},
196
+ recordException: () => {},
197
+ } as unknown as TraceContext;
198
+ }
199
+
200
+ function truncateErrorMessage(message: string): string {
201
+ if (message.length <= MAX_ERROR_MESSAGE_LENGTH) {
202
+ return message;
203
+ }
204
+ return `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}... (truncated)`;
205
+ }
206
+
207
+ type InstrumentableFunction<TArgs extends any[] = any[], TReturn = any> = ((
208
+ ...args: TArgs
209
+ ) => TReturn | Promise<TReturn>) & {
210
+ displayName?: string;
211
+ name?: string;
212
+ };
213
+
214
+ function inferFunctionName<TArgs extends any[] = any[], TReturn = any>(
215
+ fn: InstrumentableFunction<TArgs, TReturn>,
216
+ ): string | undefined {
217
+ const displayName = (fn as { displayName?: string }).displayName;
218
+ if (displayName) {
219
+ return displayName;
220
+ }
221
+
222
+ if (fn.name && fn.name !== 'anonymous') {
223
+ return fn.name;
224
+ }
225
+
226
+ const source = Function.prototype.toString.call(fn);
227
+ const match = source.match(/function\s+([^(\s]+)/);
228
+ if (match && match[1] && match[1] !== 'anonymous') {
229
+ return match[1];
230
+ }
231
+
232
+ return undefined;
233
+ }
234
+
235
+ function getSpanName<TArgs extends any[], TReturn>(
236
+ options: traceOptions<TArgs, TReturn>,
237
+ fn: InstrumentableFunction<TArgs, TReturn>,
238
+ variableName?: string,
239
+ ): string {
240
+ if (options.name) {
241
+ return options.name;
242
+ }
243
+
244
+ let fnName = variableName ?? inferFunctionName(fn);
245
+ fnName = fnName || 'anonymous';
246
+
247
+ if (options.serviceName) {
248
+ return `${options.serviceName}.${fnName}`;
249
+ }
250
+
251
+ if (fnName && fnName !== 'anonymous') {
252
+ return fnName;
253
+ }
254
+
255
+ return 'unknown';
256
+ }
257
+
258
+ function isAsyncFunction(fn: unknown): boolean {
259
+ return typeof fn === 'function' && fn.constructor?.name === 'AsyncFunction';
260
+ }
261
+
262
+ const INSTRUMENTED_SYMBOL = Symbol.for('autotel.edge.functional.instrumented');
263
+
264
+ function wrapWithTracingAsync<TArgs extends any[], TReturn>(
265
+ fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
266
+ options: traceOptions<TArgs, TReturn>,
267
+ variableName?: string,
268
+ ): (...args: TArgs) => Promise<TReturn> {
269
+ const tempFn = fnFactory(createDummyCtx());
270
+ const spanName = getSpanName(options, tempFn, variableName);
271
+
272
+ const wrappedFunction = async function wrappedFunction(this: unknown, ...args: TArgs): Promise<TReturn> {
273
+ const tracer = otelTrace.getTracer('autotel-edge');
274
+ const spanOptions: Record<string, unknown> = options.sampler ? { sampler: options.sampler } : {};
275
+
276
+ return tracer.startActiveSpan(spanName, spanOptions, async (span) => {
277
+ setSpanName(span, spanName);
278
+
279
+ try {
280
+ const actualFn = fnFactory(createTraceContext(span));
281
+
282
+ if (options.attributes) {
283
+ span.setAttributes(options.attributes as Record<string, AttributeValue>);
284
+ }
285
+
286
+ if (options.attributesFromArgs) {
287
+ const argsAttrs = options.attributesFromArgs(args);
288
+ span.setAttributes(argsAttrs as Record<string, AttributeValue>);
289
+ }
290
+
291
+ const result = await actualFn(...args);
292
+
293
+ if (options.attributesFromResult) {
294
+ const resultAttrs = options.attributesFromResult(result);
295
+ span.setAttributes(resultAttrs as Record<string, AttributeValue>);
296
+ }
297
+
298
+ span.setAttribute('code.function', spanName);
299
+ span.setStatus({ code: SpanStatusCode.OK });
300
+ span.end();
301
+ return result;
302
+ } catch (error) {
303
+ const message = truncateErrorMessage(
304
+ error instanceof Error ? error.message : String(error ?? 'Unknown error'),
305
+ );
306
+ span.setAttribute('code.function', spanName);
307
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
308
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
309
+ span.end();
310
+ throw error;
311
+ }
312
+ });
313
+ };
314
+
315
+ Object.defineProperty(wrappedFunction, 'name', {
316
+ value: tempFn.name || 'trace',
317
+ configurable: true,
318
+ });
319
+
320
+ (wrappedFunction as any)[INSTRUMENTED_SYMBOL] = true;
321
+
322
+ return wrappedFunction;
323
+ }
324
+
325
+ function wrapWithTracingSync<TArgs extends any[], TReturn>(
326
+ fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
327
+ options: traceOptions<TArgs, TReturn>,
328
+ variableName?: string,
329
+ ): (...args: TArgs) => TReturn {
330
+ const tempFn = fnFactory(createDummyCtx());
331
+ const spanName = getSpanName(options, tempFn, variableName);
332
+
333
+ const wrappedFunction = function wrappedFunction(this: unknown, ...args: TArgs): TReturn {
334
+ const tracer = otelTrace.getTracer('autotel-edge');
335
+ const spanOptions: Record<string, unknown> = options.sampler ? { sampler: options.sampler } : {};
336
+
337
+ return tracer.startActiveSpan(spanName, spanOptions, (span) => {
338
+ setSpanName(span, spanName);
339
+
340
+ try {
341
+ const actualFn = fnFactory(createTraceContext(span));
342
+
343
+ if (options.attributes) {
344
+ span.setAttributes(options.attributes as Record<string, AttributeValue>);
345
+ }
346
+
347
+ if (options.attributesFromArgs) {
348
+ const argsAttrs = options.attributesFromArgs(args);
349
+ span.setAttributes(argsAttrs as Record<string, AttributeValue>);
350
+ }
351
+
352
+ const result = actualFn(...args);
353
+
354
+ if (options.attributesFromResult) {
355
+ const resultAttrs = options.attributesFromResult(result as TReturn);
356
+ span.setAttributes(resultAttrs as Record<string, AttributeValue>);
357
+ }
358
+
359
+ span.setAttribute('code.function', spanName);
360
+ span.setStatus({ code: SpanStatusCode.OK });
361
+ span.end();
362
+ return result;
363
+ } catch (error) {
364
+ const message = truncateErrorMessage(
365
+ error instanceof Error ? error.message : String(error ?? 'Unknown error'),
366
+ );
367
+ span.setAttribute('code.function', spanName);
368
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
369
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
370
+ span.end();
371
+ throw error;
372
+ }
373
+ });
374
+ };
375
+
376
+ Object.defineProperty(wrappedFunction, 'name', {
377
+ value: tempFn.name || 'trace',
378
+ configurable: true,
379
+ });
380
+
381
+ (wrappedFunction as any)[INSTRUMENTED_SYMBOL] = true;
382
+
383
+ return wrappedFunction;
384
+ }
385
+
386
+ function wrapFactoryWithTracing<TArgs extends any[], TReturn>(
387
+ fnOrFactory:
388
+ | ((...args: TArgs) => TReturn | Promise<TReturn>)
389
+ | ((ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn>),
390
+ options: traceOptions<TArgs, TReturn>,
391
+ variableName?: string,
392
+ ): WrappedFunction<TArgs, TReturn> {
393
+ const factory = ensureTraceFactory(fnOrFactory);
394
+ const sampleFn = factory(createDummyCtx());
395
+ const useAsyncWrapper = isAsyncFunction(sampleFn);
396
+
397
+ if (useAsyncWrapper) {
398
+ return wrapWithTracingAsync(
399
+ factory as (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
400
+ options,
401
+ variableName,
402
+ ) as WrappedFunction<TArgs, TReturn>;
403
+ }
404
+
405
+ return wrapWithTracingSync(
406
+ factory as (ctx: TraceContext) => (...args: TArgs) => TReturn,
407
+ options,
408
+ variableName,
409
+ ) as WrappedFunction<TArgs, TReturn>;
410
+ }
411
+
412
+ /**
413
+ * Execute a function immediately within a trace span
414
+ * Used for the immediate execution pattern: trace((ctx) => result)
415
+ */
416
+ function executeImmediately<TReturn = any>(
417
+ fn: (ctx: TraceContext) => TReturn | Promise<TReturn>,
418
+ options: traceOptions<any[], any>,
419
+ ): TReturn | Promise<TReturn> {
420
+ const tracer = otelTrace.getTracer('@autotel/edge');
421
+ const spanName = options.name || 'anonymous';
422
+
423
+ return tracer.startActiveSpan(spanName, (span) => {
424
+ try {
425
+ setSpanName(span, spanName);
426
+ const ctxValue = createTraceContext(span);
427
+
428
+ const onSuccess = (result: TReturn) => {
429
+ span.setStatus({ code: SpanStatusCode.OK });
430
+ if (options.attributes) {
431
+ for (const [key, value] of Object.entries(options.attributes)) {
432
+ span.setAttribute(key, value as AttributeValue);
433
+ }
434
+ }
435
+ span.end();
436
+ return result;
437
+ };
438
+
439
+ const onError = (error: unknown): never => {
440
+ const errorMessage =
441
+ error instanceof Error ? error.message : 'Unknown error';
442
+ const truncatedMessage = truncateErrorMessage(errorMessage);
443
+
444
+ span.setStatus({
445
+ code: SpanStatusCode.ERROR,
446
+ message: truncatedMessage,
447
+ });
448
+
449
+ span.setAttribute('error', true);
450
+ span.setAttribute('exception.type',
451
+ error instanceof Error ? error.constructor.name : 'Error');
452
+ span.setAttribute('exception.message', truncatedMessage);
453
+
454
+ span.recordException(
455
+ error instanceof Error ? error : new Error(String(error)),
456
+ );
457
+
458
+ span.end();
459
+ throw error;
460
+ };
461
+
462
+ const result = fn(ctxValue);
463
+
464
+ // Check if result is a Promise
465
+ if (result instanceof Promise) {
466
+ return result.then(onSuccess, onError);
467
+ }
468
+
469
+ return onSuccess(result);
470
+ } catch (error) {
471
+ const errorMessage =
472
+ error instanceof Error ? error.message : 'Unknown error';
473
+ const truncatedMessage = truncateErrorMessage(errorMessage);
474
+
475
+ span.setStatus({
476
+ code: SpanStatusCode.ERROR,
477
+ message: truncatedMessage,
478
+ });
479
+
480
+ span.setAttribute('error', true);
481
+ span.setAttribute('exception.message', truncatedMessage);
482
+
483
+ span.recordException(
484
+ error instanceof Error ? error : new Error(String(error)),
485
+ );
486
+
487
+ span.end();
488
+ throw error;
489
+ }
490
+ });
491
+ }
492
+
493
+ // Sync overloads - Factory pattern with explicit return type helps TypeScript infer
494
+ // Overload 1a: Plain sync function with no args (auto-inferred name)
495
+ export function trace<TReturn = any>(
496
+ fn: () => TReturn,
497
+ ): () => TReturn;
498
+ // Overload 1b: Plain sync function (auto-inferred name)
499
+ export function trace<TArgs extends any[], TReturn = any>(
500
+ fn: (...args: TArgs) => TReturn,
501
+ ): (...args: TArgs) => TReturn;
502
+
503
+ // Overload 1c: Factory sync function with no args returning explicit type (auto-inferred name)
504
+ export function trace<TReturn = any>(
505
+ fnFactory: (ctx: TraceContext) => () => TReturn,
506
+ ): () => TReturn;
507
+ // Overload 1d: Immediate execution - sync function with context (NEW PATTERN)
508
+ export function trace<TReturn = any>(
509
+ fn: (ctx: TraceContext) => TReturn,
510
+ ): TReturn;
511
+ // Overload 1e: Factory sync function - use conditional type to extract signature (MUST come before generic)
512
+ // This overload is more specific and helps TypeScript infer types from factory functions
513
+ export function trace<TFactory extends (ctx: TraceContext) => (...args: any[]) => any>(
514
+ fnFactory: TFactory,
515
+ ): ExtractFunctionSignature<TFactory>;
516
+ // Overload 1f: Generic factory sync function (fallback)
517
+ export function trace<TArgs extends any[], TReturn = any>(
518
+ fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
519
+ ): (...args: TArgs) => TReturn;
520
+
521
+ // Overload 2a: Name + plain sync function
522
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
523
+ name: string,
524
+ fn: (...args: TArgs) => TReturn,
525
+ ): (...args: TArgs) => TReturn;
526
+
527
+ // Overload 2b: Name + immediate execution sync with context
528
+ export function trace<TReturn = any>(
529
+ name: string,
530
+ fn: (ctx: TraceContext) => TReturn,
531
+ ): TReturn;
532
+
533
+ // Overload 2c: Name + factory sync function
534
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
535
+ name: string,
536
+ fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
537
+ ): (...args: TArgs) => TReturn;
538
+
539
+ // Overload 3a: Options + plain sync function
540
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
541
+ options: traceOptions<TArgs, TReturn>,
542
+ fn: (...args: TArgs) => TReturn,
543
+ ): (...args: TArgs) => TReturn;
544
+
545
+ // Overload 3b: Options + immediate execution sync with context
546
+ export function trace<TReturn = any>(
547
+ options: traceOptions<[], TReturn>,
548
+ fn: (ctx: TraceContext) => TReturn,
549
+ ): TReturn;
550
+
551
+ // Overload 3c: Options + factory sync function
552
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
553
+ options: traceOptions<TArgs, TReturn>,
554
+ fnFactory: (ctx: TraceContext) => (...args: TArgs) => TReturn,
555
+ ): (...args: TArgs) => TReturn;
556
+
557
+ // Async overloads
558
+ // Overload 4a: Plain async function with no args (auto-inferred name)
559
+ export function trace<TReturn = any>(
560
+ fn: () => Promise<TReturn>,
561
+ ): () => Promise<TReturn>;
562
+ // Overload 4b: Plain async function (auto-inferred name)
563
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
564
+ fn: (...args: TArgs) => Promise<TReturn>,
565
+ ): (...args: TArgs) => Promise<TReturn>;
566
+
567
+ // Overload 4c: Immediate execution - async function with context (NEW PATTERN)
568
+ export function trace<TReturn = any>(
569
+ fn: (ctx: TraceContext) => Promise<TReturn>,
570
+ ): Promise<TReturn>;
571
+ // Overload 4d: Factory async function with no args (auto-inferred name)
572
+ export function trace<TReturn = any>(
573
+ fnFactory: (ctx: TraceContext) => () => Promise<TReturn>,
574
+ ): () => Promise<TReturn>;
575
+ // Overload 4e: Factory async function (auto-inferred name)
576
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
577
+ fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
578
+ ): (...args: TArgs) => Promise<TReturn>;
579
+
580
+ // Overload 5a: Name + plain async function
581
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
582
+ name: string,
583
+ fn: (...args: TArgs) => Promise<TReturn>,
584
+ ): (...args: TArgs) => Promise<TReturn>;
585
+
586
+ // Overload 5b: Name + immediate execution async with context
587
+ export function trace<TReturn = any>(
588
+ name: string,
589
+ fn: (ctx: TraceContext) => Promise<TReturn>,
590
+ ): Promise<TReturn>;
591
+
592
+ // Overload 5c: Name + factory async function
593
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
594
+ name: string,
595
+ fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
596
+ ): (...args: TArgs) => Promise<TReturn>;
597
+
598
+ // Overload 6a: Options + plain async function
599
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
600
+ options: traceOptions<TArgs, TReturn>,
601
+ fn: (...args: TArgs) => Promise<TReturn>,
602
+ ): (...args: TArgs) => Promise<TReturn>;
603
+
604
+ // Overload 6b: Options + immediate execution async with context
605
+ export function trace<TReturn = any>(
606
+ options: traceOptions<[], TReturn>,
607
+ fn: (ctx: TraceContext) => Promise<TReturn>,
608
+ ): Promise<TReturn>;
609
+
610
+ // Overload 6c: Options + factory async function
611
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
612
+ options: traceOptions<TArgs, TReturn>,
613
+ fnFactory: (ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>,
614
+ ): (...args: TArgs) => Promise<TReturn>;
615
+
616
+ // Implementation
617
+ export function trace<TArgs extends any[] = any[], TReturn = any>(
618
+ fnOrNameOrOptions:
619
+ | ((...args: TArgs) => TReturn)
620
+ | ((...args: TArgs) => Promise<TReturn>)
621
+ | ((ctx: TraceContext) => (...args: TArgs) => TReturn)
622
+ | ((ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>)
623
+ | ((ctx: TraceContext) => TReturn)
624
+ | ((ctx: TraceContext) => Promise<TReturn>)
625
+ | string
626
+ | traceOptions<TArgs, TReturn>,
627
+ maybeFn?:
628
+ | ((...args: TArgs) => TReturn)
629
+ | ((...args: TArgs) => Promise<TReturn>)
630
+ | ((ctx: TraceContext) => (...args: TArgs) => TReturn)
631
+ | ((ctx: TraceContext) => (...args: TArgs) => Promise<TReturn>)
632
+ | ((ctx: TraceContext) => TReturn)
633
+ | ((ctx: TraceContext) => Promise<TReturn>),
634
+ ): WrappedFunction<TArgs, TReturn> | TReturn | Promise<TReturn> {
635
+ // Handle: trace(fn) - single argument
636
+ if (typeof fnOrNameOrOptions === 'function') {
637
+ // Check if it's immediate execution pattern: (ctx) => result
638
+ if (
639
+ looksLikeTraceFactory(fnOrNameOrOptions as AnyFn) &&
640
+ !isFactoryReturningFunction(fnOrNameOrOptions as (ctx: TraceContext) => unknown)
641
+ ) {
642
+ // Immediate execution pattern
643
+ return executeImmediately(
644
+ fnOrNameOrOptions as (ctx: TraceContext) => TReturn | Promise<TReturn>,
645
+ {},
646
+ ) as WrappedFunction<TArgs, TReturn> | TReturn | Promise<TReturn>;
647
+ }
648
+ // Factory pattern or plain function
649
+ return wrapFactoryWithTracing(
650
+ fnOrNameOrOptions as (...args: TArgs) => TReturn,
651
+ {} as traceOptions<TArgs, TReturn>,
652
+ );
653
+ }
654
+
655
+ // Handle: trace(name, fn) or trace(options, fn) - two arguments
656
+ if (typeof fnOrNameOrOptions === 'string') {
657
+ if (!maybeFn) {
658
+ throw new Error('trace(name, fn): fn is required');
659
+ }
660
+ // Check if it's immediate execution pattern
661
+ if (
662
+ looksLikeTraceFactory(maybeFn as AnyFn) &&
663
+ !isFactoryReturningFunction(maybeFn as (ctx: TraceContext) => unknown)
664
+ ) {
665
+ // Immediate execution pattern with name
666
+ return executeImmediately(
667
+ maybeFn as (ctx: TraceContext) => TReturn | Promise<TReturn>,
668
+ { name: fnOrNameOrOptions },
669
+ ) as WrappedFunction<TArgs, TReturn> | TReturn | Promise<TReturn>;
670
+ }
671
+ return wrapFactoryWithTracing(
672
+ maybeFn as (...args: TArgs) => TReturn,
673
+ { name: fnOrNameOrOptions } as traceOptions<TArgs, TReturn>,
674
+ );
675
+ }
676
+
677
+ // Handle: trace(options, fn)
678
+ if (!maybeFn) {
679
+ throw new Error('trace(options, fn): fn is required');
680
+ }
681
+
682
+ // Check if it's immediate execution pattern
683
+ if (
684
+ looksLikeTraceFactory(maybeFn as AnyFn) &&
685
+ !isFactoryReturningFunction(maybeFn as (ctx: TraceContext) => unknown)
686
+ ) {
687
+ // Immediate execution pattern with options
688
+ return executeImmediately(
689
+ maybeFn as (ctx: TraceContext) => TReturn | Promise<TReturn>,
690
+ fnOrNameOrOptions as traceOptions<any[], any>,
691
+ ) as WrappedFunction<TArgs, TReturn> | TReturn | Promise<TReturn>;
692
+ }
693
+
694
+ return wrapFactoryWithTracing(
695
+ maybeFn as (...args: TArgs) => TReturn,
696
+ fnOrNameOrOptions as traceOptions<TArgs, TReturn>,
697
+ );
698
+ }
699
+
700
+ export function withTracing<TArgs extends any[] = any[], TReturn = any>(
701
+ options: Omit<traceOptions<TArgs, TReturn>, 'name'>,
702
+ ) {
703
+ return (
704
+ fnOrFactory:
705
+ | ((...args: TArgs) => TReturn | Promise<TReturn>)
706
+ | ((ctx: TraceContext) => (...args: TArgs) => TReturn | Promise<TReturn>)
707
+ ): WrappedFunction<TArgs, TReturn> => wrapFactoryWithTracing(fnOrFactory, options as traceOptions<TArgs, TReturn>);
708
+ }
709
+
710
+ function shouldSkip(key: string, fn: Function, skip?: (string | RegExp | ((key: string, fn: Function) => boolean))[]): boolean {
711
+ if (key.startsWith('_')) {
712
+ return true;
713
+ }
714
+
715
+ if (!skip || skip.length === 0) {
716
+ return false;
717
+ }
718
+
719
+ for (const pattern of skip) {
720
+ if (typeof pattern === 'string' && key === pattern) {
721
+ return true;
722
+ }
723
+ if (pattern instanceof RegExp && pattern.test(key)) {
724
+ return true;
725
+ }
726
+ if (typeof pattern === 'function' && pattern(key, fn)) {
727
+ return true;
728
+ }
729
+ }
730
+
731
+ return false;
732
+ }
733
+
734
+ export interface InstrumentOptions extends traceOptions {
735
+ functions: Record<string, any>;
736
+ overrides?: Record<string, Partial<traceOptions>>;
737
+ skip?: (string | RegExp | ((key: string, fn: Function) => boolean))[];
738
+ }
739
+
740
+ export function instrument<T extends Record<string, any>>(
741
+ options: InstrumentOptions,
742
+ ): T {
743
+ const { functions, ...tracingOptions } = options;
744
+ const instrumented: Record<string, any> = {};
745
+
746
+ for (const key of Object.keys(functions)) {
747
+ const fn = functions[key];
748
+
749
+ if (typeof fn !== 'function') {
750
+ instrumented[key] = fn;
751
+ continue;
752
+ }
753
+
754
+ if (shouldSkip(key, fn, tracingOptions.skip)) {
755
+ instrumented[key] = fn;
756
+ continue;
757
+ }
758
+
759
+ const fnOptions: traceOptions = {
760
+ ...tracingOptions,
761
+ ...tracingOptions.overrides?.[key],
762
+ name: tracingOptions.overrides?.[key]?.name ?? tracingOptions.name,
763
+ };
764
+
765
+ const boundFn = fn.bind(functions);
766
+ const fnFactory = (_ctx: TraceContext) => boundFn as AnyFn;
767
+
768
+ instrumented[key] = wrapFactoryWithTracing(fnFactory, fnOptions, key);
769
+ }
770
+
771
+ return instrumented as T;
772
+ }
773
+
774
+ export interface SpanOptions {
775
+ name: string;
776
+ attributes?: Record<string, string | number | boolean>;
777
+ }
778
+
779
+ export function span<T = unknown>(options: SpanOptions, fn: (span: Span) => T): T;
780
+ export function span<T = unknown>(
781
+ options: SpanOptions,
782
+ fn: (span: Span) => Promise<T>,
783
+ ): Promise<T>;
784
+ export function span<T = unknown>(
785
+ options: SpanOptions,
786
+ fn: (span: Span) => T | Promise<T>,
787
+ ): T | Promise<T> {
788
+ const tracer = otelTrace.getTracer('autotel-edge');
789
+
790
+ const execute = (span: Span) => {
791
+ setSpanName(span, options.name);
792
+
793
+ try {
794
+ if (options.attributes) {
795
+ for (const [key, value] of Object.entries(options.attributes)) {
796
+ span.setAttribute(key, value);
797
+ }
798
+ }
799
+
800
+ const result = fn(span);
801
+
802
+ if (result instanceof Promise) {
803
+ return result
804
+ .then((value) => {
805
+ span.setAttribute('code.function', options.name);
806
+ span.setStatus({ code: SpanStatusCode.OK });
807
+ span.end();
808
+ return value;
809
+ })
810
+ .catch((error) => {
811
+ const message = truncateErrorMessage(
812
+ error instanceof Error ? error.message : String(error ?? 'Unknown error'),
813
+ );
814
+ span.setAttribute('code.function', options.name);
815
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
816
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
817
+ span.end();
818
+ throw error;
819
+ });
820
+ }
821
+
822
+ span.setAttribute('code.function', options.name);
823
+ span.setStatus({ code: SpanStatusCode.OK });
824
+ span.end();
825
+ return result;
826
+ } catch (error) {
827
+ const message = truncateErrorMessage(
828
+ error instanceof Error ? error.message : String(error ?? 'Unknown error'),
829
+ );
830
+ span.setAttribute('code.function', options.name);
831
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
832
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
833
+ span.end();
834
+ throw error;
835
+ }
836
+ };
837
+
838
+ const result = tracer.startActiveSpan(options.name, execute);
839
+
840
+ if (result instanceof Promise) {
841
+ return result;
842
+ }
843
+
844
+ return result as T;
845
+ }
846
+