autotel 3.0.0 → 3.0.4

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 (114) hide show
  1. package/README.md +21 -4
  2. package/dist/attribute-redacting-processor.cjs +8 -8
  3. package/dist/attribute-redacting-processor.d.cts +10 -1
  4. package/dist/attribute-redacting-processor.d.ts +10 -1
  5. package/dist/attribute-redacting-processor.js +1 -1
  6. package/dist/attributes.cjs +21 -21
  7. package/dist/attributes.js +2 -2
  8. package/dist/auto.cjs +3 -3
  9. package/dist/auto.js +2 -2
  10. package/dist/{chunk-IUDXKLS4.js → chunk-34X3TKHA.js} +3 -3
  11. package/dist/{chunk-IUDXKLS4.js.map → chunk-34X3TKHA.js.map} +1 -1
  12. package/dist/{chunk-3QMFLJHJ.js → chunk-4LF6FV2V.js} +3 -3
  13. package/dist/{chunk-3QMFLJHJ.js.map → chunk-4LF6FV2V.js.map} +1 -1
  14. package/dist/{chunk-L7JDUDJD.cjs → chunk-AAYCDHH6.cjs} +7 -7
  15. package/dist/{chunk-L7JDUDJD.cjs.map → chunk-AAYCDHH6.cjs.map} +1 -1
  16. package/dist/{chunk-DWOBIBLY.cjs → chunk-AY2SY3MO.cjs} +5 -5
  17. package/dist/{chunk-DWOBIBLY.cjs.map → chunk-AY2SY3MO.cjs.map} +1 -1
  18. package/dist/{chunk-563EL6O6.cjs → chunk-BPO2PQ3T.cjs} +12 -8
  19. package/dist/chunk-BPO2PQ3T.cjs.map +1 -0
  20. package/dist/{chunk-ZSABTI3C.cjs → chunk-DAZ7EGR4.cjs} +17 -17
  21. package/dist/{chunk-ZSABTI3C.cjs.map → chunk-DAZ7EGR4.cjs.map} +1 -1
  22. package/dist/{chunk-ER43K7ES.js → chunk-DDXIUZEG.js} +3 -3
  23. package/dist/{chunk-ER43K7ES.js.map → chunk-DDXIUZEG.js.map} +1 -1
  24. package/dist/{chunk-JKIMEPI2.cjs → chunk-DQ2SUROF.cjs} +4 -4
  25. package/dist/{chunk-JKIMEPI2.cjs.map → chunk-DQ2SUROF.cjs.map} +1 -1
  26. package/dist/{chunk-DAAJLUTO.js → chunk-F3TNRW2P.js} +6 -5
  27. package/dist/chunk-F3TNRW2P.js.map +1 -0
  28. package/dist/{chunk-7HNQYHK4.js → chunk-HBLWOI6P.js} +3 -3
  29. package/dist/{chunk-7HNQYHK4.js.map → chunk-HBLWOI6P.js.map} +1 -1
  30. package/dist/{chunk-TDNKIHKT.js → chunk-JVWJDHDB.js} +13 -4
  31. package/dist/chunk-JVWJDHDB.js.map +1 -0
  32. package/dist/{chunk-CJ4PD2TZ.cjs → chunk-KKGM42RQ.cjs} +13 -13
  33. package/dist/{chunk-CJ4PD2TZ.cjs.map → chunk-KKGM42RQ.cjs.map} +1 -1
  34. package/dist/{chunk-KHGA4OST.cjs → chunk-LMFPZHI4.cjs} +5 -5
  35. package/dist/{chunk-KHGA4OST.cjs.map → chunk-LMFPZHI4.cjs.map} +1 -1
  36. package/dist/{chunk-CMNGGTQL.cjs → chunk-NXLRY2CE.cjs} +13 -4
  37. package/dist/chunk-NXLRY2CE.cjs.map +1 -0
  38. package/dist/{chunk-4DAG3RFS.js → chunk-OM4OSBOP.js} +4 -4
  39. package/dist/{chunk-4DAG3RFS.js.map → chunk-OM4OSBOP.js.map} +1 -1
  40. package/dist/{chunk-MOK3E54E.cjs → chunk-WSGAHSZQ.cjs} +34 -33
  41. package/dist/chunk-WSGAHSZQ.cjs.map +1 -0
  42. package/dist/{chunk-QG3U5ONP.js → chunk-Z7VAOK5X.js} +3 -3
  43. package/dist/{chunk-QG3U5ONP.js.map → chunk-Z7VAOK5X.js.map} +1 -1
  44. package/dist/{chunk-W35FVJBC.js → chunk-ZDPIWKWD.js} +9 -5
  45. package/dist/chunk-ZDPIWKWD.js.map +1 -0
  46. package/dist/correlation-id.cjs +11 -11
  47. package/dist/correlation-id.js +3 -3
  48. package/dist/decorators.cjs +5 -5
  49. package/dist/decorators.js +4 -4
  50. package/dist/event.cjs +7 -7
  51. package/dist/event.js +4 -4
  52. package/dist/functional.cjs +11 -11
  53. package/dist/functional.d.cts +20 -17
  54. package/dist/functional.d.ts +20 -17
  55. package/dist/functional.js +4 -4
  56. package/dist/http.cjs +4 -4
  57. package/dist/http.js +3 -3
  58. package/dist/index.cjs +226 -92
  59. package/dist/index.cjs.map +1 -1
  60. package/dist/index.d.cts +67 -3
  61. package/dist/index.d.ts +67 -3
  62. package/dist/index.js +138 -15
  63. package/dist/index.js.map +1 -1
  64. package/dist/instrumentation.cjs +9 -9
  65. package/dist/instrumentation.js +2 -2
  66. package/dist/messaging.cjs +8 -8
  67. package/dist/messaging.js +5 -5
  68. package/dist/semantic-helpers.cjs +9 -9
  69. package/dist/semantic-helpers.js +5 -5
  70. package/dist/webhook.cjs +6 -6
  71. package/dist/webhook.js +4 -4
  72. package/dist/workflow-distributed.cjs +6 -6
  73. package/dist/workflow-distributed.js +4 -4
  74. package/dist/workflow.cjs +9 -9
  75. package/dist/workflow.js +5 -5
  76. package/package.json +43 -45
  77. package/skills/analyze-traces/SKILL.md +178 -0
  78. package/skills/autotel-core/SKILL.md +0 -7
  79. package/skills/autotel-events/SKILL.md +0 -6
  80. package/skills/autotel-frameworks/SKILL.md +0 -9
  81. package/skills/autotel-instrumentation/SKILL.md +0 -7
  82. package/skills/autotel-request-logging/SKILL.md +0 -8
  83. package/skills/autotel-structured-errors/SKILL.md +0 -7
  84. package/skills/build-audit-trails/SKILL.md +302 -0
  85. package/skills/debug-missing-spans/SKILL.md +248 -0
  86. package/skills/migrate-to-autotel/SKILL.md +268 -0
  87. package/skills/review-otel-patterns/SKILL.md +488 -0
  88. package/skills/review-otel-patterns/references/code-review.md +75 -0
  89. package/skills/review-otel-patterns/references/processor-pipeline.md +205 -0
  90. package/skills/review-otel-patterns/references/structured-errors.md +102 -0
  91. package/skills/review-otel-patterns/references/wide-spans.md +85 -0
  92. package/skills/tune-sampling/SKILL.md +210 -0
  93. package/src/attribute-redacting-processor.test.ts +6 -4
  94. package/src/attribute-redacting-processor.ts +11 -2
  95. package/src/drain-toolkit.test.ts +113 -0
  96. package/src/drain-toolkit.ts +129 -0
  97. package/src/enricher-toolkit.test.ts +67 -0
  98. package/src/enricher-toolkit.ts +79 -0
  99. package/src/functional.test.ts +18 -0
  100. package/src/functional.ts +32 -20
  101. package/src/index.ts +19 -0
  102. package/src/redact-values.test.ts +24 -10
  103. package/src/redact-values.ts +9 -2
  104. package/src/request-logger.test.ts +91 -0
  105. package/src/request-logger.ts +36 -2
  106. package/src/structured-error.test.ts +4 -1
  107. package/bin/intent.js +0 -6
  108. package/dist/chunk-563EL6O6.cjs.map +0 -1
  109. package/dist/chunk-CMNGGTQL.cjs.map +0 -1
  110. package/dist/chunk-DAAJLUTO.js.map +0 -1
  111. package/dist/chunk-MOK3E54E.cjs.map +0 -1
  112. package/dist/chunk-TDNKIHKT.js.map +0 -1
  113. package/dist/chunk-W35FVJBC.js.map +0 -1
  114. package/src/package-manifest.test.ts +0 -24
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { defineDrain, defineHttpDrain } from './drain-toolkit';
3
+
4
+ describe('defineDrain', () => {
5
+ it('calls send with transformed payloads', async () => {
6
+ const send = vi.fn(async () => {});
7
+ const drain = defineDrain<
8
+ { event: { id: string } },
9
+ { key: string },
10
+ string
11
+ >({
12
+ name: 'test',
13
+ resolve: async () => ({ key: 'k' }),
14
+ transform: (contexts) => contexts.map((c) => c.event.id),
15
+ send,
16
+ });
17
+
18
+ await drain({ event: { id: 'a' } });
19
+ expect(send).toHaveBeenCalledWith(['a'], { key: 'k' });
20
+ });
21
+
22
+ it('skips send when resolve returns null', async () => {
23
+ const send = vi.fn(async () => {});
24
+ const drain = defineDrain({
25
+ name: 'test',
26
+ resolve: async () => null,
27
+ send,
28
+ });
29
+
30
+ await drain({ event: { id: 'a' } });
31
+ expect(send).not.toHaveBeenCalled();
32
+ });
33
+
34
+ it('isolates send errors', async () => {
35
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
36
+ const drain = defineDrain({
37
+ name: 'test',
38
+ resolve: async () => ({ ok: true }),
39
+ send: async () => {
40
+ throw new Error('fail');
41
+ },
42
+ });
43
+
44
+ await expect(drain({ event: { id: 'a' } })).resolves.toBeUndefined();
45
+ expect(spy).toHaveBeenCalled();
46
+ spy.mockRestore();
47
+ });
48
+ });
49
+
50
+ describe('defineHttpDrain', () => {
51
+ const originalFetch = globalThis.fetch;
52
+
53
+ beforeEach(() => {
54
+ vi.restoreAllMocks();
55
+ });
56
+
57
+ afterEach(() => {
58
+ globalThis.fetch = originalFetch;
59
+ });
60
+
61
+ it('encodes payload and posts via fetch', async () => {
62
+ const fetchMock = vi.fn(async () => new Response(null, { status: 200 }));
63
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
64
+
65
+ const drain = defineHttpDrain<
66
+ { event: { id: string } },
67
+ { token: string },
68
+ { id: string }
69
+ >({
70
+ name: 'http-drain',
71
+ resolve: async () => ({ token: 't' }),
72
+ transform: (contexts) => contexts.map((c) => c.event),
73
+ encode: (payloads, config) => ({
74
+ url: 'https://example.com/ingest',
75
+ headers: {
76
+ 'content-type': 'application/json',
77
+ authorization: `Bearer ${config.token}`,
78
+ },
79
+ body: JSON.stringify(payloads),
80
+ }),
81
+ retries: 1,
82
+ timeoutMs: 2000,
83
+ });
84
+
85
+ await drain({ event: { id: 'evt_1' } });
86
+
87
+ expect(fetchMock).toHaveBeenCalledTimes(1);
88
+ expect(fetchMock.mock.calls[0]?.[0]).toBe('https://example.com/ingest');
89
+ });
90
+
91
+ it('retries failed requests', async () => {
92
+ const fetchMock = vi
93
+ .fn()
94
+ .mockRejectedValueOnce(new Error('network'))
95
+ .mockResolvedValueOnce(new Response(null, { status: 200 }));
96
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
97
+
98
+ const drain = defineHttpDrain({
99
+ name: 'http-drain',
100
+ resolve: async () => ({ ok: true }),
101
+ encode: () => ({
102
+ url: 'https://example.com/ingest',
103
+ headers: { 'content-type': 'application/json' },
104
+ body: '[]',
105
+ }),
106
+ retries: 2,
107
+ timeoutMs: 2000,
108
+ });
109
+
110
+ await drain({ event: { id: 'evt_1' } });
111
+ expect(fetchMock).toHaveBeenCalledTimes(2);
112
+ });
113
+ });
@@ -0,0 +1,129 @@
1
+ export interface DrainOptions<TContext, TConfig, TPayload = TContext> {
2
+ /** Stable identifier used in error logs. */
3
+ name: string;
4
+ /** Return null to skip draining (e.g. missing API key in dev). */
5
+ resolve: () => TConfig | null | Promise<TConfig | null>;
6
+ /** Transform contexts into payloads. Defaults to identity. */
7
+ transform?: (contexts: TContext[]) => TPayload[];
8
+ /** Transport implementation. */
9
+ send: (payloads: TPayload[], config: TConfig) => Promise<void>;
10
+ }
11
+
12
+ export interface HttpDrainRequest {
13
+ url: string;
14
+ headers: Record<string, string>;
15
+ body: string;
16
+ }
17
+
18
+ export interface HttpDrainOptions<
19
+ TContext,
20
+ TConfig,
21
+ TPayload = TContext,
22
+ > extends Omit<DrainOptions<TContext, TConfig, TPayload>, 'send'> {
23
+ encode: (payloads: TPayload[], config: TConfig) => HttpDrainRequest | null;
24
+ timeoutMs?: number;
25
+ retries?: number;
26
+ resolveTimeoutMs?: (config: TConfig) => number | undefined;
27
+ resolveRetries?: (config: TConfig) => number | undefined;
28
+ }
29
+
30
+ const DEFAULT_TIMEOUT_MS = 5000;
31
+ const DEFAULT_RETRIES = 2;
32
+
33
+ function delay(ms: number): Promise<void> {
34
+ return new Promise((resolve) => {
35
+ const t = setTimeout(resolve, ms);
36
+ t.unref?.();
37
+ });
38
+ }
39
+
40
+ async function postWithRetry(options: {
41
+ name: string;
42
+ request: HttpDrainRequest;
43
+ timeoutMs: number;
44
+ retries: number;
45
+ }): Promise<void> {
46
+ const { name, request, timeoutMs, retries } = options;
47
+ const attempts = Math.max(1, retries);
48
+ let lastError: unknown;
49
+
50
+ for (let attempt = 1; attempt <= attempts; attempt++) {
51
+ const controller = new AbortController();
52
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
53
+ timeout.unref?.();
54
+ try {
55
+ const response = await fetch(request.url, {
56
+ method: 'POST',
57
+ headers: request.headers,
58
+ body: request.body,
59
+ signal: controller.signal,
60
+ });
61
+ if (!response.ok) {
62
+ throw new Error(
63
+ `[autotel/${name}] HTTP ${response.status} draining ${request.url}`,
64
+ );
65
+ }
66
+ return;
67
+ } catch (error) {
68
+ lastError = error;
69
+ if (attempt < attempts) {
70
+ await delay(100 * attempt);
71
+ }
72
+ } finally {
73
+ clearTimeout(timeout);
74
+ }
75
+ }
76
+
77
+ throw lastError;
78
+ }
79
+
80
+ export function defineDrain<TContext, TConfig, TPayload = TContext>(
81
+ options: DrainOptions<TContext, TConfig, TPayload>,
82
+ ): (ctx: TContext | TContext[]) => Promise<void> {
83
+ return async (ctx: TContext | TContext[]) => {
84
+ const contexts = Array.isArray(ctx) ? ctx : [ctx];
85
+ if (contexts.length === 0) return;
86
+
87
+ const config = await options.resolve();
88
+ if (!config) return;
89
+
90
+ const payloads = options.transform
91
+ ? options.transform(contexts)
92
+ : (contexts as unknown as TPayload[]);
93
+
94
+ if (payloads.length === 0) return;
95
+
96
+ try {
97
+ await options.send(payloads, config);
98
+ } catch (error) {
99
+ console.error(`[autotel/${options.name}] drain failed:`, error);
100
+ }
101
+ };
102
+ }
103
+
104
+ export function defineHttpDrain<TContext, TConfig, TPayload = TContext>(
105
+ options: HttpDrainOptions<TContext, TConfig, TPayload>,
106
+ ): (ctx: TContext | TContext[]) => Promise<void> {
107
+ return defineDrain<TContext, TConfig, TPayload>({
108
+ name: options.name,
109
+ resolve: options.resolve,
110
+ transform: options.transform,
111
+ send: async (payloads, config) => {
112
+ const request = options.encode(payloads, config);
113
+ if (!request) return;
114
+ const timeoutMs =
115
+ options.resolveTimeoutMs?.(config) ??
116
+ options.timeoutMs ??
117
+ DEFAULT_TIMEOUT_MS;
118
+ const retries =
119
+ options.resolveRetries?.(config) ?? options.retries ?? DEFAULT_RETRIES;
120
+
121
+ await postWithRetry({
122
+ name: options.name,
123
+ request,
124
+ timeoutMs,
125
+ retries,
126
+ });
127
+ },
128
+ });
129
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { defineEnricher } from './enricher-toolkit';
3
+
4
+ describe('defineEnricher', () => {
5
+ it('merges computed values into existing field by default', () => {
6
+ const enricher = defineEnricher({
7
+ name: 'tenant-enricher',
8
+ field: 'tenant',
9
+ compute: () => ({ plan: 'pro' }),
10
+ });
11
+
12
+ const ctx = {
13
+ event: { tenant: { id: 't_1' } },
14
+ };
15
+
16
+ enricher(ctx);
17
+ expect(ctx.event).toEqual({ tenant: { id: 't_1', plan: 'pro' } });
18
+ });
19
+
20
+ it('overwrites field when overwrite=true', () => {
21
+ const enricher = defineEnricher(
22
+ {
23
+ name: 'tenant-enricher',
24
+ field: 'tenant',
25
+ compute: () => ({ plan: 'pro' }),
26
+ },
27
+ { overwrite: true },
28
+ );
29
+
30
+ const ctx = {
31
+ event: { tenant: { id: 't_1' } },
32
+ };
33
+
34
+ enricher(ctx);
35
+ expect(ctx.event).toEqual({ tenant: { plan: 'pro' } });
36
+ });
37
+
38
+ it('skips enrichment when compute returns undefined', () => {
39
+ const enricher = defineEnricher({
40
+ name: 'noop-enricher',
41
+ field: 'tenant',
42
+ compute: () => undefined,
43
+ });
44
+
45
+ const ctx = { event: { a: 1 } as Record<string, unknown> };
46
+ enricher(ctx);
47
+ expect(ctx.event).toEqual({ a: 1 });
48
+ });
49
+
50
+ it('isolates compute errors and logs them', () => {
51
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
52
+ const enricher = defineEnricher({
53
+ name: 'broken-enricher',
54
+ field: 'tenant',
55
+ compute: () => {
56
+ throw new Error('boom');
57
+ },
58
+ });
59
+
60
+ const ctx = { event: {} as Record<string, unknown> };
61
+ enricher(ctx);
62
+
63
+ expect(ctx.event).toEqual({});
64
+ expect(spy).toHaveBeenCalled();
65
+ spy.mockRestore();
66
+ });
67
+ });
@@ -0,0 +1,79 @@
1
+ export interface EnrichContext<TEvent extends Record<string, unknown>> {
2
+ event: TEvent;
3
+ request?: {
4
+ method?: string;
5
+ path?: string;
6
+ requestId?: string;
7
+ };
8
+ response?: {
9
+ status?: number;
10
+ };
11
+ headers?: Record<string, string>;
12
+ }
13
+
14
+ export interface EnricherDefinition<
15
+ TEvent extends Record<string, unknown>,
16
+ TValue extends object,
17
+ > {
18
+ /** Stable identifier used in error logs. */
19
+ name: string;
20
+ /** Top-level field to merge computed values into. */
21
+ field: keyof TEvent & string;
22
+ /** Return undefined to skip enrichment. */
23
+ compute: (ctx: EnrichContext<TEvent>) => TValue | undefined;
24
+ }
25
+
26
+ export interface EnricherOptions {
27
+ /** Replace existing field value instead of merge. Default false. */
28
+ overwrite?: boolean;
29
+ }
30
+
31
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
32
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
33
+ }
34
+
35
+ function mergeInto(
36
+ target: Record<string, unknown>,
37
+ source: Record<string, unknown>,
38
+ ): void {
39
+ for (const key in source) {
40
+ const sourceVal = source[key];
41
+ if (sourceVal === undefined) continue;
42
+ const targetVal = target[key];
43
+ if (isPlainObject(sourceVal) && isPlainObject(targetVal)) {
44
+ mergeInto(targetVal, sourceVal);
45
+ } else {
46
+ target[key] = sourceVal;
47
+ }
48
+ }
49
+ }
50
+
51
+ export function defineEnricher<
52
+ TEvent extends Record<string, unknown>,
53
+ TValue extends object,
54
+ >(
55
+ def: EnricherDefinition<TEvent, TValue>,
56
+ options: EnricherOptions = {},
57
+ ): (ctx: EnrichContext<TEvent>) => void {
58
+ return (ctx: EnrichContext<TEvent>) => {
59
+ let computed: TValue | undefined;
60
+ try {
61
+ computed = def.compute(ctx);
62
+ } catch (error) {
63
+ console.error(`[autotel/${def.name}] enrich failed:`, error);
64
+ return;
65
+ }
66
+
67
+ if (!computed) return;
68
+
69
+ if (options.overwrite || !isPlainObject(ctx.event[def.field])) {
70
+ (ctx.event as Record<string, unknown>)[def.field] = computed;
71
+ return;
72
+ }
73
+
74
+ mergeInto(
75
+ ctx.event[def.field] as unknown as Record<string, unknown>,
76
+ computed as unknown as Record<string, unknown>,
77
+ );
78
+ };
79
+ }
@@ -62,6 +62,24 @@ describe('Functional API', () => {
62
62
  expect(promise).toBeInstanceOf(Promise);
63
63
  await expect(promise).resolves.toBe(84);
64
64
  });
65
+
66
+ it('accepts a string name as first argument (sync)', () => {
67
+ const result = span('sync-name-shorthand', () => 'ok');
68
+ expect(result).toBe('ok');
69
+ });
70
+
71
+ it('accepts a string name as first argument (async)', async () => {
72
+ await expect(
73
+ span('async-name-shorthand', async () => 'ok'),
74
+ ).resolves.toBe('ok');
75
+ });
76
+
77
+ it('records spans created via the string-name shorthand', async () => {
78
+ const collector = createTraceCollector();
79
+ await span('shorthand.recorded', async () => undefined);
80
+ const names = collector.getSpans().map((s) => s.name);
81
+ expect(names).toContain('shorthand.recorded');
82
+ });
65
83
  });
66
84
 
67
85
  describe('trace()', () => {
package/src/functional.ts CHANGED
@@ -2124,44 +2124,56 @@ export interface SpanOptions {
2124
2124
  * Useful for adding tracing to specific code blocks without wrapping
2125
2125
  * the entire function. Supports both synchronous and asynchronous functions.
2126
2126
  *
2127
+ * Mirrors `trace()`: pass a span name as the first argument for the common
2128
+ * case, or full `SpanOptions` when you need to attach attributes.
2129
+ *
2127
2130
  * @example
2128
2131
  * ```typescript
2129
- * // Async function
2130
- * async function processOrder(order: Order) {
2131
- * await span({
2132
- * name: 'payment.charge',
2133
- * attributes: { amount: order.total }
2134
- * }, async (span) => {
2132
+ * // Name shorthand
2133
+ * await span('payment.charge', async (span) => {
2134
+ * await chargeCustomer(order);
2135
+ * })
2136
+ *
2137
+ * // Full options when attributes are needed
2138
+ * await span(
2139
+ * { name: 'payment.charge', attributes: { amount: order.total } },
2140
+ * async (span) => {
2135
2141
  * await chargeCustomer(order);
2136
- * })
2137
- * }
2142
+ * },
2143
+ * )
2138
2144
  *
2139
- * // Sync function
2140
- * function calculateTotal(items: Item[]) {
2141
- * return span({
2142
- * name: 'calculateTotal',
2143
- * attributes: { itemCount: items.length }
2144
- * }, (span) => {
2145
- * return items.reduce((sum, item) => sum + item.price, 0);
2146
- * })
2147
- * }
2145
+ * // Sync
2146
+ * const total = span('calculateTotal', (span) => {
2147
+ * return items.reduce((sum, item) => sum + item.price, 0);
2148
+ * })
2148
2149
  * ```
2149
2150
  */
2150
- // Overload for sync functions (more specific - should come first)
2151
+ // Overloads sync first (more specific match), then async.
2152
+ // Each shape is offered with a string name OR a full SpanOptions object so
2153
+ // span() aligns with trace()'s argument flexibility.
2154
+ export function span<T = unknown>(
2155
+ name: string,
2156
+ fn: (span: Span) => T,
2157
+ ): T;
2158
+ export function span<T = unknown>(
2159
+ name: string,
2160
+ fn: (span: Span) => Promise<T>,
2161
+ ): Promise<T>;
2151
2162
  export function span<T = unknown>(
2152
2163
  options: SpanOptions,
2153
2164
  fn: (span: Span) => T,
2154
2165
  ): T;
2155
- // Overload for async functions
2156
2166
  export function span<T = unknown>(
2157
2167
  options: SpanOptions,
2158
2168
  fn: (span: Span) => Promise<T>,
2159
2169
  ): Promise<T>;
2160
2170
  // Implementation
2161
2171
  export function span<T = unknown>(
2162
- options: SpanOptions,
2172
+ nameOrOptions: string | SpanOptions,
2163
2173
  fn: (span: Span) => T | Promise<T>,
2164
2174
  ): T | Promise<T> {
2175
+ const options: SpanOptions =
2176
+ typeof nameOrOptions === 'string' ? { name: nameOrOptions } : nameOrOptions;
2165
2177
  const config = getConfig();
2166
2178
  const tracer = config.tracer;
2167
2179
  const { name, attributes } = options;
package/src/index.ts CHANGED
@@ -60,12 +60,16 @@ export {
60
60
  AttributeRedactingProcessor,
61
61
  REDACTOR_PATTERNS,
62
62
  REDACTOR_PRESETS,
63
+ builtinPatterns,
63
64
  createAttributeRedactor,
64
65
  createRedactedSpan,
66
+ normalizeAttributeRedactorConfig,
65
67
  type AttributeRedactorFn,
66
68
  type AttributeRedactorPreset,
67
69
  type AttributeRedactorConfig,
68
70
  type AttributeRedactingProcessorOptions,
71
+ type BuiltinPatternName,
72
+ type MaskFn,
69
73
  type ValuePatternConfig,
70
74
  } from './attribute-redacting-processor';
71
75
 
@@ -123,6 +127,8 @@ export {
123
127
  type RequestLogger,
124
128
  type RequestLogSnapshot,
125
129
  type RequestLoggerOptions,
130
+ type ForkLifecycle,
131
+ type ForkOptions,
126
132
  } from './request-logger';
127
133
 
128
134
  // Structured errors
@@ -147,6 +153,19 @@ export {
147
153
  type DrainPipelineOptions,
148
154
  type PipelineDrainFn,
149
155
  } from './drain-pipeline';
156
+ export {
157
+ defineDrain,
158
+ defineHttpDrain,
159
+ type DrainOptions,
160
+ type HttpDrainOptions,
161
+ type HttpDrainRequest,
162
+ } from './drain-toolkit';
163
+ export {
164
+ defineEnricher,
165
+ type EnricherDefinition,
166
+ type EnrichContext,
167
+ type EnricherOptions,
168
+ } from './enricher-toolkit';
150
169
 
151
170
  // Pretty log formatting
152
171
  export { formatDuration } from './pretty-log-formatter';
@@ -10,18 +10,32 @@ describe('createStringRedactor', () => {
10
10
  redact = createStringRedactor('default');
11
11
  });
12
12
 
13
- it('redacts emails', () => {
13
+ it('smart-masks emails', () => {
14
14
  expect(redact('Contact user@example.com for info')).toBe(
15
- 'Contact [REDACTED] for info',
15
+ 'Contact u***@***.com for info',
16
16
  );
17
17
  });
18
18
 
19
- it('redacts phone numbers', () => {
20
- expect(redact('Call 555-123-4567 now')).toBe('Call [REDACTED] now');
19
+ it('smart-masks international phone numbers (country code + last 2 digits)', () => {
20
+ expect(redact('Call +33 1 23 45 67 89 now')).toBe('Call +33******89 now');
21
21
  });
22
22
 
23
- it('redacts credit card numbers', () => {
24
- expect(redact('Card: 4111-1111-1111-1111')).toBe('Card: [REDACTED]');
23
+ it('smart-masks phone numbers with parens (last 2 digits)', () => {
24
+ expect(redact('Call (415) 555-1234 now')).toBe('Call ********34 now');
25
+ });
26
+
27
+ it('smart-masks common US phone formats', () => {
28
+ expect(redact('Call 555-123-4567 now')).toBe('Call ********67 now');
29
+ expect(redact('Call 5551234567 now')).toBe('Call ********67 now');
30
+ });
31
+
32
+ it('does not mistake bare digit runs for phone numbers', () => {
33
+ // UUIDs, order ids etc. should pass through untouched.
34
+ expect(redact('Order: 12345678 ok')).toBe('Order: 12345678 ok');
35
+ });
36
+
37
+ it('smart-masks credit card numbers (last four digits preserved)', () => {
38
+ expect(redact('Card: 4111-1111-1111-1111')).toBe('Card: ****1111');
25
39
  });
26
40
 
27
41
  it('returns input unchanged when no patterns match', () => {
@@ -36,15 +50,15 @@ describe('createStringRedactor', () => {
36
50
  redact = createStringRedactor('strict');
37
51
  });
38
52
 
39
- it('redacts JWTs', () => {
53
+ it('smart-masks JWTs', () => {
40
54
  const jwt =
41
55
  'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123_-def';
42
- expect(redact(`Token: ${jwt}`)).toBe('Token: [REDACTED]');
56
+ expect(redact(`Token: ${jwt}`)).toBe('Token: eyJ***.***');
43
57
  });
44
58
 
45
- it('redacts bearer tokens', () => {
59
+ it('smart-masks bearer tokens', () => {
46
60
  expect(redact('Authorization: Bearer abc123.xyz')).toBe(
47
- 'Authorization: [REDACTED]',
61
+ 'Authorization: Bearer ***',
48
62
  );
49
63
  });
50
64
  });
@@ -18,9 +18,16 @@ export function createStringRedactor(
18
18
 
19
19
  return (value: string): string => {
20
20
  let result = value;
21
- for (const { pattern, replacement } of valuePatterns) {
21
+ for (const { pattern, replacement, mask } of valuePatterns) {
22
22
  pattern.lastIndex = 0;
23
- result = result.replaceAll(pattern, replacement ?? defaultReplacement);
23
+ // Smart masks (e.g. email a***@***.com) take precedence over the
24
+ // static replacement so callers see the same output as the
25
+ // span-attribute redactor does.
26
+ if (mask) {
27
+ result = result.replaceAll(pattern, (match) => mask(match));
28
+ } else {
29
+ result = result.replaceAll(pattern, replacement ?? defaultReplacement);
30
+ }
24
31
  }
25
32
  return result;
26
33
  };