autotel 2.26.3 → 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 (144) hide show
  1. package/README.md +29 -19
  2. package/dist/attributes.d.cts +3 -3
  3. package/dist/attributes.d.ts +3 -3
  4. package/dist/business-baggage.d.cts +1 -1
  5. package/dist/business-baggage.d.ts +1 -1
  6. package/dist/{chunk-YN7USLHW.js → chunk-3QMFLJHJ.js} +11 -10
  7. package/dist/chunk-3QMFLJHJ.js.map +1 -0
  8. package/dist/{chunk-BJ2XPN77.js → chunk-4DAG3RFS.js} +4 -4
  9. package/dist/{chunk-BJ2XPN77.js.map → chunk-4DAG3RFS.js.map} +1 -1
  10. package/dist/chunk-4P6ZOARG.cjs +33 -0
  11. package/dist/chunk-4P6ZOARG.cjs.map +1 -0
  12. package/dist/{chunk-U54FTVFH.js → chunk-7HNQYHK4.js} +3 -3
  13. package/dist/{chunk-U54FTVFH.js.map → chunk-7HNQYHK4.js.map} +1 -1
  14. package/dist/chunk-CJ4PD2TZ.cjs +1207 -0
  15. package/dist/chunk-CJ4PD2TZ.cjs.map +1 -0
  16. package/dist/{chunk-HPUGKUMZ.js → chunk-DAAJLUTO.js} +13 -640
  17. package/dist/chunk-DAAJLUTO.js.map +1 -0
  18. package/dist/{chunk-B3ZHLLMP.js → chunk-DSMSIVTG.js} +2 -2
  19. package/dist/chunk-DSMSIVTG.js.map +1 -0
  20. package/dist/{chunk-6YGUN7IY.cjs → chunk-DWOBIBLY.cjs} +18 -17
  21. package/dist/chunk-DWOBIBLY.cjs.map +1 -0
  22. package/dist/{chunk-QC5MNKVF.js → chunk-IUDXKLS4.js} +13 -12
  23. package/dist/chunk-IUDXKLS4.js.map +1 -0
  24. package/dist/{chunk-OBWXM4NN.cjs → chunk-KHGA4OST.cjs} +15 -14
  25. package/dist/chunk-KHGA4OST.cjs.map +1 -0
  26. package/dist/chunk-KIL5CUN6.js +31 -0
  27. package/dist/chunk-KIL5CUN6.js.map +1 -0
  28. package/dist/{chunk-YEVCD6DR.cjs → chunk-L7JDUDJD.cjs} +7 -7
  29. package/dist/{chunk-YEVCD6DR.cjs.map → chunk-L7JDUDJD.cjs.map} +1 -1
  30. package/dist/{chunk-UTZR7P7E.cjs → chunk-MOK3E54E.cjs} +29 -659
  31. package/dist/chunk-MOK3E54E.cjs.map +1 -0
  32. package/dist/{chunk-GML3FBOT.cjs → chunk-NCSMD3TK.cjs} +2 -2
  33. package/dist/chunk-NCSMD3TK.cjs.map +1 -0
  34. package/dist/chunk-QG3U5ONP.js +1183 -0
  35. package/dist/chunk-QG3U5ONP.js.map +1 -0
  36. package/dist/chunk-SEO6NAQT.js +14 -0
  37. package/dist/chunk-SEO6NAQT.js.map +1 -0
  38. package/dist/chunk-VQTCQKHQ.cjs +17 -0
  39. package/dist/chunk-VQTCQKHQ.cjs.map +1 -0
  40. package/dist/{chunk-WZOKY3PW.cjs → chunk-ZSABTI3C.cjs} +8 -8
  41. package/dist/{chunk-WZOKY3PW.cjs.map → chunk-ZSABTI3C.cjs.map} +1 -1
  42. package/dist/correlation-id.cjs +22 -10
  43. package/dist/correlation-id.js +14 -2
  44. package/dist/decorators.cjs +5 -6
  45. package/dist/decorators.cjs.map +1 -1
  46. package/dist/decorators.d.cts +1 -1
  47. package/dist/decorators.d.ts +1 -1
  48. package/dist/decorators.js +4 -5
  49. package/dist/decorators.js.map +1 -1
  50. package/dist/event.cjs +6 -7
  51. package/dist/event.js +3 -4
  52. package/dist/functional.cjs +11 -12
  53. package/dist/functional.d.cts +1 -1
  54. package/dist/functional.d.ts +1 -1
  55. package/dist/functional.js +4 -5
  56. package/dist/http.cjs +13 -2
  57. package/dist/http.cjs.map +1 -1
  58. package/dist/http.js +12 -1
  59. package/dist/http.js.map +1 -1
  60. package/dist/index.cjs +134 -243
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.d.cts +23 -8
  63. package/dist/index.d.ts +23 -8
  64. package/dist/index.js +52 -176
  65. package/dist/index.js.map +1 -1
  66. package/dist/messaging-adapters.d.cts +1 -1
  67. package/dist/messaging-adapters.d.ts +1 -1
  68. package/dist/messaging-testing.d.cts +1 -1
  69. package/dist/messaging-testing.d.ts +1 -1
  70. package/dist/messaging.cjs +9 -9
  71. package/dist/messaging.d.cts +1 -1
  72. package/dist/messaging.d.ts +1 -1
  73. package/dist/messaging.js +6 -6
  74. package/dist/semantic-helpers.cjs +9 -10
  75. package/dist/semantic-helpers.d.cts +1 -1
  76. package/dist/semantic-helpers.d.ts +1 -1
  77. package/dist/semantic-helpers.js +5 -6
  78. package/dist/{trace-context-t5X1AP-e.d.ts → trace-context-DbGKd1Rn.d.cts} +18 -5
  79. package/dist/{trace-context-t5X1AP-e.d.cts → trace-context-DbGKd1Rn.d.ts} +18 -5
  80. package/dist/trace-helpers.cjs +13 -13
  81. package/dist/trace-helpers.d.cts +2 -2
  82. package/dist/trace-helpers.d.ts +2 -2
  83. package/dist/trace-helpers.js +1 -1
  84. package/dist/{utils-CbUkl8r1.d.cts → utils-BahBCFtJ.d.cts} +1 -1
  85. package/dist/{utils-Buel3cj0.d.ts → utils-CLKwaUlG.d.ts} +1 -1
  86. package/dist/webhook.cjs +19 -10
  87. package/dist/webhook.cjs.map +1 -1
  88. package/dist/webhook.d.cts +1 -1
  89. package/dist/webhook.d.ts +1 -1
  90. package/dist/webhook.js +18 -9
  91. package/dist/webhook.js.map +1 -1
  92. package/dist/workflow-distributed.cjs +23 -19
  93. package/dist/workflow-distributed.cjs.map +1 -1
  94. package/dist/workflow-distributed.d.cts +1 -1
  95. package/dist/workflow-distributed.d.ts +1 -1
  96. package/dist/workflow-distributed.js +21 -17
  97. package/dist/workflow-distributed.js.map +1 -1
  98. package/dist/workflow.cjs +10 -10
  99. package/dist/workflow.d.cts +1 -1
  100. package/dist/workflow.d.ts +1 -1
  101. package/dist/workflow.js +6 -6
  102. package/package.json +1 -1
  103. package/skills/autotel-core/SKILL.md +2 -0
  104. package/skills/autotel-events/SKILL.md +2 -0
  105. package/skills/autotel-frameworks/SKILL.md +2 -0
  106. package/skills/autotel-instrumentation/SKILL.md +2 -0
  107. package/skills/autotel-request-logging/SKILL.md +2 -0
  108. package/skills/autotel-structured-errors/SKILL.md +2 -0
  109. package/src/correlated-events.test.ts +151 -0
  110. package/src/correlated-events.ts +47 -0
  111. package/src/functional.ts +2 -0
  112. package/src/gen-ai-events.ts +14 -5
  113. package/src/index.ts +20 -4
  114. package/src/messaging.ts +10 -9
  115. package/src/request-logger.ts +4 -3
  116. package/src/structured-error.test.ts +83 -1
  117. package/src/structured-error.ts +9 -2
  118. package/src/trace-context.ts +39 -11
  119. package/src/trace-helpers.ts +2 -2
  120. package/src/trace-hybrid.test.ts +42 -0
  121. package/src/trace-hybrid.ts +37 -0
  122. package/src/webhook.ts +16 -7
  123. package/src/workflow-distributed.ts +18 -13
  124. package/src/workflow.ts +7 -6
  125. package/dist/chunk-6YGUN7IY.cjs.map +0 -1
  126. package/dist/chunk-B3ZHLLMP.js.map +0 -1
  127. package/dist/chunk-BBBWDIYQ.js +0 -211
  128. package/dist/chunk-BBBWDIYQ.js.map +0 -1
  129. package/dist/chunk-D5LMF53P.cjs +0 -150
  130. package/dist/chunk-D5LMF53P.cjs.map +0 -1
  131. package/dist/chunk-GML3FBOT.cjs.map +0 -1
  132. package/dist/chunk-HPUGKUMZ.js.map +0 -1
  133. package/dist/chunk-HZ3FYBJG.cjs +0 -217
  134. package/dist/chunk-HZ3FYBJG.cjs.map +0 -1
  135. package/dist/chunk-JSNUWSBH.cjs +0 -62
  136. package/dist/chunk-JSNUWSBH.cjs.map +0 -1
  137. package/dist/chunk-OBWXM4NN.cjs.map +0 -1
  138. package/dist/chunk-QC5MNKVF.js.map +0 -1
  139. package/dist/chunk-S4OFEXLA.js +0 -53
  140. package/dist/chunk-S4OFEXLA.js.map +0 -1
  141. package/dist/chunk-UTZR7P7E.cjs.map +0 -1
  142. package/dist/chunk-WD4RP6IV.js +0 -146
  143. package/dist/chunk-WD4RP6IV.js.map +0 -1
  144. package/dist/chunk-YN7USLHW.js.map +0 -1
package/src/index.ts CHANGED
@@ -81,7 +81,6 @@ export type {
81
81
  InstrumentOptions,
82
82
  } from './functional';
83
83
  export {
84
- trace,
85
84
  instrument,
86
85
  withTracing,
87
86
  span,
@@ -89,6 +88,9 @@ export {
89
88
  withBaggage,
90
89
  ctx,
91
90
  } from './functional';
91
+ // `trace` is the hybrid: callable like autotel's `trace(fn)` AND carries the
92
+ // full `@opentelemetry/api` TraceAPI surface (getActiveSpan, getTracer, etc).
93
+ export { trace } from './trace-hybrid';
92
94
 
93
95
  // Operation context (for advanced usage)
94
96
  export type { OperationContext } from './operation-context';
@@ -333,19 +335,33 @@ export {
333
335
  type TLSAttrs,
334
336
  } from './attributes';
335
337
 
336
- // Re-export common OpenTelemetry types and utilities
337
- // This allows plugins and apps to use OTel without needing separate @opentelemetry/api installation
338
+ // Re-export common OpenTelemetry types and utilities so plugins, apps, and
339
+ // existing OTel code can `import { ... } from 'autotel'` without also taking
340
+ // a separate `@opentelemetry/api` dependency.
338
341
  export type {
339
342
  Span,
340
343
  SpanContext,
344
+ SpanAttributes,
341
345
  Tracer,
346
+ TracerProvider,
342
347
  Context,
348
+ Attributes,
349
+ AttributeValue,
350
+ Link,
343
351
  Link as SpanLink,
352
+ TimeInput,
353
+ HrTime,
354
+ Baggage,
355
+ BaggageEntry,
356
+ Exception,
357
+ TraceFlags,
358
+ TraceState,
344
359
  TextMapSetter,
345
360
  TextMapGetter,
346
361
  } from '@opentelemetry/api';
347
362
  export { SpanKind, ROOT_CONTEXT } from '@opentelemetry/api';
348
- // Note: trace exported from functional.ts, context/propagation/SpanStatusCode already exported above
363
+ // Note: trace, context, propagation, SpanStatusCode already exported above
364
+ // (`trace` is the hybrid; `otelTrace` is the pure TraceAPI singleton).
349
365
 
350
366
  // Export typed baggage helper
351
367
  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
  });
@@ -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.";
@@ -101,9 +102,9 @@ export function getRequestLogger(
101
102
  fields?: Record<string, unknown>,
102
103
  ) => {
103
104
  const attrs = fields ? flattenToAttributes(fields) : undefined;
104
- activeContext.addEvent(`log.${level}`, {
105
+ emitCorrelatedEvent(activeContext, `log.${level}`, {
105
106
  message,
106
- ...attrs,
107
+ ...(attrs ?? {}),
107
108
  });
108
109
  };
109
110
 
@@ -191,7 +192,7 @@ export function getRequestLogger(
191
192
  context: mergedContext,
192
193
  };
193
194
 
194
- activeContext.addEvent('log.emit.manual', {
195
+ emitCorrelatedEvent(activeContext, 'log.emit.manual', {
195
196
  ...flattened,
196
197
  });
197
198
 
@@ -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,54 @@ 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({ code: 2, message: 'Order failed' });
154
+ expect(setAttributes).toHaveBeenCalledWith(
155
+ expect.objectContaining({
156
+ 'error.message': 'Order failed',
157
+ 'error.why': 'Inventory unavailable',
158
+ 'error.fix': 'Retry after restock',
159
+ }),
160
+ );
161
+ });
162
+
163
+ it('coerces non-Error values to Error so it is safe in catch blocks', () => {
164
+ const { span, recordException, setStatus, setAttributes } =
165
+ createFakeSpan();
166
+ const ctx = createTraceContext(span);
167
+
168
+ ctx.recordError('boom');
169
+
170
+ expect(recordException).toHaveBeenCalledTimes(1);
171
+ const recorded = recordException.mock.calls[0][0];
172
+ expect(recorded).toBeInstanceOf(Error);
173
+ expect(recorded.message).toBe('boom');
174
+ expect(setStatus).toHaveBeenCalledWith({ code: 2, message: 'boom' });
175
+ expect(setAttributes).toHaveBeenCalled();
176
+ });
177
+ });
178
+
179
+ describe('ctx.track', () => {
180
+ it('exposes track on the trace context as the ergonomic replacement for ctx.addEvent', () => {
181
+ const { span } = createFakeSpan();
182
+ const ctx = createTraceContext(span);
183
+
184
+ expect(typeof ctx.track).toBe('function');
185
+ // Smoke test — should not throw without init() (track is a no-op when no queue is configured)
186
+ expect(() => ctx.track('test.event', { foo: 'bar' })).not.toThrow();
187
+ });
188
+ });
@@ -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
  /**
@@ -241,7 +241,7 @@ export function getTracer(name: string, version?: string): Tracer {
241
241
  * Get the currently active span
242
242
  *
243
243
  * Returns undefined if no span is currently active.
244
- * Useful for adding attributes or events to the current span.
244
+ * Useful for adding attributes to the current span.
245
245
  *
246
246
  * @returns Active span or undefined
247
247
  *
@@ -252,7 +252,7 @@ export function getTracer(name: string, version?: string): Tracer {
252
252
  * const span = getActiveSpan();
253
253
  * if (span) {
254
254
  * span.setAttribute('user.id', userId);
255
- * span.addEvent('User action', { action: 'click' });
255
+ * span.setAttribute('user.action', 'click');
256
256
  * }
257
257
  * ```
258
258
  *
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { trace as otelTraceApi } from '@opentelemetry/api';
3
+ import { trace } from './trace-hybrid';
4
+
5
+ describe('hybrid trace', () => {
6
+ it('is callable like autotel trace', () => {
7
+ expect(typeof trace).toBe('function');
8
+
9
+ const wrapped = trace(async (x: number) => x * 2);
10
+ expect(typeof wrapped).toBe('function');
11
+ });
12
+
13
+ it('exposes the OTel TraceAPI surface', () => {
14
+ expect(typeof trace.getActiveSpan).toBe('function');
15
+ expect(typeof trace.getTracer).toBe('function');
16
+ expect(typeof trace.getTracerProvider).toBe('function');
17
+ expect(typeof trace.setSpan).toBe('function');
18
+ expect(typeof trace.getSpan).toBe('function');
19
+ expect(typeof trace.setSpanContext).toBe('function');
20
+ expect(typeof trace.getSpanContext).toBe('function');
21
+ expect(typeof trace.deleteSpan).toBe('function');
22
+ expect(typeof trace.wrapSpanContext).toBe('function');
23
+ expect(typeof trace.isSpanContextValid).toBe('function');
24
+ expect(typeof trace.disable).toBe('function');
25
+ expect(typeof trace.setGlobalTracerProvider).toBe('function');
26
+ });
27
+
28
+ it('forwards getActiveSpan / getTracerProvider to the OTel singleton', () => {
29
+ expect(trace.getActiveSpan()).toBe(otelTraceApi.getActiveSpan());
30
+ // Same TracerProvider singleton — guarantees getTracer goes through one
31
+ // place. (Tracer instances themselves may not be referentially identical
32
+ // because the proxy creates a new wrapper per call.)
33
+ expect(trace.getTracerProvider()).toBe(otelTraceApi.getTracerProvider());
34
+ });
35
+
36
+ it('preserves `this` for class methods (no unbound-this errors)', () => {
37
+ // setGlobalTracerProvider/getTracerProvider rely on `this._proxyTracerProvider`.
38
+ // Calling through the destructured reference must not throw.
39
+ const { getTracerProvider } = trace;
40
+ expect(() => getTracerProvider()).not.toThrow();
41
+ });
42
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Hybrid `trace` export: callable like autotel's `trace(fn)`, AND exposes the
3
+ * full `@opentelemetry/api` `TraceAPI` surface (`trace.getActiveSpan()`,
4
+ * `trace.getTracer()`, …) so existing OTel code "just works" when imported
5
+ * from `autotel`.
6
+ *
7
+ * Implementation: `Object.assign` mutates the autotel `trace` function to
8
+ * attach the OTel TraceAPI methods. Because every reference to `trace` across
9
+ * autotel resolves to the same function instance, this is a one-time, global
10
+ * augmentation.
11
+ */
12
+
13
+ import { trace as otelTraceApi } from '@opentelemetry/api';
14
+ import { trace as autotelTraceFn } from './functional';
15
+
16
+ const otelMethods = {
17
+ // Class methods on TraceAPI — bind to the singleton.
18
+ setGlobalTracerProvider:
19
+ otelTraceApi.setGlobalTracerProvider.bind(otelTraceApi),
20
+ getTracerProvider: otelTraceApi.getTracerProvider.bind(otelTraceApi),
21
+ getTracer: otelTraceApi.getTracer.bind(otelTraceApi),
22
+ disable: otelTraceApi.disable.bind(otelTraceApi),
23
+ // Instance fields on TraceAPI — already standalone, copy by reference.
24
+ wrapSpanContext: otelTraceApi.wrapSpanContext,
25
+ isSpanContextValid: otelTraceApi.isSpanContextValid,
26
+ deleteSpan: otelTraceApi.deleteSpan,
27
+ getSpan: otelTraceApi.getSpan,
28
+ getActiveSpan: otelTraceApi.getActiveSpan,
29
+ getSpanContext: otelTraceApi.getSpanContext,
30
+ setSpan: otelTraceApi.setSpan,
31
+ setSpanContext: otelTraceApi.setSpanContext,
32
+ };
33
+
34
+ export const trace: typeof autotelTraceFn & typeof otelTraceApi = Object.assign(
35
+ autotelTraceFn,
36
+ otelMethods,
37
+ ) as typeof autotelTraceFn & typeof otelTraceApi;
package/src/webhook.ts CHANGED
@@ -36,8 +36,10 @@
36
36
 
37
37
  import { SpanKind, trace as otelTrace } from '@opentelemetry/api';
38
38
  import type { SpanContext, Link } from '@opentelemetry/api';
39
+ import { emitCorrelatedEvent } from './correlated-events';
39
40
  import { trace } from './functional';
40
- import type { TraceContext } from './trace-context';
41
+ import type { AttributeValue, TraceContext } from './trace-context';
42
+ import { recordStructuredError } from './structured-error';
41
43
 
42
44
  // ============================================================================
43
45
  // Types
@@ -426,10 +428,9 @@ export function createParkingLot(config: ParkingLotConfig): ParkingLot {
426
428
 
427
429
  await store.save(fullKey, storedContext);
428
430
 
429
- // Add event to current span
430
431
  const activeSpan = otelTrace.getActiveSpan();
431
432
  if (activeSpan) {
432
- activeSpan.addEvent('trace_context_parked', {
433
+ const parkAttrs: Record<string, AttributeValue> = {
433
434
  'parking_lot.correlation_key': correlationKey,
434
435
  'parking_lot.ttl_ms': defaultTTLMs,
435
436
  ...(metadata &&
@@ -439,7 +440,16 @@ export function createParkingLot(config: ParkingLotConfig): ParkingLot {
439
440
  v,
440
441
  ]),
441
442
  )),
442
- });
443
+ };
444
+ emitCorrelatedEvent(
445
+ {
446
+ setAttribute: (k, v) => activeSpan.setAttribute(k, v),
447
+ setAttributes: (a) => activeSpan.setAttributes(a),
448
+ addEvent: (n, a) => activeSpan.addEvent(n, a),
449
+ },
450
+ 'trace_context_parked',
451
+ parkAttrs,
452
+ );
443
453
  }
444
454
 
445
455
  // Return the unprefixed key so callers can use the same key for retrieve()
@@ -520,8 +530,7 @@ export function createParkingLot(config: ParkingLotConfig): ParkingLot {
520
530
  const link = parkingLot.createLink(parkedContext);
521
531
  baseCtx.addLinks([link]);
522
532
 
523
- // Add event
524
- baseCtx.addEvent('parked_context_retrieved', {
533
+ emitCorrelatedEvent(baseCtx, 'parked_context_retrieved', {
525
534
  'parking_lot.correlation_key': correlationKey,
526
535
  'parking_lot.elapsed_ms': elapsedMs!,
527
536
  'parking_lot.original_trace_id': parkedContext.traceId,
@@ -533,7 +542,7 @@ export function createParkingLot(config: ParkingLotConfig): ParkingLot {
533
542
  const error = new Error(
534
543
  `Required parked context not found for key: ${correlationKey}`,
535
544
  );
536
- baseCtx.recordException(error);
545
+ recordStructuredError(baseCtx, error);
537
546
  throw error;
538
547
  }
539
548
  }
@@ -49,6 +49,7 @@
49
49
 
50
50
  import { context, propagation, SpanKind } from '@opentelemetry/api';
51
51
  import { createSafeBaggageSchema } from './business-baggage';
52
+ import { emitCorrelatedEvent } from './correlated-events';
52
53
  import { trace } from './functional';
53
54
  import type { TraceContext } from './trace-context';
54
55
 
@@ -422,7 +423,7 @@ export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
422
423
  baggageValues.stepIndex = stepIndex;
423
424
  WorkflowBaggage.set(baseCtx, baggageValues);
424
425
 
425
- baseCtx.addEvent('workflow.step_progress', {
426
+ emitCorrelatedEvent(baseCtx, 'workflow.step_progress', {
426
427
  'workflow.step.name': stepName,
427
428
  'workflow.step.index': stepIndex,
428
429
  });
@@ -433,7 +434,7 @@ export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
433
434
  config.onStart?.(workflowCtx);
434
435
 
435
436
  // Add start event
436
- baseCtx.addEvent('workflow.started', {
437
+ emitCorrelatedEvent(baseCtx, 'workflow.started', {
437
438
  'workflow.id': workflowId,
438
439
  'workflow.name': config.name,
439
440
  });
@@ -446,7 +447,7 @@ export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
446
447
  config.onComplete?.(workflowCtx, result);
447
448
 
448
449
  // Add completion event
449
- baseCtx.addEvent('workflow.completed', {
450
+ emitCorrelatedEvent(baseCtx, 'workflow.completed', {
450
451
  'workflow.id': workflowId,
451
452
  });
452
453
 
@@ -456,7 +457,7 @@ export function traceDistributedWorkflow<TArgs extends unknown[], TReturn>(
456
457
  config.onError?.(workflowCtx, error as Error);
457
458
 
458
459
  // Add error event
459
- baseCtx.addEvent('workflow.failed', {
460
+ emitCorrelatedEvent(baseCtx, 'workflow.failed', {
460
461
  'workflow.id': workflowId,
461
462
  'workflow.error': (error as Error).message,
462
463
  });
@@ -634,12 +635,16 @@ export function traceDistributedStep<TArgs extends unknown[], TReturn>(
634
635
  requiresCompensation(data?: Record<string, unknown>): void {
635
636
  compensationData = data;
636
637
  baseCtx.setAttribute('workflow.step.requires_compensation', true);
637
- baseCtx.addEvent('workflow.step.compensation_registered', {
638
- 'workflow.step.name': config.name,
639
- ...(data && {
640
- 'workflow.step.compensation_data': JSON.stringify(data),
641
- }),
642
- });
638
+ emitCorrelatedEvent(
639
+ baseCtx,
640
+ 'workflow.step.compensation_registered',
641
+ {
642
+ 'workflow.step.name': config.name,
643
+ ...(data && {
644
+ 'workflow.step.compensation_data': JSON.stringify(data),
645
+ }),
646
+ },
647
+ );
643
648
  },
644
649
  };
645
650
 
@@ -647,7 +652,7 @@ export function traceDistributedStep<TArgs extends unknown[], TReturn>(
647
652
  config.onStart?.(stepCtx);
648
653
 
649
654
  // Add start event
650
- baseCtx.addEvent('workflow.step.started', {
655
+ emitCorrelatedEvent(baseCtx, 'workflow.step.started', {
651
656
  'workflow.step.name': config.name,
652
657
  ...(baggageValues && { 'workflow.id': baggageValues.workflowId }),
653
658
  });
@@ -660,7 +665,7 @@ export function traceDistributedStep<TArgs extends unknown[], TReturn>(
660
665
  config.onComplete?.(stepCtx, result);
661
666
 
662
667
  // Add completion event
663
- baseCtx.addEvent('workflow.step.completed', {
668
+ emitCorrelatedEvent(baseCtx, 'workflow.step.completed', {
664
669
  'workflow.step.name': config.name,
665
670
  });
666
671
 
@@ -670,7 +675,7 @@ export function traceDistributedStep<TArgs extends unknown[], TReturn>(
670
675
  config.onError?.(stepCtx, error as Error);
671
676
 
672
677
  // Add error event with compensation info if registered
673
- baseCtx.addEvent('workflow.step.failed', {
678
+ emitCorrelatedEvent(baseCtx, 'workflow.step.failed', {
674
679
  'workflow.step.name': config.name,
675
680
  'workflow.step.error': (error as Error).message,
676
681
  ...(compensationData && {