autotel 4.0.0 → 4.2.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 (232) hide show
  1. package/README.md +26 -1
  2. package/dist/auto.cjs +2 -2
  3. package/dist/auto.js +1 -1
  4. package/dist/correlation-id.cjs +1 -1
  5. package/dist/correlation-id.js +1 -1
  6. package/dist/decorators.cjs +1 -1
  7. package/dist/decorators.js +1 -1
  8. package/dist/{event-Dlqr4ZNL.cjs → event-BhHREDJk.cjs} +3 -3
  9. package/dist/{event-Dlqr4ZNL.cjs.map → event-BhHREDJk.cjs.map} +1 -1
  10. package/dist/{event-_58ryBjh.js → event-ByBTV9M2.js} +3 -3
  11. package/dist/{event-_58ryBjh.js.map → event-ByBTV9M2.js.map} +1 -1
  12. package/dist/event.cjs +1 -1
  13. package/dist/event.js +1 -1
  14. package/dist/{functional-BGkT8J-h.js → functional-DtI0u4vx.js} +19 -19
  15. package/dist/functional-DtI0u4vx.js.map +1 -0
  16. package/dist/{functional-C4CzoVrX.cjs → functional-zpzNLhky.cjs} +4 -4
  17. package/dist/{functional-C4CzoVrX.cjs.map → functional-zpzNLhky.cjs.map} +1 -1
  18. package/dist/functional.cjs +1 -1
  19. package/dist/functional.js +1 -1
  20. package/dist/http.cjs +1 -1
  21. package/dist/http.js +1 -1
  22. package/dist/index.cjs +5 -5
  23. package/dist/index.d.cts +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +5 -5
  26. package/dist/{init-DJQOdVlN.d.ts → init-B7u-DjxM.d.ts} +57 -2
  27. package/dist/init-B7u-DjxM.d.ts.map +1 -0
  28. package/dist/{init-DvapOXCc.cjs → init-BX7AmFRl.cjs} +40 -21
  29. package/dist/init-BX7AmFRl.cjs.map +1 -0
  30. package/dist/{init-Ch6t7MNI.js → init-D-jnNMix.js} +39 -20
  31. package/dist/init-D-jnNMix.js.map +1 -0
  32. package/dist/{init-CNp-ee80.d.cts → init-DSrRmVnz.d.cts} +57 -2
  33. package/dist/init-DSrRmVnz.d.cts.map +1 -0
  34. package/dist/instrumentation.cjs +1 -1
  35. package/dist/instrumentation.js +1 -1
  36. package/dist/logger-D3Ej3DII.js +446 -0
  37. package/dist/logger-D3Ej3DII.js.map +1 -0
  38. package/dist/logger-thMPLpOG.cjs +487 -0
  39. package/dist/logger-thMPLpOG.cjs.map +1 -0
  40. package/dist/logger.cjs +8 -236
  41. package/dist/logger.js +2 -204
  42. package/dist/messaging.cjs +1 -1
  43. package/dist/messaging.js +1 -1
  44. package/dist/semantic-helpers.cjs +1 -1
  45. package/dist/semantic-helpers.js +1 -1
  46. package/dist/{track-3HY4NGV-.cjs → track-D59FfpL0.cjs} +2 -2
  47. package/dist/{track-3HY4NGV-.cjs.map → track-D59FfpL0.cjs.map} +1 -1
  48. package/dist/{track-nsKVy-pj.js → track-wc0HafS_.js} +6 -6
  49. package/dist/track-wc0HafS_.js.map +1 -0
  50. package/dist/webhook.cjs +1 -1
  51. package/dist/webhook.js +1 -1
  52. package/dist/workflow-distributed.cjs +1 -1
  53. package/dist/workflow-distributed.js +1 -1
  54. package/dist/workflow.cjs +1 -1
  55. package/dist/workflow.js +1 -1
  56. package/dist/{yaml-config-B3dQ82GR.cjs → yaml-config-Ck2uB0Dp.cjs} +2 -1
  57. package/dist/yaml-config-Ck2uB0Dp.cjs.map +1 -0
  58. package/dist/yaml-config.cjs +1 -1
  59. package/dist/yaml-config.d.cts +7 -1
  60. package/dist/yaml-config.d.cts.map +1 -1
  61. package/dist/yaml-config.d.ts +7 -1
  62. package/dist/yaml-config.d.ts.map +1 -1
  63. package/dist/yaml-config.js +1 -0
  64. package/dist/yaml-config.js.map +1 -1
  65. package/package.json +1 -2
  66. package/skills/autotel-core/SKILL.md +2 -0
  67. package/skills/autotel-instrumentation/SKILL.md +25 -0
  68. package/skills/debug-missing-spans/SKILL.md +3 -1
  69. package/skills/migrate-to-autotel/SKILL.md +24 -23
  70. package/skills/review-otel-patterns/SKILL.md +5 -4
  71. package/dist/functional-BGkT8J-h.js.map +0 -1
  72. package/dist/init-CNp-ee80.d.cts.map +0 -1
  73. package/dist/init-Ch6t7MNI.js.map +0 -1
  74. package/dist/init-DJQOdVlN.d.ts.map +0 -1
  75. package/dist/init-DvapOXCc.cjs.map +0 -1
  76. package/dist/logger.cjs.map +0 -1
  77. package/dist/logger.js.map +0 -1
  78. package/dist/track-nsKVy-pj.js.map +0 -1
  79. package/dist/yaml-config-B3dQ82GR.cjs.map +0 -1
  80. package/src/attribute-redacting-processor.test.ts +0 -763
  81. package/src/attribute-redacting-processor.ts +0 -621
  82. package/src/attributes/attachers.ts +0 -161
  83. package/src/attributes/builders.ts +0 -529
  84. package/src/attributes/domains.ts +0 -42
  85. package/src/attributes/index.ts +0 -81
  86. package/src/attributes/registry.ts +0 -323
  87. package/src/attributes/types.ts +0 -211
  88. package/src/attributes/utils.ts +0 -64
  89. package/src/attributes/validators.ts +0 -266
  90. package/src/attributes.test.ts +0 -292
  91. package/src/auto.ts +0 -67
  92. package/src/autotel-logger.test.ts +0 -548
  93. package/src/autotel-logger.ts +0 -364
  94. package/src/baggage-span-processor.test.ts +0 -202
  95. package/src/baggage-span-processor.ts +0 -100
  96. package/src/business-baggage.test.ts +0 -500
  97. package/src/business-baggage.ts +0 -669
  98. package/src/circuit-breaker.test.ts +0 -341
  99. package/src/circuit-breaker.ts +0 -184
  100. package/src/config.test.ts +0 -94
  101. package/src/config.ts +0 -172
  102. package/src/correlated-events.test.ts +0 -151
  103. package/src/correlated-events.ts +0 -47
  104. package/src/correlation-id.test.ts +0 -163
  105. package/src/correlation-id.ts +0 -206
  106. package/src/db.test.ts +0 -252
  107. package/src/db.ts +0 -447
  108. package/src/decorators.test.ts +0 -153
  109. package/src/decorators.ts +0 -188
  110. package/src/define-event.test.ts +0 -41
  111. package/src/define-event.ts +0 -58
  112. package/src/devtools.ts +0 -60
  113. package/src/drain-pipeline.test.ts +0 -68
  114. package/src/drain-pipeline.ts +0 -199
  115. package/src/drain-toolkit.test.ts +0 -113
  116. package/src/drain-toolkit.ts +0 -129
  117. package/src/enricher-toolkit.test.ts +0 -67
  118. package/src/enricher-toolkit.ts +0 -79
  119. package/src/enrichers.test.ts +0 -150
  120. package/src/enrichers.ts +0 -145
  121. package/src/env-config.test.ts +0 -323
  122. package/src/env-config.ts +0 -309
  123. package/src/error-catalog.test.ts +0 -133
  124. package/src/error-catalog.ts +0 -262
  125. package/src/event-queue.test.ts +0 -864
  126. package/src/event-queue.ts +0 -699
  127. package/src/event-subscriber.ts +0 -262
  128. package/src/event-testing.ts +0 -197
  129. package/src/event.test.ts +0 -1104
  130. package/src/event.ts +0 -988
  131. package/src/events-config.ts +0 -235
  132. package/src/exporters.ts +0 -165
  133. package/src/filtering-span-processor.test.ts +0 -281
  134. package/src/filtering-span-processor.ts +0 -111
  135. package/src/flatten-attributes.test.ts +0 -76
  136. package/src/flatten-attributes.ts +0 -80
  137. package/src/functional.strict-types.typecheck.ts +0 -53
  138. package/src/functional.test.ts +0 -1464
  139. package/src/functional.ts +0 -2539
  140. package/src/functional.types.test.ts +0 -135
  141. package/src/hook.mjs +0 -15
  142. package/src/http.test.ts +0 -485
  143. package/src/http.ts +0 -424
  144. package/src/index.ts +0 -433
  145. package/src/init-auto-redactor.test.ts +0 -53
  146. package/src/init-redactor.test.ts +0 -8
  147. package/src/init.customization.test.ts +0 -594
  148. package/src/init.integrations.test.ts +0 -399
  149. package/src/init.openllmetry.test.ts +0 -194
  150. package/src/init.protocol.test.ts +0 -215
  151. package/src/init.ts +0 -2312
  152. package/src/instrumentation.test.ts +0 -108
  153. package/src/instrumentation.ts +0 -319
  154. package/src/logger.test.ts +0 -125
  155. package/src/logger.ts +0 -341
  156. package/src/messaging-adapters.test.ts +0 -595
  157. package/src/messaging-adapters.ts +0 -583
  158. package/src/messaging-testing.test.ts +0 -573
  159. package/src/messaging-testing.ts +0 -935
  160. package/src/messaging.test.ts +0 -1646
  161. package/src/messaging.ts +0 -2245
  162. package/src/metric-helpers.ts +0 -47
  163. package/src/metric-testing.ts +0 -197
  164. package/src/metric.ts +0 -446
  165. package/src/metrics.test.ts +0 -241
  166. package/src/node-require.ts +0 -123
  167. package/src/operation-context.ts +0 -93
  168. package/src/parse-error.test.ts +0 -73
  169. package/src/parse-error.ts +0 -112
  170. package/src/posthog-logs.test.ts +0 -115
  171. package/src/posthog-logs.ts +0 -77
  172. package/src/pretty-console-exporter.test.ts +0 -545
  173. package/src/pretty-console-exporter.ts +0 -413
  174. package/src/pretty-log-formatter.test.ts +0 -123
  175. package/src/pretty-log-formatter.ts +0 -210
  176. package/src/processors/canonical-log-line-processor.test.ts +0 -523
  177. package/src/processors/canonical-log-line-processor.ts +0 -396
  178. package/src/processors.ts +0 -152
  179. package/src/rate-limiter.test.ts +0 -199
  180. package/src/rate-limiter.ts +0 -98
  181. package/src/redact-values.test.ts +0 -90
  182. package/src/redact-values.ts +0 -34
  183. package/src/register.ts +0 -37
  184. package/src/request-logger.test.ts +0 -545
  185. package/src/request-logger.ts +0 -342
  186. package/src/sampling.test.ts +0 -1060
  187. package/src/sampling.ts +0 -737
  188. package/src/security-schema.test.ts +0 -45
  189. package/src/security-schema.ts +0 -107
  190. package/src/semantic-conventions.ts +0 -15
  191. package/src/semantic-helpers.test.ts +0 -226
  192. package/src/semantic-helpers.ts +0 -438
  193. package/src/shutdown.test.ts +0 -364
  194. package/src/shutdown.ts +0 -246
  195. package/src/span-name-normalizer.test.ts +0 -377
  196. package/src/span-name-normalizer.ts +0 -213
  197. package/src/stable-hash.ts +0 -27
  198. package/src/structured-error.test.ts +0 -191
  199. package/src/structured-error.ts +0 -157
  200. package/src/stub.integration.test.ts +0 -361
  201. package/src/tail-sampling-processor.test.ts +0 -230
  202. package/src/tail-sampling-processor.ts +0 -55
  203. package/src/test-span-collector.test.ts +0 -234
  204. package/src/test-span-collector.ts +0 -150
  205. package/src/testing.ts +0 -705
  206. package/src/trace-context.test.ts +0 -73
  207. package/src/trace-context.ts +0 -567
  208. package/src/trace-helpers.new.test.ts +0 -278
  209. package/src/trace-helpers.test.ts +0 -290
  210. package/src/trace-helpers.ts +0 -710
  211. package/src/trace-hybrid.test.ts +0 -42
  212. package/src/trace-hybrid.ts +0 -37
  213. package/src/tracer-provider.test.ts +0 -183
  214. package/src/tracer-provider.ts +0 -266
  215. package/src/track.test.ts +0 -154
  216. package/src/track.ts +0 -216
  217. package/src/validate.test.ts +0 -287
  218. package/src/validate.ts +0 -307
  219. package/src/validation-attributes.ts +0 -43
  220. package/src/validation.test.ts +0 -330
  221. package/src/validation.ts +0 -246
  222. package/src/variable-name-inference.test.ts +0 -178
  223. package/src/variable-name-inference.ts +0 -242
  224. package/src/webhook.test.ts +0 -649
  225. package/src/webhook.ts +0 -637
  226. package/src/workflow-distributed.test.ts +0 -786
  227. package/src/workflow-distributed.ts +0 -916
  228. package/src/workflow.async-safety.integration.test.ts +0 -345
  229. package/src/workflow.test.ts +0 -647
  230. package/src/workflow.ts +0 -810
  231. package/src/yaml-config.test.ts +0 -337
  232. package/src/yaml-config.ts +0 -342
package/src/sampling.ts DELETED
@@ -1,737 +0,0 @@
1
- /**
2
- * Sampling Strategies
3
- *
4
- * Provides intelligent sampling beyond simple random rates.
5
- * Helps reduce telemetry costs while capturing critical data.
6
- *
7
- * Key strategies:
8
- * - Always trace errors and slow requests (critical for debugging)
9
- * - Sample by user ID for consistent request tracing
10
- * - Adaptive sampling based on load
11
- * - Sample by feature flags for A/B testing correlation
12
- *
13
- * @example
14
- * ```typescript
15
- * import { AlwaysOnErrorSampler, UserIdSampler } from './sampling'
16
- *
17
- * @Instrumented({
18
- * serviceName: 'user',
19
- * sampler: new AlwaysOnErrorSampler(0.1) // 10% baseline, 100% on errors
20
- * })
21
- * class UserService { }
22
- * ```
23
- */
24
-
25
- import type { Link, Attributes } from '@opentelemetry/api';
26
- import { TraceFlags } from '@opentelemetry/api';
27
- import { type Logger } from './logger';
28
-
29
- /**
30
- * Tail sampling attribute keys (autotel-internal, not OTel semconv)
31
- */
32
- export const AUTOTEL_SAMPLING_TAIL_KEEP = 'autotel.sampling.tail.keep';
33
- export const AUTOTEL_SAMPLING_TAIL_EVALUATED =
34
- 'autotel.sampling.tail.evaluated';
35
-
36
- /**
37
- * Sampler interface - return true to trace, false to skip
38
- */
39
- export interface Sampler {
40
- /**
41
- * Decide whether to trace this operation
42
- *
43
- * @param context - Sampling context
44
- * @returns true to trace, false to skip
45
- */
46
- shouldSample(context: SamplingContext): boolean;
47
-
48
- /**
49
- * Whether this sampler needs tail sampling (post-execution decision)
50
- * If true, spans are always created and shouldKeepTrace() is called after execution
51
- *
52
- * @returns true if this sampler needs to evaluate after operation completes
53
- */
54
- needsTailSampling?(): boolean;
55
-
56
- /**
57
- * Re-evaluate sampling decision after operation completes (tail sampling)
58
- * Only called if needsTailSampling() returns true
59
- *
60
- * @param context - Sampling context
61
- * @param result - Operation result
62
- * @returns true if this trace should be kept, false to drop it
63
- */
64
- shouldKeepTrace?(context: SamplingContext, result: OperationResult): boolean;
65
- }
66
-
67
- /**
68
- * Context information for sampling decisions
69
- */
70
- export interface SamplingContext {
71
- /** Operation name */
72
- operationName: string;
73
- /** Method arguments (for extracting user IDs, etc.) */
74
- args: unknown[];
75
- /** Optional metadata (e.g., feature flags, request headers) */
76
- metadata?: Record<string, unknown>;
77
- /** Optional span links for links-based sampling */
78
- links?: Link[];
79
- }
80
-
81
- /**
82
- * Result of a trace operation (for post-execution sampling)
83
- */
84
- export interface OperationResult {
85
- /** Whether the operation succeeded */
86
- success: boolean;
87
- /** Duration in milliseconds */
88
- duration: number;
89
- /** Error if operation failed */
90
- error?: Error;
91
- }
92
-
93
- /**
94
- * Simple random sampler
95
- *
96
- * @example
97
- * ```typescript
98
- * new RandomSampler(0.1) // Sample 10% of requests
99
- * ```
100
- */
101
- export class RandomSampler implements Sampler {
102
- constructor(private readonly sampleRate: number) {
103
- if (sampleRate < 0 || sampleRate > 1) {
104
- throw new Error('Sample rate must be between 0 and 1');
105
- }
106
- }
107
-
108
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
109
- shouldSample(_context: SamplingContext): boolean {
110
- return Math.random() < this.sampleRate;
111
- }
112
- }
113
-
114
- /**
115
- * Always sample (100% tracing)
116
- */
117
- export class AlwaysSampler implements Sampler {
118
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
119
- shouldSample(_context: SamplingContext): boolean {
120
- return true;
121
- }
122
- }
123
-
124
- /**
125
- * Never sample (0% tracing)
126
- */
127
- export class NeverSampler implements Sampler {
128
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
129
- shouldSample(_context: SamplingContext): boolean {
130
- return false;
131
- }
132
- }
133
-
134
- /**
135
- * Adaptive sampler that always traces errors and slow requests
136
- *
137
- * This is the recommended sampler for production use.
138
- * It ensures you never miss critical issues while keeping costs down.
139
- *
140
- * Strategy:
141
- * - Always trace errors (critical for debugging)
142
- * - Always trace slow requests (performance issues)
143
- * - Use baseline sample rate for successful fast requests
144
- *
145
- * **IMPORTANT - Tail Sampling Requirement:**
146
- * This sampler uses tail sampling (makes decisions AFTER execution).
147
- * You MUST use TailSamplingSpanProcessor for it to work correctly:
148
- *
149
- * - If using initInstrumentation(): TailSamplingSpanProcessor is auto-configured
150
- * - If using custom TracerProvider: You MUST manually register TailSamplingSpanProcessor
151
- *
152
- * Without TailSamplingSpanProcessor, ALL spans are exported (defeating the cost savings).
153
- *
154
- * @see TailSamplingSpanProcessor
155
- * @see README.md "Tail Sampling with Custom Providers" section
156
- *
157
- * @example
158
- * ```typescript
159
- * new AdaptiveSampler({
160
- * baselineSampleRate: 0.1, // 10% of normal requests
161
- * slowThresholdMs: 1000, // Requests > 1s are "slow"
162
- * alwaysSampleErrors: true, // Always trace errors
163
- * alwaysSampleSlow: true // Always trace slow requests
164
- * })
165
- * ```
166
- */
167
- export class AdaptiveSampler implements Sampler {
168
- private baselineSampleRate: number;
169
- private slowThresholdMs: number;
170
- private alwaysSampleErrors: boolean;
171
- private alwaysSampleSlow: boolean;
172
- private linksBased: boolean;
173
- private linksRate: number;
174
- private logger?: Logger;
175
-
176
- // Track whether we should sample this request
177
- private readonly samplingDecisions = new WeakMap<unknown[], boolean>();
178
- // Track operation results to enable post-execution decision
179
- private readonly operationResults = new WeakMap<unknown[], OperationResult>();
180
-
181
- constructor(
182
- options: {
183
- baselineSampleRate?: number;
184
- slowThresholdMs?: number;
185
- alwaysSampleErrors?: boolean;
186
- alwaysSampleSlow?: boolean;
187
- /** Enable links-based sampling for event-driven architectures */
188
- linksBased?: boolean;
189
- /** Sampling rate for spans linked to sampled spans (0.0-1.0) */
190
- linksRate?: number;
191
- logger?: Logger;
192
- } = {},
193
- ) {
194
- this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
195
- this.slowThresholdMs = options.slowThresholdMs ?? 1000;
196
- this.alwaysSampleErrors = options.alwaysSampleErrors ?? true;
197
- this.alwaysSampleSlow = options.alwaysSampleSlow ?? true;
198
- this.linksBased = options.linksBased ?? false;
199
- this.linksRate = options.linksRate ?? 1;
200
- this.logger = options.logger;
201
-
202
- if (this.baselineSampleRate < 0 || this.baselineSampleRate > 1) {
203
- throw new Error('Baseline sample rate must be between 0 and 1');
204
- }
205
- if (this.linksRate < 0 || this.linksRate > 1) {
206
- throw new Error('Links rate must be between 0 and 1');
207
- }
208
- }
209
-
210
- needsTailSampling(): boolean {
211
- // AdaptiveSampler ALWAYS needs tail sampling to implement error/slow capture
212
- return true;
213
- }
214
-
215
- shouldSample(context: SamplingContext): boolean {
216
- // For tail sampling, we optimistically create spans for all requests
217
- // The real decision happens in shouldKeepTrace() after execution
218
- // We still store the baseline decision for shouldKeepTrace() to use
219
- const baselineDecision = Math.random() < this.baselineSampleRate;
220
- this.samplingDecisions.set(context.args, baselineDecision);
221
-
222
- // Always return true to create the span (tail sampling will decide if we keep it)
223
- return true;
224
- }
225
-
226
- /**
227
- * Check if any links point to sampled spans.
228
- *
229
- * A span is considered linked to a sampled span if any of its links
230
- * have trace_flags with the sampled bit set (0x01).
231
- *
232
- * @param links - Array of span links to check
233
- * @returns true if any linked span is sampled, false otherwise
234
- */
235
- hasSampledLink(links: Link[]): boolean {
236
- if (!links || links.length === 0) {
237
- return false;
238
- }
239
- return links.some(
240
- (link) =>
241
- link.context && (link.context.traceFlags & TraceFlags.SAMPLED) !== 0,
242
- );
243
- }
244
-
245
- /**
246
- * Re-evaluate sampling decision after operation completes
247
- *
248
- * This allows us to always capture errors and slow requests,
249
- * even if they weren't initially sampled.
250
- *
251
- * @param context - Sampling context
252
- * @param result - Operation result
253
- * @returns true if this operation should be kept (not discarded)
254
- */
255
- shouldKeepTrace(context: SamplingContext, result: OperationResult): boolean {
256
- const baselineDecision = this.samplingDecisions.get(context.args) ?? false;
257
-
258
- // Always keep errors
259
- if (this.alwaysSampleErrors && !result.success) {
260
- if (!baselineDecision) {
261
- this.logger?.debug(
262
- {
263
- operation: context.operationName,
264
- error: result.error?.message,
265
- },
266
- 'Adaptive sampling: Keeping error trace',
267
- );
268
- }
269
- return true;
270
- }
271
-
272
- // Always keep slow requests
273
- if (this.alwaysSampleSlow && result.duration >= this.slowThresholdMs) {
274
- if (!baselineDecision) {
275
- this.logger?.debug(
276
- {
277
- operation: context.operationName,
278
- duration: result.duration,
279
- },
280
- 'Adaptive sampling: Keeping slow trace',
281
- );
282
- }
283
- return true;
284
- }
285
-
286
- // Check for sampled links (links-based sampling for event-driven systems)
287
- if (
288
- this.linksBased &&
289
- context.links &&
290
- this.hasSampledLink(context.links)
291
- ) {
292
- // Use linksRate to decide whether to keep the linked span
293
- const keepLinked = Math.random() < this.linksRate;
294
- if (keepLinked && !baselineDecision) {
295
- this.logger?.debug(
296
- {
297
- operation: context.operationName,
298
- linkCount: context.links.length,
299
- },
300
- 'Adaptive sampling: Keeping trace due to sampled link',
301
- );
302
- }
303
- return keepLinked;
304
- }
305
-
306
- // Otherwise, use baseline decision
307
- return baselineDecision;
308
- }
309
- }
310
-
311
- /**
312
- * User-based sampler for consistent tracing
313
- *
314
- * Always samples requests from specific user IDs.
315
- * Useful for debugging specific user issues or monitoring VIP users.
316
- *
317
- * @example
318
- * ```typescript
319
- * new UserIdSampler({
320
- * baselineSampleRate: 0.01, // 1% of normal users
321
- * alwaysSampleUsers: ['vip_123'], // Always trace VIP users
322
- * extractUserId: (args) => args[0]?.userId // Extract user ID from first arg
323
- * })
324
- * ```
325
- */
326
- export class UserIdSampler implements Sampler {
327
- private baselineSampleRate: number;
328
- private alwaysSampleUsers: Set<string>;
329
- private extractUserId: (args: unknown[]) => string | undefined;
330
- private logger?: Logger;
331
-
332
- constructor(options: {
333
- baselineSampleRate?: number;
334
- alwaysSampleUsers?: string[];
335
- extractUserId: (args: unknown[]) => string | undefined;
336
- logger?: Logger;
337
- }) {
338
- this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
339
- this.alwaysSampleUsers = new Set(options.alwaysSampleUsers || []);
340
- this.extractUserId = options.extractUserId;
341
- this.logger = options.logger;
342
- }
343
-
344
- shouldSample(context: SamplingContext): boolean {
345
- const userId = this.extractUserId(context.args);
346
-
347
- // Always sample specific users
348
- if (userId && this.alwaysSampleUsers.has(userId)) {
349
- this.logger?.debug(
350
- {
351
- operation: context.operationName,
352
- userId,
353
- },
354
- 'Sampling user request',
355
- );
356
- return true;
357
- }
358
-
359
- // For consistent per-user sampling, hash the user ID
360
- if (userId) {
361
- const hash = this.hashString(userId);
362
- return hash < this.baselineSampleRate;
363
- }
364
-
365
- // Fallback to random sampling if no user ID
366
- return Math.random() < this.baselineSampleRate;
367
- }
368
-
369
- /**
370
- * Add user IDs to always-sample list
371
- */
372
- addAlwaysSampleUsers(...userIds: string[]): void {
373
- for (const userId of userIds) {
374
- this.alwaysSampleUsers.add(userId);
375
- }
376
- }
377
-
378
- /**
379
- * Remove user IDs from always-sample list
380
- */
381
- removeAlwaysSampleUsers(...userIds: string[]): void {
382
- for (const userId of userIds) {
383
- this.alwaysSampleUsers.delete(userId);
384
- }
385
- }
386
-
387
- /**
388
- * Simple hash function for consistent user sampling
389
- */
390
- private hashString(str: string): number {
391
- let hash = 0;
392
- for (let i = 0; i < str.length; i++) {
393
- const char = str.codePointAt(i) ?? 0;
394
- hash = (hash << 5) - hash + char;
395
- hash = hash & hash; // Convert to 32-bit integer
396
- }
397
- return Math.abs(hash) / 2_147_483_647; // Normalize to 0-1
398
- }
399
- }
400
-
401
- /**
402
- * Composite sampler that combines multiple samplers
403
- *
404
- * Samples if ANY of the child samplers returns true.
405
- *
406
- * @example
407
- * ```typescript
408
- * new CompositeSampler([
409
- * new UserIdSampler({ extractUserId: (args) => args[0]?.userId }),
410
- * new AdaptiveSampler({ baselineSampleRate: 0.1 })
411
- * ])
412
- * ```
413
- */
414
- export class CompositeSampler implements Sampler {
415
- constructor(private readonly samplers: Sampler[]) {
416
- if (samplers.length === 0) {
417
- throw new Error('CompositeSampler requires at least one child sampler');
418
- }
419
- }
420
-
421
- shouldSample(context: SamplingContext): boolean {
422
- return this.samplers.some((sampler) => sampler.shouldSample(context));
423
- }
424
- }
425
-
426
- /**
427
- * Feature flag sampler
428
- *
429
- * Always samples requests with specific feature flags enabled.
430
- * Perfect for correlating A/B test experiments with metrics.
431
- *
432
- * @example
433
- * ```typescript
434
- * new FeatureFlagSampler({
435
- * baselineSampleRate: 0.01,
436
- * alwaysSampleFlags: ['new_checkout', 'experimental_ui'],
437
- * extractFlags: (args, metadata) => metadata?.featureFlags
438
- * })
439
- * ```
440
- */
441
- export class FeatureFlagSampler implements Sampler {
442
- private baselineSampleRate: number;
443
- private alwaysSampleFlags: Set<string>;
444
- private extractFlags: (
445
- args: unknown[],
446
- metadata?: Record<string, unknown>,
447
- ) => string[] | undefined;
448
- private logger?: Logger;
449
-
450
- constructor(options: {
451
- baselineSampleRate?: number;
452
- alwaysSampleFlags?: string[];
453
- extractFlags: (
454
- args: unknown[],
455
- metadata?: Record<string, unknown>,
456
- ) => string[] | undefined;
457
- logger?: Logger;
458
- }) {
459
- this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
460
- this.alwaysSampleFlags = new Set(options.alwaysSampleFlags || []);
461
- this.extractFlags = options.extractFlags;
462
- this.logger = options.logger;
463
- }
464
-
465
- shouldSample(context: SamplingContext): boolean {
466
- const flags = this.extractFlags(context.args, context.metadata);
467
-
468
- // Always sample if any monitored flag is enabled
469
- if (flags && flags.some((flag) => this.alwaysSampleFlags.has(flag))) {
470
- this.logger?.debug(
471
- {
472
- operation: context.operationName,
473
- flags,
474
- },
475
- 'Sampling feature flag request',
476
- );
477
- return true;
478
- }
479
-
480
- // Fallback to random sampling
481
- return Math.random() < this.baselineSampleRate;
482
- }
483
-
484
- /**
485
- * Add feature flags to always-sample list
486
- */
487
- addAlwaysSampleFlags(...flags: string[]): void {
488
- for (const flag of flags) {
489
- this.alwaysSampleFlags.add(flag);
490
- }
491
- }
492
-
493
- /**
494
- * Remove feature flags from always-sample list
495
- */
496
- removeAlwaysSampleFlags(...flags: string[]): void {
497
- for (const flag of flags) {
498
- this.alwaysSampleFlags.delete(flag);
499
- }
500
- }
501
- }
502
-
503
- // ============================================================================
504
- // Sampling Presets
505
- // ============================================================================
506
-
507
- /**
508
- * Named sampling presets for common environments.
509
- * Use with `init({ sampling: 'production' })` or directly via factories.
510
- */
511
- export type SamplingPreset =
512
- | 'development'
513
- | 'errors-only'
514
- | 'production'
515
- | 'off';
516
-
517
- /**
518
- * Sampling preset factories.
519
- *
520
- * For most users, the string shorthand on `init()` is simpler:
521
- * ```typescript
522
- * init({ service: 'my-app', sampling: 'production' })
523
- * ```
524
- *
525
- * Use factories when you need to customize:
526
- * ```typescript
527
- * init({ service: 'my-app', sampler: samplingPresets.production({ baselineSampleRate: 0.05 }) })
528
- * ```
529
- */
530
- export const samplingPresets = {
531
- /** Capture everything — best for local development and debugging */
532
- development: () => new AlwaysSampler(),
533
-
534
- /** Only bad outcomes — zero baseline, errors always kept */
535
- errorsOnly: () =>
536
- new AdaptiveSampler({
537
- baselineSampleRate: 0,
538
- alwaysSampleErrors: true,
539
- }),
540
-
541
- /**
542
- * Balanced production defaults — 10% baseline + errors + slow traces.
543
- * Pass overrides to tune (uses the same option names as AdaptiveSampler).
544
- */
545
- production: (overrides?: {
546
- baselineSampleRate?: number;
547
- slowThresholdMs?: number;
548
- alwaysSampleErrors?: boolean;
549
- alwaysSampleSlow?: boolean;
550
- }) =>
551
- new AdaptiveSampler({
552
- baselineSampleRate: 0.1,
553
- alwaysSampleErrors: true,
554
- alwaysSampleSlow: true,
555
- slowThresholdMs: 1000,
556
- ...overrides,
557
- }),
558
-
559
- /** Disable sampling entirely */
560
- off: () => new NeverSampler(),
561
- };
562
-
563
- /**
564
- * Resolve a preset string to a Sampler instance.
565
- * Used internally by `init()` when `sampling` string is provided.
566
- *
567
- * @throws Error if preset is not recognized
568
- */
569
- export function resolveSamplingPreset(preset: SamplingPreset): Sampler {
570
- switch (preset) {
571
- case 'development':
572
- return samplingPresets.development();
573
- case 'errors-only':
574
- return samplingPresets.errorsOnly();
575
- case 'production':
576
- return samplingPresets.production();
577
- case 'off':
578
- return samplingPresets.off();
579
- default:
580
- throw new Error(
581
- `Unknown sampling preset: "${preset}". Valid presets: development, errors-only, production, off`,
582
- );
583
- }
584
- }
585
-
586
- // ============================================================================
587
- // Link Helper Functions
588
- // ============================================================================
589
-
590
- /**
591
- * Create a Link from W3C trace context headers (e.g., from a message queue).
592
- *
593
- * This is useful for message consumers that need to link to the producer span.
594
- * The headers should contain at least a `traceparent` header in W3C format.
595
- *
596
- * @param headers - Dictionary containing traceparent/tracestate headers
597
- * @param attributes - Optional attributes for the link
598
- * @returns Link object if context could be extracted, null otherwise
599
- *
600
- * @example
601
- * ```typescript
602
- * // In a Kafka consumer
603
- * const headers = { traceparent: '00-abc123...-def456...-01' };
604
- * const link = createLinkFromHeaders(headers);
605
- * if (link) {
606
- * // Use with tracer.startActiveSpan options or ctx.addLink()
607
- * tracer.startActiveSpan('process.message', { links: [link] }, span => { ... });
608
- * }
609
- * ```
610
- */
611
- export function createLinkFromHeaders(
612
- headers: Record<string, string>,
613
- attributes?: Attributes,
614
- ): Link | null {
615
- // Parse W3C traceparent header directly for reliability
616
- // Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)
617
- const traceparent = headers.traceparent || headers['traceparent'];
618
- if (!traceparent) {
619
- return null;
620
- }
621
-
622
- const spanContext = parseTraceparent(traceparent);
623
- if (!spanContext || !isValidSpanContext(spanContext)) {
624
- return null;
625
- }
626
-
627
- return {
628
- context: spanContext,
629
- attributes: attributes ?? {},
630
- };
631
- }
632
-
633
- /**
634
- * Extract Links from a batch of messages for fan-in scenarios.
635
- *
636
- * Useful for batch processing where multiple producer spans should be linked.
637
- * This enables tracing causality in event-driven architectures where a single
638
- * consumer processes messages from multiple producers.
639
- *
640
- * @param messages - List of message objects
641
- * @param headersKey - Key in each message containing trace headers (default: 'headers')
642
- * @returns List of Link objects for all valid trace contexts
643
- *
644
- * @example
645
- * ```typescript
646
- * // Processing a batch of SQS/Kafka messages
647
- * const messages = [
648
- * { body: '...', headers: { traceparent: '...' } },
649
- * { body: '...', headers: { traceparent: '...' } },
650
- * ];
651
- * const links = extractLinksFromBatch(messages);
652
- *
653
- * tracer.startActiveSpan('process.batch', { links }, span => {
654
- * for (const msg of messages) {
655
- * processMessage(msg);
656
- * }
657
- * });
658
- * ```
659
- */
660
- export function extractLinksFromBatch(
661
- messages: Array<{ [key: string]: unknown }>,
662
- headersKey: string = 'headers',
663
- ): Link[] {
664
- const links: Link[] = [];
665
-
666
- for (const msg of messages) {
667
- const msgHeaders = msg[headersKey];
668
- if (msgHeaders && typeof msgHeaders === 'object' && msgHeaders !== null) {
669
- const link = createLinkFromHeaders(msgHeaders as Record<string, string>, {
670
- 'messaging.batch.message_index': links.length,
671
- });
672
- if (link) {
673
- links.push(link);
674
- }
675
- }
676
- }
677
-
678
- return links;
679
- }
680
-
681
- /**
682
- * Parse W3C traceparent header into SpanContext
683
- * Format: version-traceId-spanId-traceFlags (e.g., 00-abc123...-def456...-01)
684
- *
685
- * @see https://www.w3.org/TR/trace-context/#traceparent-header
686
- */
687
- function parseTraceparent(
688
- traceparent: string,
689
- ): import('@opentelemetry/api').SpanContext | null {
690
- // W3C traceparent format: version-traceId-parentId-traceFlags
691
- // Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
692
- const TRACEPARENT_REGEX =
693
- /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
694
-
695
- const match = traceparent.match(TRACEPARENT_REGEX);
696
- if (!match || match.length < 5) {
697
- return null;
698
- }
699
-
700
- const version = match[1];
701
- const traceId = match[2];
702
- const spanId = match[3];
703
- const flags = match[4];
704
-
705
- // Validate all parts are present (TypeScript narrowing)
706
- if (!version || !traceId || !spanId || !flags) {
707
- return null;
708
- }
709
-
710
- // Version 00 is currently the only version, but we should be forward compatible
711
- if (version === 'ff') {
712
- // Version ff is invalid according to spec
713
- return null;
714
- }
715
-
716
- return {
717
- traceId,
718
- spanId,
719
- traceFlags: Number.parseInt(flags, 16),
720
- isRemote: true,
721
- };
722
- }
723
-
724
- /**
725
- * Check if a SpanContext is valid (has non-zero trace and span IDs)
726
- */
727
- function isValidSpanContext(
728
- spanContext: import('@opentelemetry/api').SpanContext | null,
729
- ): spanContext is import('@opentelemetry/api').SpanContext {
730
- if (!spanContext) return false;
731
- // TraceId should not be all zeros (00000000000000000000000000000000)
732
- // SpanId should not be all zeros (0000000000000000)
733
- return (
734
- spanContext.traceId !== '00000000000000000000000000000000' &&
735
- spanContext.spanId !== '0000000000000000'
736
- );
737
- }