autotel 2.26.3 → 3.0.3

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 (191) hide show
  1. package/README.md +50 -23
  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.d.cts +3 -3
  8. package/dist/attributes.d.ts +3 -3
  9. package/dist/attributes.js +2 -2
  10. package/dist/auto.cjs +3 -3
  11. package/dist/auto.js +2 -2
  12. package/dist/business-baggage.d.cts +1 -1
  13. package/dist/business-baggage.d.ts +1 -1
  14. package/dist/chunk-4P6ZOARG.cjs +33 -0
  15. package/dist/chunk-4P6ZOARG.cjs.map +1 -0
  16. package/dist/{chunk-U54FTVFH.js → chunk-52PUSFC2.js} +3 -3
  17. package/dist/{chunk-U54FTVFH.js.map → chunk-52PUSFC2.js.map} +1 -1
  18. package/dist/{chunk-YEVCD6DR.cjs → chunk-7SMNC4LS.cjs} +7 -7
  19. package/dist/{chunk-YEVCD6DR.cjs.map → chunk-7SMNC4LS.cjs.map} +1 -1
  20. package/dist/{chunk-563EL6O6.cjs → chunk-BPO2PQ3T.cjs} +12 -8
  21. package/dist/chunk-BPO2PQ3T.cjs.map +1 -0
  22. package/dist/{chunk-WZOKY3PW.cjs → chunk-DAZ7EGR4.cjs} +19 -19
  23. package/dist/{chunk-WZOKY3PW.cjs.map → chunk-DAZ7EGR4.cjs.map} +1 -1
  24. package/dist/{chunk-ER43K7ES.js → chunk-DDXIUZEG.js} +3 -3
  25. package/dist/{chunk-ER43K7ES.js.map → chunk-DDXIUZEG.js.map} +1 -1
  26. package/dist/{chunk-JKIMEPI2.cjs → chunk-DQ2SUROF.cjs} +4 -4
  27. package/dist/{chunk-JKIMEPI2.cjs.map → chunk-DQ2SUROF.cjs.map} +1 -1
  28. package/dist/{chunk-B3ZHLLMP.js → chunk-DSMSIVTG.js} +2 -2
  29. package/dist/chunk-DSMSIVTG.js.map +1 -0
  30. package/dist/{chunk-OBWXM4NN.cjs → chunk-HKZHUGGN.cjs} +15 -14
  31. package/dist/chunk-HKZHUGGN.cjs.map +1 -0
  32. package/dist/{chunk-TDNKIHKT.js → chunk-JVWJDHDB.js} +13 -4
  33. package/dist/chunk-JVWJDHDB.js.map +1 -0
  34. package/dist/{chunk-YN7USLHW.js → chunk-K7HSRLP5.js} +11 -10
  35. package/dist/chunk-K7HSRLP5.js.map +1 -0
  36. package/dist/chunk-KIL5CUN6.js +31 -0
  37. package/dist/chunk-KIL5CUN6.js.map +1 -0
  38. package/dist/chunk-KKGM42RQ.cjs +1207 -0
  39. package/dist/chunk-KKGM42RQ.cjs.map +1 -0
  40. package/dist/{chunk-6YGUN7IY.cjs → chunk-MOO75VE4.cjs} +18 -17
  41. package/dist/chunk-MOO75VE4.cjs.map +1 -0
  42. package/dist/{chunk-GML3FBOT.cjs → chunk-NCSMD3TK.cjs} +2 -2
  43. package/dist/chunk-NCSMD3TK.cjs.map +1 -0
  44. package/dist/{chunk-CMNGGTQL.cjs → chunk-NXLRY2CE.cjs} +13 -4
  45. package/dist/chunk-NXLRY2CE.cjs.map +1 -0
  46. package/dist/{chunk-BJ2XPN77.js → chunk-OM4OSBOP.js} +5 -5
  47. package/dist/{chunk-BJ2XPN77.js.map → chunk-OM4OSBOP.js.map} +1 -1
  48. package/dist/{chunk-HPUGKUMZ.js → chunk-PMRWMRXY.js} +13 -640
  49. package/dist/chunk-PMRWMRXY.js.map +1 -0
  50. package/dist/{chunk-UTZR7P7E.cjs → chunk-QPH5ZKP5.cjs} +43 -673
  51. package/dist/chunk-QPH5ZKP5.cjs.map +1 -0
  52. package/dist/chunk-SEO6NAQT.js +14 -0
  53. package/dist/chunk-SEO6NAQT.js.map +1 -0
  54. package/dist/{chunk-QC5MNKVF.js → chunk-TFRZOUTV.js} +13 -12
  55. package/dist/chunk-TFRZOUTV.js.map +1 -0
  56. package/dist/chunk-VQTCQKHQ.cjs +17 -0
  57. package/dist/chunk-VQTCQKHQ.cjs.map +1 -0
  58. package/dist/chunk-Z7VAOK5X.js +1183 -0
  59. package/dist/chunk-Z7VAOK5X.js.map +1 -0
  60. package/dist/{chunk-W35FVJBC.js → chunk-ZDPIWKWD.js} +9 -5
  61. package/dist/chunk-ZDPIWKWD.js.map +1 -0
  62. package/dist/correlation-id.cjs +22 -10
  63. package/dist/correlation-id.js +14 -2
  64. package/dist/decorators.cjs +7 -8
  65. package/dist/decorators.cjs.map +1 -1
  66. package/dist/decorators.d.cts +1 -1
  67. package/dist/decorators.d.ts +1 -1
  68. package/dist/decorators.js +6 -7
  69. package/dist/decorators.js.map +1 -1
  70. package/dist/event.cjs +8 -9
  71. package/dist/event.js +5 -6
  72. package/dist/functional.cjs +13 -14
  73. package/dist/functional.d.cts +1 -1
  74. package/dist/functional.d.ts +1 -1
  75. package/dist/functional.js +6 -7
  76. package/dist/http.cjs +13 -2
  77. package/dist/http.cjs.map +1 -1
  78. package/dist/http.js +12 -1
  79. package/dist/http.js.map +1 -1
  80. package/dist/index.cjs +305 -280
  81. package/dist/index.cjs.map +1 -1
  82. package/dist/index.d.cts +89 -10
  83. package/dist/index.d.ts +89 -10
  84. package/dist/index.js +180 -181
  85. package/dist/index.js.map +1 -1
  86. package/dist/instrumentation.cjs +9 -9
  87. package/dist/instrumentation.js +2 -2
  88. package/dist/messaging-adapters.d.cts +1 -1
  89. package/dist/messaging-adapters.d.ts +1 -1
  90. package/dist/messaging-testing.d.cts +1 -1
  91. package/dist/messaging-testing.d.ts +1 -1
  92. package/dist/messaging.cjs +11 -11
  93. package/dist/messaging.d.cts +1 -1
  94. package/dist/messaging.d.ts +1 -1
  95. package/dist/messaging.js +8 -8
  96. package/dist/semantic-helpers.cjs +11 -12
  97. package/dist/semantic-helpers.d.cts +1 -1
  98. package/dist/semantic-helpers.d.ts +1 -1
  99. package/dist/semantic-helpers.js +7 -8
  100. package/dist/{trace-context-t5X1AP-e.d.cts → trace-context-DbGKd1Rn.d.cts} +18 -5
  101. package/dist/{trace-context-t5X1AP-e.d.ts → trace-context-DbGKd1Rn.d.ts} +18 -5
  102. package/dist/trace-helpers.cjs +13 -13
  103. package/dist/trace-helpers.d.cts +2 -2
  104. package/dist/trace-helpers.d.ts +2 -2
  105. package/dist/trace-helpers.js +1 -1
  106. package/dist/{utils-CbUkl8r1.d.cts → utils-BahBCFtJ.d.cts} +1 -1
  107. package/dist/{utils-Buel3cj0.d.ts → utils-CLKwaUlG.d.ts} +1 -1
  108. package/dist/webhook.cjs +21 -12
  109. package/dist/webhook.cjs.map +1 -1
  110. package/dist/webhook.d.cts +1 -1
  111. package/dist/webhook.d.ts +1 -1
  112. package/dist/webhook.js +20 -11
  113. package/dist/webhook.js.map +1 -1
  114. package/dist/workflow-distributed.cjs +25 -21
  115. package/dist/workflow-distributed.cjs.map +1 -1
  116. package/dist/workflow-distributed.d.cts +1 -1
  117. package/dist/workflow-distributed.d.ts +1 -1
  118. package/dist/workflow-distributed.js +23 -19
  119. package/dist/workflow-distributed.js.map +1 -1
  120. package/dist/workflow.cjs +12 -12
  121. package/dist/workflow.d.cts +1 -1
  122. package/dist/workflow.d.ts +1 -1
  123. package/dist/workflow.js +8 -8
  124. package/package.json +43 -45
  125. package/skills/analyze-traces/SKILL.md +178 -0
  126. package/skills/autotel-core/SKILL.md +2 -7
  127. package/skills/autotel-events/SKILL.md +2 -6
  128. package/skills/autotel-frameworks/SKILL.md +2 -9
  129. package/skills/autotel-instrumentation/SKILL.md +2 -7
  130. package/skills/autotel-request-logging/SKILL.md +2 -8
  131. package/skills/autotel-structured-errors/SKILL.md +2 -7
  132. package/skills/build-audit-trails/SKILL.md +302 -0
  133. package/skills/debug-missing-spans/SKILL.md +248 -0
  134. package/skills/migrate-to-autotel/SKILL.md +268 -0
  135. package/skills/review-otel-patterns/SKILL.md +488 -0
  136. package/skills/review-otel-patterns/references/code-review.md +75 -0
  137. package/skills/review-otel-patterns/references/processor-pipeline.md +205 -0
  138. package/skills/review-otel-patterns/references/structured-errors.md +102 -0
  139. package/skills/review-otel-patterns/references/wide-spans.md +85 -0
  140. package/skills/tune-sampling/SKILL.md +210 -0
  141. package/src/attribute-redacting-processor.test.ts +6 -4
  142. package/src/attribute-redacting-processor.ts +11 -2
  143. package/src/correlated-events.test.ts +151 -0
  144. package/src/correlated-events.ts +47 -0
  145. package/src/drain-toolkit.test.ts +113 -0
  146. package/src/drain-toolkit.ts +129 -0
  147. package/src/enricher-toolkit.test.ts +67 -0
  148. package/src/enricher-toolkit.ts +79 -0
  149. package/src/functional.ts +2 -0
  150. package/src/gen-ai-events.ts +14 -5
  151. package/src/index.ts +39 -4
  152. package/src/messaging.ts +10 -9
  153. package/src/redact-values.test.ts +24 -10
  154. package/src/redact-values.ts +9 -2
  155. package/src/request-logger.test.ts +91 -0
  156. package/src/request-logger.ts +40 -5
  157. package/src/structured-error.test.ts +86 -1
  158. package/src/structured-error.ts +9 -2
  159. package/src/trace-context.ts +39 -11
  160. package/src/trace-helpers.ts +2 -2
  161. package/src/trace-hybrid.test.ts +42 -0
  162. package/src/trace-hybrid.ts +37 -0
  163. package/src/webhook.ts +16 -7
  164. package/src/workflow-distributed.ts +18 -13
  165. package/src/workflow.ts +7 -6
  166. package/bin/intent.js +0 -6
  167. package/dist/chunk-563EL6O6.cjs.map +0 -1
  168. package/dist/chunk-6YGUN7IY.cjs.map +0 -1
  169. package/dist/chunk-B3ZHLLMP.js.map +0 -1
  170. package/dist/chunk-BBBWDIYQ.js +0 -211
  171. package/dist/chunk-BBBWDIYQ.js.map +0 -1
  172. package/dist/chunk-CMNGGTQL.cjs.map +0 -1
  173. package/dist/chunk-D5LMF53P.cjs +0 -150
  174. package/dist/chunk-D5LMF53P.cjs.map +0 -1
  175. package/dist/chunk-GML3FBOT.cjs.map +0 -1
  176. package/dist/chunk-HPUGKUMZ.js.map +0 -1
  177. package/dist/chunk-HZ3FYBJG.cjs +0 -217
  178. package/dist/chunk-HZ3FYBJG.cjs.map +0 -1
  179. package/dist/chunk-JSNUWSBH.cjs +0 -62
  180. package/dist/chunk-JSNUWSBH.cjs.map +0 -1
  181. package/dist/chunk-OBWXM4NN.cjs.map +0 -1
  182. package/dist/chunk-QC5MNKVF.js.map +0 -1
  183. package/dist/chunk-S4OFEXLA.js +0 -53
  184. package/dist/chunk-S4OFEXLA.js.map +0 -1
  185. package/dist/chunk-TDNKIHKT.js.map +0 -1
  186. package/dist/chunk-UTZR7P7E.cjs.map +0 -1
  187. package/dist/chunk-W35FVJBC.js.map +0 -1
  188. package/dist/chunk-WD4RP6IV.js +0 -146
  189. package/dist/chunk-WD4RP6IV.js.map +0 -1
  190. package/dist/chunk-YN7USLHW.js.map +0 -1
  191. package/src/package-manifest.test.ts +0 -24
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
 
@@ -81,7 +85,6 @@ export type {
81
85
  InstrumentOptions,
82
86
  } from './functional';
83
87
  export {
84
- trace,
85
88
  instrument,
86
89
  withTracing,
87
90
  span,
@@ -89,6 +92,9 @@ export {
89
92
  withBaggage,
90
93
  ctx,
91
94
  } from './functional';
95
+ // `trace` is the hybrid: callable like autotel's `trace(fn)` AND carries the
96
+ // full `@opentelemetry/api` TraceAPI surface (getActiveSpan, getTracer, etc).
97
+ export { trace } from './trace-hybrid';
92
98
 
93
99
  // Operation context (for advanced usage)
94
100
  export type { OperationContext } from './operation-context';
@@ -121,6 +127,8 @@ export {
121
127
  type RequestLogger,
122
128
  type RequestLogSnapshot,
123
129
  type RequestLoggerOptions,
130
+ type ForkLifecycle,
131
+ type ForkOptions,
124
132
  } from './request-logger';
125
133
 
126
134
  // Structured errors
@@ -145,6 +153,19 @@ export {
145
153
  type DrainPipelineOptions,
146
154
  type PipelineDrainFn,
147
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';
148
169
 
149
170
  // Pretty log formatting
150
171
  export { formatDuration } from './pretty-log-formatter';
@@ -333,19 +354,33 @@ export {
333
354
  type TLSAttrs,
334
355
  } from './attributes';
335
356
 
336
- // Re-export common OpenTelemetry types and utilities
337
- // This allows plugins and apps to use OTel without needing separate @opentelemetry/api installation
357
+ // Re-export common OpenTelemetry types and utilities so plugins, apps, and
358
+ // existing OTel code can `import { ... } from 'autotel'` without also taking
359
+ // a separate `@opentelemetry/api` dependency.
338
360
  export type {
339
361
  Span,
340
362
  SpanContext,
363
+ SpanAttributes,
341
364
  Tracer,
365
+ TracerProvider,
342
366
  Context,
367
+ Attributes,
368
+ AttributeValue,
369
+ Link,
343
370
  Link as SpanLink,
371
+ TimeInput,
372
+ HrTime,
373
+ Baggage,
374
+ BaggageEntry,
375
+ Exception,
376
+ TraceFlags,
377
+ TraceState,
344
378
  TextMapSetter,
345
379
  TextMapGetter,
346
380
  } from '@opentelemetry/api';
347
381
  export { SpanKind, ROOT_CONTEXT } from '@opentelemetry/api';
348
- // Note: trace exported from functional.ts, context/propagation/SpanStatusCode already exported above
382
+ // Note: trace, context, propagation, SpanStatusCode already exported above
383
+ // (`trace` is the hybrid; `otelTrace` is the pure TraceAPI singleton).
349
384
 
350
385
  // Export typed baggage helper
351
386
  export { defineBaggageSchema } from './trace-context';
package/src/messaging.ts CHANGED
@@ -50,6 +50,7 @@ import type {
50
50
  } from '@opentelemetry/api';
51
51
  import { trace } from './functional';
52
52
  import type { TraceContext } from './trace-context';
53
+ import { emitCorrelatedEvent } from './correlated-events';
53
54
  import { createLinkFromHeaders, extractLinksFromBatch } from './sampling';
54
55
 
55
56
  // ============================================================================
@@ -1406,7 +1407,7 @@ function extendContextForConsumer(
1406
1407
  producerLink.context.spanId;
1407
1408
  }
1408
1409
 
1409
- baseCtx.addEvent('dlq_routed', eventAttrs);
1410
+ emitCorrelatedEvent(baseCtx, 'dlq_routed', eventAttrs);
1410
1411
 
1411
1412
  // Call user's onDLQ callback if provided
1412
1413
  if (config.onDLQ) {
@@ -1447,7 +1448,7 @@ function extendContextForConsumer(
1447
1448
  }),
1448
1449
  };
1449
1450
 
1450
- baseCtx.addEvent('dlq_replay', eventAttrs);
1451
+ emitCorrelatedEvent(baseCtx, 'dlq_replay', eventAttrs);
1451
1452
  },
1452
1453
 
1453
1454
  recordRetry(attemptNumber: number, maxAttempts?: number): void {
@@ -1455,7 +1456,7 @@ function extendContextForConsumer(
1455
1456
  if (maxAttempts !== undefined) {
1456
1457
  baseCtx.setAttribute('messaging.retry.max_attempts', maxAttempts);
1457
1458
  }
1458
- baseCtx.addEvent('retry_attempt', {
1459
+ emitCorrelatedEvent(baseCtx, 'retry_attempt', {
1459
1460
  'messaging.retry.count': attemptNumber,
1460
1461
  ...(maxAttempts !== undefined && {
1461
1462
  'messaging.retry.max_attempts': maxAttempts,
@@ -1585,7 +1586,7 @@ function extendContextForConsumer(
1585
1586
  event.partitions.map((p) => `${p.topic}:${p.partition}`).join(',');
1586
1587
  }
1587
1588
 
1588
- baseCtx.addEvent(`consumer_group_${event.type}`, eventAttrs);
1589
+ emitCorrelatedEvent(baseCtx, `consumer_group_${event.type}`, eventAttrs);
1589
1590
 
1590
1591
  // Call user's onRebalance callback if provided
1591
1592
  if (config.consumerGroupTracking?.onRebalance) {
@@ -1627,7 +1628,7 @@ function extendContextForConsumer(
1627
1628
  );
1628
1629
  }
1629
1630
 
1630
- baseCtx.addEvent('consumer_group_heartbeat', {
1631
+ emitCorrelatedEvent(baseCtx, 'consumer_group_heartbeat', {
1631
1632
  'messaging.consumer_group.heartbeat.healthy': healthy,
1632
1633
  'messaging.consumer_group.heartbeat.timestamp':
1633
1634
  groupState.lastHeartbeat,
@@ -1644,7 +1645,7 @@ function extendContextForConsumer(
1644
1645
  baseCtx.setAttribute(`${prefix}.end_offset`, lag.endOffset);
1645
1646
  baseCtx.setAttribute(`${prefix}.lag`, lag.lag);
1646
1647
 
1647
- baseCtx.addEvent('partition_lag_recorded', {
1648
+ emitCorrelatedEvent(baseCtx, 'partition_lag_recorded', {
1648
1649
  'messaging.consumer_group.lag.topic': lag.topic,
1649
1650
  'messaging.consumer_group.lag.partition': lag.partition,
1650
1651
  'messaging.consumer_group.lag.current_offset': lag.currentOffset,
@@ -1899,7 +1900,7 @@ async function extractLagMetrics(
1899
1900
  ctx.setAttribute('messaging.kafka.consumer_lag', lag);
1900
1901
 
1901
1902
  // Add lag event
1902
- ctx.addEvent('consumer_lag_measured', {
1903
+ emitCorrelatedEvent(ctx, 'consumer_lag_measured', {
1903
1904
  'messaging.kafka.consumer_lag': lag,
1904
1905
  'messaging.kafka.message.offset': currentOffset,
1905
1906
  'messaging.kafka.high_watermark': endOffset,
@@ -2119,7 +2120,7 @@ function extractAndProcessOrdering(
2119
2120
  }
2120
2121
 
2121
2122
  // Add event for each out-of-order message
2122
- ctx.addEvent('message_out_of_order', {
2123
+ emitCorrelatedEvent(ctx, 'message_out_of_order', {
2123
2124
  'messaging.ordering.batch_index': i,
2124
2125
  'messaging.ordering.current_sequence': msgSequence,
2125
2126
  'messaging.ordering.expected_sequence': expectedSequence,
@@ -2155,7 +2156,7 @@ function extractAndProcessOrdering(
2155
2156
  duplicateCount++;
2156
2157
 
2157
2158
  // Add event for each duplicate
2158
- ctx.addEvent('message_duplicate', {
2159
+ emitCorrelatedEvent(ctx, 'message_duplicate', {
2159
2160
  'messaging.ordering.batch_index': i,
2160
2161
  'messaging.message.id': msgId,
2161
2162
  });
@@ -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
  };
@@ -254,6 +254,97 @@ describe('log.fork()', () => {
254
254
  expect(ctx.setAttributes).not.toHaveBeenCalled();
255
255
  expect(childSpan.end).toHaveBeenCalledTimes(1);
256
256
  });
257
+
258
+ it('fork lifecycle hooks fire around child handler', async () => {
259
+ const ctx = createMockContext();
260
+ const log = getRequestLogger(ctx);
261
+ const childSpan = {
262
+ spanContext: () => ({
263
+ traceId: 'a'.repeat(32),
264
+ spanId: 'b'.repeat(16),
265
+ }),
266
+ setAttribute: vi.fn(),
267
+ setAttributes: vi.fn(),
268
+ setStatus: vi.fn(),
269
+ recordException: vi.fn(),
270
+ addEvent: vi.fn(),
271
+ addLink: vi.fn(),
272
+ addLinks: vi.fn(),
273
+ updateName: vi.fn(),
274
+ isRecording: vi.fn(() => true),
275
+ end: vi.fn(),
276
+ };
277
+
278
+ vi.spyOn(otelTrace, 'getTracer').mockReturnValue({
279
+ startActiveSpan: (
280
+ _name: string,
281
+ cb: (span: typeof childSpan) => Promise<void>,
282
+ ) => cb(childSpan),
283
+ } as unknown as ReturnType<typeof otelTrace.getTracer>);
284
+
285
+ const calls: string[] = [];
286
+ const onChildEnter = vi.fn(() => calls.push('enter'));
287
+ const onChildExit = vi.fn(() => calls.push('exit'));
288
+
289
+ log.fork(
290
+ 'bg',
291
+ async () => {
292
+ calls.push('handler');
293
+ },
294
+ { lifecycle: { onChildEnter, onChildExit } },
295
+ );
296
+
297
+ await new Promise((resolve) => setImmediate(resolve));
298
+ await new Promise((resolve) => setImmediate(resolve));
299
+
300
+ expect(onChildEnter).toHaveBeenCalledTimes(1);
301
+ expect(onChildExit).toHaveBeenCalledTimes(1);
302
+ expect(calls).toEqual(['enter', 'handler', 'exit']);
303
+ });
304
+
305
+ it('fork onChildExit hook errors do not crash fork cleanup', async () => {
306
+ const ctx = createMockContext();
307
+ const log = getRequestLogger(ctx);
308
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
309
+ const childSpan = {
310
+ spanContext: () => ({
311
+ traceId: 'a'.repeat(32),
312
+ spanId: 'b'.repeat(16),
313
+ }),
314
+ setAttribute: vi.fn(),
315
+ setAttributes: vi.fn(),
316
+ setStatus: vi.fn(),
317
+ recordException: vi.fn(),
318
+ addEvent: vi.fn(),
319
+ addLink: vi.fn(),
320
+ addLinks: vi.fn(),
321
+ updateName: vi.fn(),
322
+ isRecording: vi.fn(() => true),
323
+ end: vi.fn(),
324
+ };
325
+
326
+ vi.spyOn(otelTrace, 'getTracer').mockReturnValue({
327
+ startActiveSpan: (
328
+ _name: string,
329
+ cb: (span: typeof childSpan) => Promise<void>,
330
+ ) => cb(childSpan),
331
+ } as unknown as ReturnType<typeof otelTrace.getTracer>);
332
+
333
+ log.fork('bg', async () => {}, {
334
+ lifecycle: {
335
+ onChildExit: () => {
336
+ throw new Error('hook exploded');
337
+ },
338
+ },
339
+ });
340
+
341
+ await new Promise((resolve) => setImmediate(resolve));
342
+ await new Promise((resolve) => setImmediate(resolve));
343
+
344
+ expect(childSpan.end).toHaveBeenCalledTimes(1);
345
+ expect(consoleSpy).toHaveBeenCalled();
346
+ consoleSpy.mockRestore();
347
+ });
257
348
  });
258
349
 
259
350
  describe('getRequestLogger', () => {
@@ -4,6 +4,7 @@ import type { TraceContext } from './trace-context';
4
4
  import { createTraceContext } from './trace-context';
5
5
  import { recordStructuredError } from './structured-error';
6
6
  import { flattenToAttributes } from './flatten-attributes';
7
+ import { emitCorrelatedEvent } from './correlated-events';
7
8
 
8
9
  const POST_EMIT_FORK_HINT =
9
10
  "For intentional background work tied to this request, use log.fork('label', fn) when available.";
@@ -55,7 +56,11 @@ export interface RequestLogger {
55
56
  error(error: Error | string, fields?: Record<string, unknown>): void;
56
57
  getContext(): Record<string, unknown>;
57
58
  emitNow(overrides?: Record<string, unknown>): RequestLogSnapshot;
58
- fork(label: string, fn: () => void | Promise<void>): void;
59
+ fork(
60
+ label: string,
61
+ fn: () => void | Promise<void>,
62
+ options?: ForkOptions,
63
+ ): void;
59
64
  }
60
65
 
61
66
  export interface RequestLogSnapshot {
@@ -71,6 +76,21 @@ export interface RequestLoggerOptions {
71
76
  onEmit?: (snapshot: RequestLogSnapshot) => void | Promise<void>;
72
77
  }
73
78
 
79
+ /**
80
+ * Optional lifecycle hooks for adapters that need to track child loggers
81
+ * spawned by `log.fork()` (e.g. active logger maps in framework integrations).
82
+ */
83
+ export interface ForkLifecycle {
84
+ /** Called after the child logger is created, before `fn` runs. */
85
+ onChildEnter?: (child: RequestLogger) => void;
86
+ /** Called after the child has finished (emit + drain), success or failure. */
87
+ onChildExit?: (child: RequestLogger) => void;
88
+ }
89
+
90
+ export interface ForkOptions {
91
+ lifecycle?: ForkLifecycle;
92
+ }
93
+
74
94
  function resolveContext(ctx?: TraceContext): TraceContext {
75
95
  if (ctx) return ctx;
76
96
 
@@ -101,9 +121,9 @@ export function getRequestLogger(
101
121
  fields?: Record<string, unknown>,
102
122
  ) => {
103
123
  const attrs = fields ? flattenToAttributes(fields) : undefined;
104
- activeContext.addEvent(`log.${level}`, {
124
+ emitCorrelatedEvent(activeContext, `log.${level}`, {
105
125
  message,
106
- ...attrs,
126
+ ...(attrs ?? {}),
107
127
  });
108
128
  };
109
129
 
@@ -191,7 +211,7 @@ export function getRequestLogger(
191
211
  context: mergedContext,
192
212
  };
193
213
 
194
- activeContext.addEvent('log.emit.manual', {
214
+ emitCorrelatedEvent(activeContext, 'log.emit.manual', {
195
215
  ...flattened,
196
216
  });
197
217
 
@@ -206,7 +226,11 @@ export function getRequestLogger(
206
226
  return snapshot;
207
227
  },
208
228
 
209
- fork(label: string, fn: () => void | Promise<void>): void {
229
+ fork(
230
+ label: string,
231
+ fn: () => void | Promise<void>,
232
+ forkOptions?: ForkOptions,
233
+ ): void {
210
234
  const parentRequestId = activeContext.correlationId;
211
235
  if (typeof parentRequestId !== 'string' || parentRequestId.length === 0) {
212
236
  throw new Error(
@@ -216,6 +240,7 @@ export function getRequestLogger(
216
240
  }
217
241
 
218
242
  const tracer = otelTrace.getTracer('autotel.request-logger');
243
+ const lifecycle = forkOptions?.lifecycle;
219
244
  void tracer.startActiveSpan(`request.fork:${label}`, (childSpan) => {
220
245
  const childContext: TraceContext = {
221
246
  ...createTraceContext(childSpan),
@@ -229,6 +254,8 @@ export function getRequestLogger(
229
254
  _parentCorrelationId: parentRequestId,
230
255
  });
231
256
 
257
+ lifecycle?.onChildEnter?.(childLog);
258
+
232
259
  void Promise.resolve()
233
260
  .then(() => fn())
234
261
  .then(() => {
@@ -240,6 +267,14 @@ export function getRequestLogger(
240
267
  childLog.emitNow();
241
268
  })
242
269
  .finally(() => {
270
+ try {
271
+ lifecycle?.onChildExit?.(childLog);
272
+ } catch (hookError) {
273
+ console.warn(
274
+ '[autotel] fork onChildExit hook threw:',
275
+ hookError,
276
+ );
277
+ }
243
278
  childSpan.end();
244
279
  });
245
280
  });
@@ -1,10 +1,41 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
+ import type { Span, SpanContext } from '@opentelemetry/api';
2
3
  import {
3
4
  createStructuredError,
4
5
  getStructuredErrorAttributes,
5
6
  recordStructuredError,
6
7
  } from './structured-error';
7
- import type { TraceContext } from './trace-context';
8
+ import { createTraceContext, type TraceContext } from './trace-context';
9
+
10
+ function createFakeSpan(): {
11
+ span: Span;
12
+ recordException: ReturnType<typeof vi.fn>;
13
+ setStatus: ReturnType<typeof vi.fn>;
14
+ setAttributes: ReturnType<typeof vi.fn>;
15
+ } {
16
+ const recordException = vi.fn();
17
+ const setStatus = vi.fn();
18
+ const setAttributes = vi.fn();
19
+ const spanContext: SpanContext = {
20
+ traceId: '0123456789abcdef0123456789abcdef',
21
+ spanId: '0123456789abcdef',
22
+ traceFlags: 1,
23
+ };
24
+ const span = {
25
+ spanContext: () => spanContext,
26
+ setAttribute: vi.fn(),
27
+ setAttributes,
28
+ setStatus,
29
+ recordException,
30
+ addEvent: vi.fn(),
31
+ addLink: vi.fn(),
32
+ addLinks: vi.fn(),
33
+ updateName: vi.fn(),
34
+ isRecording: () => true,
35
+ end: vi.fn(),
36
+ } as unknown as Span;
37
+ return { span, recordException, setStatus, setAttributes };
38
+ }
8
39
 
9
40
  describe('structured-error helpers', () => {
10
41
  it('creates an error with structured diagnostic fields', () => {
@@ -104,3 +135,57 @@ describe('structured-error helpers', () => {
104
135
  );
105
136
  });
106
137
  });
138
+
139
+ describe('ctx.recordError', () => {
140
+ it('records a structured error onto the underlying span', () => {
141
+ const { span, recordException, setStatus, setAttributes } =
142
+ createFakeSpan();
143
+ const ctx = createTraceContext(span);
144
+ const err = createStructuredError({
145
+ message: 'Order failed',
146
+ why: 'Inventory unavailable',
147
+ fix: 'Retry after restock',
148
+ });
149
+
150
+ ctx.recordError(err);
151
+
152
+ expect(recordException).toHaveBeenCalledWith(err);
153
+ expect(setStatus).toHaveBeenCalledWith({
154
+ code: 2,
155
+ message: 'Order failed',
156
+ });
157
+ expect(setAttributes).toHaveBeenCalledWith(
158
+ expect.objectContaining({
159
+ 'error.message': 'Order failed',
160
+ 'error.why': 'Inventory unavailable',
161
+ 'error.fix': 'Retry after restock',
162
+ }),
163
+ );
164
+ });
165
+
166
+ it('coerces non-Error values to Error so it is safe in catch blocks', () => {
167
+ const { span, recordException, setStatus, setAttributes } =
168
+ createFakeSpan();
169
+ const ctx = createTraceContext(span);
170
+
171
+ ctx.recordError('boom');
172
+
173
+ expect(recordException).toHaveBeenCalledTimes(1);
174
+ const recorded = recordException.mock.calls[0][0];
175
+ expect(recorded).toBeInstanceOf(Error);
176
+ expect(recorded.message).toBe('boom');
177
+ expect(setStatus).toHaveBeenCalledWith({ code: 2, message: 'boom' });
178
+ expect(setAttributes).toHaveBeenCalled();
179
+ });
180
+ });
181
+
182
+ describe('ctx.track', () => {
183
+ it('exposes track on the trace context as the ergonomic replacement for ctx.addEvent', () => {
184
+ const { span } = createFakeSpan();
185
+ const ctx = createTraceContext(span);
186
+
187
+ expect(typeof ctx.track).toBe('function');
188
+ // Smoke test — should not throw without init() (track is a no-op when no queue is configured)
189
+ expect(() => ctx.track('test.event', { foo: 'bar' })).not.toThrow();
190
+ });
191
+ });
@@ -138,10 +138,17 @@ export function getStructuredErrorAttributes(
138
138
  }
139
139
 
140
140
  export function recordStructuredError(
141
- ctx: Pick<TraceContext, 'recordException' | 'setAttributes' | 'setStatus'>,
141
+ ctx: Pick<TraceContext, 'setAttributes' | 'setStatus'>,
142
142
  error: Error,
143
143
  ): void {
144
- ctx.recordException(error);
144
+ const maybeRecordException = (
145
+ ctx as unknown as {
146
+ recordException?: (e: Error) => void;
147
+ }
148
+ ).recordException;
149
+ if (typeof maybeRecordException === 'function') {
150
+ maybeRecordException(error);
151
+ }
145
152
  ctx.setStatus({
146
153
  code: SpanStatusCode.ERROR,
147
154
  message: error.message,
@@ -8,10 +8,11 @@ import type {
8
8
  BaggageEntry,
9
9
  Context,
10
10
  Link,
11
- TimeInput,
12
11
  } from '@opentelemetry/api';
13
12
  import { context, propagation } from '@opentelemetry/api';
14
13
  import { AsyncLocalStorage } from 'node:async_hooks';
14
+ import { recordStructuredError } from './structured-error';
15
+ import { track } from './track';
15
16
 
16
17
  type AsyncLocalBox<T> = {
17
18
  value: T;
@@ -132,14 +133,6 @@ export interface SpanMethods {
132
133
  setAttributes(attrs: Record<string, AttributeValue>): void;
133
134
  /** Set the status of the span */
134
135
  setStatus(status: { code: SpanStatusCode; message?: string }): void;
135
- /** Record an exception on the span */
136
- recordException(exception: Error, time?: TimeInput): void;
137
- /** Add an event to the span (for logging milestones/checkpoints) */
138
- addEvent(
139
- name: string,
140
- attributesOrStartTime?: Record<string, AttributeValue> | TimeInput,
141
- startTime?: TimeInput,
142
- ): void;
143
136
  /** Add a link to another span */
144
137
  addLink(link: Link): void;
145
138
  /** Add multiple links to other spans */
@@ -148,6 +141,26 @@ export interface SpanMethods {
148
141
  updateName(name: string): void;
149
142
  /** Check if the span is recording */
150
143
  isRecording(): boolean;
144
+ /**
145
+ * Record an error on the span: sets ERROR status, structured `error.*`
146
+ * attributes (including `why`/`fix`/`link` from `createStructuredError`),
147
+ * and during the OTel Span Event API back-compat window also records the
148
+ * exception via the legacy span event API.
149
+ *
150
+ * Replaces the deprecated `recordException` (OTEP 4430). Accepts `unknown`
151
+ * so it can be called directly with the value caught from a `catch` block.
152
+ */
153
+ recordError(error: unknown): void;
154
+ /**
155
+ * Emit a tracked event correlated to this span. Equivalent to the standalone
156
+ * `track(event, data)` but reads naturally on `ctx`. Replaces the deprecated
157
+ * `ctx.addEvent` (OTEP 4430) — events become correlated logs rather than
158
+ * span events.
159
+ */
160
+ track<Events extends Record<string, unknown> = Record<string, unknown>>(
161
+ event: keyof Events & string,
162
+ data?: Events[keyof Events & string],
163
+ ): void;
151
164
  }
152
165
 
153
166
  /**
@@ -411,7 +424,13 @@ export function createTraceContext<
411
424
  : never,
412
425
  };
413
426
 
414
- return {
427
+ // `recordException` and `addEvent` are intentionally bound at runtime but
428
+ // omitted from the `SpanMethods` type. They exist solely so existing call
429
+ // sites keep working through the OTel Span Event API deprecation window
430
+ // (see MIGRATION.md). New code MUST go through `recordStructuredError`,
431
+ // `emitCorrelatedEvent`, or the request logger. The cast below is what hides
432
+ // these compatibility-only fields from the public type.
433
+ const traceCtx = {
415
434
  traceId: spanContext.traceId,
416
435
  spanId: spanContext.spanId,
417
436
  correlationId: spanContext.traceId.slice(0, 16),
@@ -424,8 +443,17 @@ export function createTraceContext<
424
443
  addLinks: span.addLinks.bind(span),
425
444
  updateName: span.updateName.bind(span),
426
445
  isRecording: span.isRecording.bind(span),
446
+ recordError: (error: unknown) => {
447
+ const err = error instanceof Error ? error : new Error(String(error));
448
+ recordStructuredError(traceCtx, err);
449
+ },
450
+ track: (event: string, data?: Record<string, unknown>) => {
451
+ track(event, data);
452
+ },
427
453
  ...baggageHelpers,
428
- };
454
+ } as unknown as TraceContext<TBaggage>;
455
+
456
+ return traceCtx;
429
457
  }
430
458
 
431
459
  /**