autotel 2.25.2 → 2.25.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 (113) hide show
  1. package/README.md +152 -1
  2. package/dist/attributes.d.cts +2 -2
  3. package/dist/attributes.d.ts +2 -2
  4. package/dist/auto.cjs +6 -6
  5. package/dist/auto.js +4 -4
  6. package/dist/{chunk-QUW4I2OI.js → chunk-3ZLYWPMY.js} +3 -3
  7. package/dist/{chunk-QUW4I2OI.js.map → chunk-3ZLYWPMY.js.map} +1 -1
  8. package/dist/{chunk-BGVKKL2N.cjs → chunk-7FIGORWI.cjs} +46 -2
  9. package/dist/chunk-7FIGORWI.cjs.map +1 -0
  10. package/dist/{chunk-MNMLCLHH.cjs → chunk-AQXPGGQK.cjs} +5 -5
  11. package/dist/{chunk-MNMLCLHH.cjs.map → chunk-AQXPGGQK.cjs.map} +1 -1
  12. package/dist/{chunk-4KGC5N3J.cjs → chunk-FXKB7EPR.cjs} +13 -13
  13. package/dist/{chunk-4KGC5N3J.cjs.map → chunk-FXKB7EPR.cjs.map} +1 -1
  14. package/dist/{chunk-4SCBD22Z.js → chunk-IFKDBL65.js} +3 -3
  15. package/dist/{chunk-4SCBD22Z.js.map → chunk-IFKDBL65.js.map} +1 -1
  16. package/dist/{chunk-JVICEM6W.cjs → chunk-ITYASFHQ.cjs} +91 -19
  17. package/dist/chunk-ITYASFHQ.cjs.map +1 -0
  18. package/dist/{chunk-IKRHEUS7.js → chunk-JM63D22M.js} +10 -10
  19. package/dist/{chunk-IKRHEUS7.js.map → chunk-JM63D22M.js.map} +1 -1
  20. package/dist/{chunk-GVLK7YUU.cjs → chunk-KZEC4CHV.cjs} +6 -4
  21. package/dist/chunk-KZEC4CHV.cjs.map +1 -0
  22. package/dist/{chunk-VXLEJWLY.js → chunk-MNBAXRVG.js} +89 -17
  23. package/dist/chunk-MNBAXRVG.js.map +1 -0
  24. package/dist/{chunk-6YIDHH2S.cjs → chunk-OFPZULMQ.cjs} +32 -10
  25. package/dist/chunk-OFPZULMQ.cjs.map +1 -0
  26. package/dist/{chunk-UKUYBUFQ.cjs → chunk-OWXXS4JB.cjs} +7 -7
  27. package/dist/{chunk-UKUYBUFQ.cjs.map → chunk-OWXXS4JB.cjs.map} +1 -1
  28. package/dist/{chunk-77PLEJ54.js → chunk-PKXD2RMI.js} +4 -4
  29. package/dist/{chunk-77PLEJ54.js.map → chunk-PKXD2RMI.js.map} +1 -1
  30. package/dist/{chunk-RWOVNF3V.cjs → chunk-RMGSBMQF.cjs} +36 -36
  31. package/dist/chunk-RMGSBMQF.cjs.map +1 -0
  32. package/dist/{chunk-YTGF4L2C.js → chunk-RUD7KS4R.js} +27 -5
  33. package/dist/chunk-RUD7KS4R.js.map +1 -0
  34. package/dist/{chunk-KUSYIHW7.js → chunk-VVYSQXQL.js} +3 -3
  35. package/dist/{chunk-KUSYIHW7.js.map → chunk-VVYSQXQL.js.map} +1 -1
  36. package/dist/{chunk-XND7WBVX.js → chunk-VYA6QDNA.js} +43 -3
  37. package/dist/chunk-VYA6QDNA.js.map +1 -0
  38. package/dist/{chunk-L627YSSP.cjs → chunk-WOWTZ4EB.cjs} +9 -9
  39. package/dist/{chunk-L627YSSP.cjs.map → chunk-WOWTZ4EB.cjs.map} +1 -1
  40. package/dist/{chunk-X4RMFFMR.js → chunk-XDKK53OL.js} +6 -4
  41. package/dist/chunk-XDKK53OL.js.map +1 -0
  42. package/dist/decorators.cjs +5 -5
  43. package/dist/decorators.js +5 -5
  44. package/dist/event.cjs +8 -8
  45. package/dist/event.js +5 -5
  46. package/dist/functional.cjs +12 -12
  47. package/dist/functional.js +5 -5
  48. package/dist/index.cjs +71 -51
  49. package/dist/index.d.cts +4 -4
  50. package/dist/index.d.ts +4 -4
  51. package/dist/index.js +13 -13
  52. package/dist/{init-Q4uIQKbq.d.cts → init-CIzpC5kZ.d.cts} +9 -2
  53. package/dist/{init-ls4xSZe5.d.ts → init-C_PiC_Su.d.ts} +9 -2
  54. package/dist/instrumentation.cjs +12 -12
  55. package/dist/instrumentation.cjs.map +1 -1
  56. package/dist/instrumentation.js +4 -4
  57. package/dist/instrumentation.js.map +1 -1
  58. package/dist/messaging-adapters.d.cts +1 -1
  59. package/dist/messaging-adapters.d.ts +1 -1
  60. package/dist/messaging.cjs +9 -9
  61. package/dist/messaging.d.cts +1 -1
  62. package/dist/messaging.d.ts +1 -1
  63. package/dist/messaging.js +6 -6
  64. package/dist/metric-helpers.d.cts +1 -1
  65. package/dist/metric-helpers.d.ts +1 -1
  66. package/dist/sampling.cjs +26 -10
  67. package/dist/sampling.d.cts +49 -1
  68. package/dist/sampling.d.ts +49 -1
  69. package/dist/sampling.js +1 -1
  70. package/dist/semantic-helpers.cjs +10 -10
  71. package/dist/semantic-helpers.js +6 -6
  72. package/dist/tail-sampling-processor.cjs +6 -2
  73. package/dist/tail-sampling-processor.d.cts +2 -2
  74. package/dist/tail-sampling-processor.d.ts +2 -2
  75. package/dist/tail-sampling-processor.js +5 -1
  76. package/dist/trace-helpers.d.cts +1 -1
  77. package/dist/trace-helpers.d.ts +1 -1
  78. package/dist/{utils-D1trOLNm.d.ts → utils-Buel3cj0.d.ts} +1 -1
  79. package/dist/{utils-DuNJfXSH.d.cts → utils-CbUkl8r1.d.cts} +1 -1
  80. package/dist/webhook.cjs +6 -6
  81. package/dist/webhook.js +5 -5
  82. package/dist/workflow-distributed.cjs +7 -7
  83. package/dist/workflow-distributed.js +5 -5
  84. package/dist/workflow.cjs +10 -10
  85. package/dist/workflow.js +6 -6
  86. package/dist/yaml-config.cjs +5 -5
  87. package/dist/yaml-config.d.cts +5 -4
  88. package/dist/yaml-config.d.ts +5 -4
  89. package/dist/yaml-config.js +2 -2
  90. package/package.json +1 -1
  91. package/src/env-config.test.ts +77 -0
  92. package/src/env-config.ts +106 -0
  93. package/src/functional.ts +13 -7
  94. package/src/index.ts +6 -0
  95. package/src/init.customization.test.ts +61 -0
  96. package/src/init.integrations.test.ts +6 -1
  97. package/src/init.ts +23 -13
  98. package/src/instrumentation.ts +1 -1
  99. package/src/sampling.test.ts +96 -3
  100. package/src/sampling.ts +90 -0
  101. package/src/tail-sampling-processor.test.ts +26 -22
  102. package/src/tail-sampling-processor.ts +8 -4
  103. package/src/yaml-config.test.ts +71 -0
  104. package/src/yaml-config.ts +32 -2
  105. package/dist/chunk-6YIDHH2S.cjs.map +0 -1
  106. package/dist/chunk-BGVKKL2N.cjs.map +0 -1
  107. package/dist/chunk-GVLK7YUU.cjs.map +0 -1
  108. package/dist/chunk-JVICEM6W.cjs.map +0 -1
  109. package/dist/chunk-RWOVNF3V.cjs.map +0 -1
  110. package/dist/chunk-VXLEJWLY.js.map +0 -1
  111. package/dist/chunk-X4RMFFMR.js.map +0 -1
  112. package/dist/chunk-XND7WBVX.js.map +0 -1
  113. package/dist/chunk-YTGF4L2C.js.map +0 -1
package/src/env-config.ts CHANGED
@@ -1,12 +1,22 @@
1
1
  /**
2
2
  * Standard OpenTelemetry environment variables
3
3
  */
4
+ import type { Sampler as OtelSampler } from '@opentelemetry/sdk-trace-base';
5
+ import {
6
+ AlwaysOffSampler,
7
+ AlwaysOnSampler,
8
+ ParentBasedSampler,
9
+ TraceIdRatioBasedSampler,
10
+ } from '@opentelemetry/sdk-trace-base';
11
+
4
12
  export interface OtelEnvVars {
5
13
  OTEL_SERVICE_NAME?: string;
6
14
  OTEL_EXPORTER_OTLP_ENDPOINT?: string;
7
15
  OTEL_EXPORTER_OTLP_HEADERS?: string;
8
16
  OTEL_RESOURCE_ATTRIBUTES?: string;
9
17
  OTEL_EXPORTER_OTLP_PROTOCOL?: 'http' | 'grpc';
18
+ OTEL_TRACES_SAMPLER?: string;
19
+ OTEL_TRACES_SAMPLER_ARG?: string;
10
20
  }
11
21
 
12
22
  /**
@@ -33,6 +43,7 @@ export interface EnvConfig {
33
43
  protocol?: 'http' | 'grpc';
34
44
  headers?: Record<string, string>;
35
45
  resourceAttributes?: Record<string, string>;
46
+ otelSampler?: OtelSampler;
36
47
  }
37
48
 
38
49
  /**
@@ -93,9 +104,99 @@ export function resolveOtelEnv(): OtelEnvVars {
93
104
  }
94
105
  }
95
106
 
107
+ if (process.env.OTEL_TRACES_SAMPLER) {
108
+ const value = process.env.OTEL_TRACES_SAMPLER.trim();
109
+ if (value) {
110
+ env.OTEL_TRACES_SAMPLER = value;
111
+ }
112
+ }
113
+
114
+ if (process.env.OTEL_TRACES_SAMPLER_ARG) {
115
+ const value = process.env.OTEL_TRACES_SAMPLER_ARG.trim();
116
+ if (value) {
117
+ env.OTEL_TRACES_SAMPLER_ARG = value;
118
+ }
119
+ }
120
+
96
121
  return env;
97
122
  }
98
123
 
124
+ function parseRatioSamplerArg(
125
+ samplerName: string,
126
+ samplerArg: string | undefined,
127
+ ): number {
128
+ if (samplerArg === undefined) {
129
+ return 1.0;
130
+ }
131
+
132
+ const ratio = Number(samplerArg);
133
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
134
+ console.error(
135
+ `[autotel] Invalid OTEL_TRACES_SAMPLER_ARG="${samplerArg}" for ${samplerName}. Expected a number in [0..1]. Falling back to 1.0.`,
136
+ );
137
+ return 1.0;
138
+ }
139
+
140
+ return ratio;
141
+ }
142
+
143
+ function warnOnUnusedSamplerArg(
144
+ samplerName: string,
145
+ samplerArg: string | undefined,
146
+ ): void {
147
+ if (samplerArg !== undefined) {
148
+ console.error(
149
+ `[autotel] OTEL_TRACES_SAMPLER_ARG is not used by OTEL_TRACES_SAMPLER="${samplerName}". Ignoring value "${samplerArg}".`,
150
+ );
151
+ }
152
+ }
153
+
154
+ export function createSamplerFromEnv(
155
+ env: Pick<OtelEnvVars, 'OTEL_TRACES_SAMPLER' | 'OTEL_TRACES_SAMPLER_ARG'>,
156
+ ): OtelSampler | undefined {
157
+ const samplerName = env.OTEL_TRACES_SAMPLER;
158
+ if (!samplerName) {
159
+ return undefined;
160
+ }
161
+
162
+ switch (samplerName) {
163
+ case 'always_on':
164
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
165
+ return new AlwaysOnSampler();
166
+ case 'always_off':
167
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
168
+ return new AlwaysOffSampler();
169
+ case 'traceidratio':
170
+ return new TraceIdRatioBasedSampler(
171
+ parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG),
172
+ );
173
+ case 'parentbased_always_on':
174
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
175
+ return new ParentBasedSampler({ root: new AlwaysOnSampler() });
176
+ case 'parentbased_always_off':
177
+ warnOnUnusedSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG);
178
+ return new ParentBasedSampler({ root: new AlwaysOffSampler() });
179
+ case 'parentbased_traceidratio':
180
+ return new ParentBasedSampler({
181
+ root: new TraceIdRatioBasedSampler(
182
+ parseRatioSamplerArg(samplerName, env.OTEL_TRACES_SAMPLER_ARG),
183
+ ),
184
+ });
185
+ case 'jaeger_remote':
186
+ case 'parentbased_jaeger_remote':
187
+ case 'xray':
188
+ console.error(
189
+ `[autotel] OTEL_TRACES_SAMPLER="${samplerName}" is not supported yet by autotel. Falling back to the next sampler source.`,
190
+ );
191
+ return undefined;
192
+ default:
193
+ console.error(
194
+ `[autotel] Unknown OTEL_TRACES_SAMPLER="${samplerName}". Falling back to the next sampler source.`,
195
+ );
196
+ return undefined;
197
+ }
198
+ }
199
+
99
200
  /**
100
201
  * Parse OTEL_RESOURCE_ATTRIBUTES from comma-separated key=value pairs
101
202
  * Example: "service.version=1.0.0,deployment.environment=production"
@@ -191,6 +292,11 @@ export function envToConfig(env: OtelEnvVars): EnvConfig {
191
292
  config.resourceAttributes = resourceAttrs;
192
293
  }
193
294
 
295
+ const sampler = createSamplerFromEnv(env);
296
+ if (sampler) {
297
+ config.otelSampler = sampler;
298
+ }
299
+
194
300
  return config;
195
301
  }
196
302
 
package/src/functional.ts CHANGED
@@ -42,7 +42,13 @@ import {
42
42
  } from '@opentelemetry/api';
43
43
  import { getConfig } from './config';
44
44
  import { getConfig as getInitConfig, getSdk } from './init';
45
- import { type Sampler, type SamplingContext, AlwaysSampler } from './sampling';
45
+ import {
46
+ type Sampler,
47
+ type SamplingContext,
48
+ AlwaysSampler,
49
+ AUTOTEL_SAMPLING_TAIL_KEEP,
50
+ AUTOTEL_SAMPLING_TAIL_EVALUATED,
51
+ } from './sampling';
46
52
  import { getEventQueue } from './track';
47
53
  import type { TraceContext } from './trace-context';
48
54
  import {
@@ -797,8 +803,8 @@ function wrapWithTracing<TArgs extends unknown[], TReturn>(
797
803
  duration,
798
804
  error,
799
805
  });
800
- span.setAttribute('sampling.tail.keep', shouldKeepSpan);
801
- span.setAttribute('sampling.tail.evaluated', true);
806
+ span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
807
+ span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
802
808
  }
803
809
  };
804
810
 
@@ -1104,8 +1110,8 @@ function wrapWithTracingSync<TArgs extends unknown[], TReturn>(
1104
1110
  duration,
1105
1111
  error,
1106
1112
  });
1107
- span.setAttribute('sampling.tail.keep', shouldKeepSpan);
1108
- span.setAttribute('sampling.tail.evaluated', true);
1113
+ span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
1114
+ span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
1109
1115
  }
1110
1116
  };
1111
1117
 
@@ -1352,8 +1358,8 @@ function executeImmediately<TReturn = unknown>(
1352
1358
  duration,
1353
1359
  error,
1354
1360
  });
1355
- span.setAttribute('sampling.tail.keep', shouldKeepSpan);
1356
- span.setAttribute('sampling.tail.evaluated', true);
1361
+ span.setAttribute(AUTOTEL_SAMPLING_TAIL_KEEP, shouldKeepSpan);
1362
+ span.setAttribute(AUTOTEL_SAMPLING_TAIL_EVALUATED, true);
1357
1363
  }
1358
1364
  };
1359
1365
 
package/src/index.ts CHANGED
@@ -60,6 +60,7 @@ export {
60
60
  AttributeRedactingProcessor,
61
61
  REDACTOR_PATTERNS,
62
62
  REDACTOR_PRESETS,
63
+ createAttributeRedactor,
63
64
  createRedactedSpan,
64
65
  type AttributeRedactorFn,
65
66
  type AttributeRedactorPreset,
@@ -151,6 +152,7 @@ export { formatDuration } from './pretty-log-formatter';
151
152
  export {
152
153
  type Sampler,
153
154
  type SamplingContext,
155
+ type SamplingPreset,
154
156
  AlwaysSampler,
155
157
  NeverSampler,
156
158
  RandomSampler,
@@ -158,6 +160,10 @@ export {
158
160
  UserIdSampler,
159
161
  createLinkFromHeaders,
160
162
  extractLinksFromBatch,
163
+ samplingPresets,
164
+ resolveSamplingPreset,
165
+ AUTOTEL_SAMPLING_TAIL_KEEP,
166
+ AUTOTEL_SAMPLING_TAIL_EVALUATED,
161
167
  } from './sampling';
162
168
 
163
169
  // Events API
@@ -3,6 +3,7 @@ import type { MetricReader } from '@opentelemetry/sdk-metrics';
3
3
  import type { NodeSDK } from '@opentelemetry/sdk-node';
4
4
  import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
5
5
  import { mock, mockDeep, type DeepMockProxy } from 'vitest-mock-extended';
6
+ import { AlwaysSampler, NeverSampler } from './sampling';
6
7
 
7
8
  type SdkRecord = {
8
9
  options: Record<string, unknown>;
@@ -112,6 +113,7 @@ async function loadInitWithMocks() {
112
113
  return {
113
114
  init: mod.init,
114
115
  getConfig: mod.getConfig,
116
+ getDefaultSampler: mod.getDefaultSampler,
115
117
  resolveLogsFlag: mod.resolveLogsFlag,
116
118
  sdkInstances,
117
119
  traceExporterOptions,
@@ -128,6 +130,8 @@ describe('init() customization', () => {
128
130
  delete process.env.AUTOTEL_METRICS;
129
131
  delete process.env.AUTOTEL_LOGS;
130
132
  delete process.env.OTEL_LOGS_EXPORTER;
133
+ delete process.env.OTEL_TRACES_SAMPLER;
134
+ delete process.env.OTEL_TRACES_SAMPLER_ARG;
131
135
  delete process.env.NODE_ENV;
132
136
  });
133
137
 
@@ -227,6 +231,63 @@ describe('init() customization', () => {
227
231
  });
228
232
  });
229
233
 
234
+ it('resolves sampling preset shorthand to a sampler instance', async () => {
235
+ const { init, getDefaultSampler } = await loadInitWithMocks();
236
+
237
+ init({
238
+ service: 'sampling-preset-app',
239
+ sampling: 'development',
240
+ });
241
+
242
+ const sampler = getDefaultSampler();
243
+ expect(sampler.constructor.name).toBe('AlwaysSampler');
244
+ expect(sampler.shouldSample({ operationName: 'test', args: [] })).toBe(
245
+ true,
246
+ );
247
+ });
248
+
249
+ it('prefers explicit sampler over sampling preset shorthand', async () => {
250
+ const { init, getDefaultSampler } = await loadInitWithMocks();
251
+ const explicitSampler = new NeverSampler();
252
+
253
+ init({
254
+ service: 'sampling-precedence-app',
255
+ sampler: explicitSampler,
256
+ sampling: 'development',
257
+ });
258
+
259
+ expect(getDefaultSampler()).toBe(explicitSampler);
260
+ });
261
+
262
+ it('uses OTEL_TRACES_SAMPLER when no explicit sampling config is provided', async () => {
263
+ process.env.OTEL_TRACES_SAMPLER = 'always_off';
264
+ const { init, sdkInstances } = await loadInitWithMocks();
265
+
266
+ init({
267
+ service: 'env-sampler-app',
268
+ });
269
+
270
+ const options = sdkInstances.at(-1)?.options as Record<string, unknown>;
271
+ expect((options.sampler as { toString(): string }).toString()).toContain(
272
+ 'AlwaysOffSampler',
273
+ );
274
+ });
275
+
276
+ it('prefers explicit sampling config over OTEL_TRACES_SAMPLER', async () => {
277
+ process.env.OTEL_TRACES_SAMPLER = 'always_off';
278
+ const { init, sdkInstances } = await loadInitWithMocks();
279
+
280
+ init({
281
+ service: 'explicit-over-env-sampler-app',
282
+ sampling: 'development',
283
+ });
284
+
285
+ const options = sdkInstances.at(-1)?.options as Record<string, unknown>;
286
+ expect((options.sampler as { toString(): string }).toString()).toBe(
287
+ 'AutotelSamplerAdapter',
288
+ );
289
+ });
290
+
230
291
  it('supports sdkFactory overrides', async () => {
231
292
  const { init, sdkInstances } = await loadInitWithMocks();
232
293
  const customSdk = mockDeep<NodeSDK>();
@@ -182,13 +182,18 @@ describe('init() integrations vs instrumentations', () => {
182
182
  _resetAutoInstrumentationsLoader();
183
183
  });
184
184
 
185
- afterEach(() => {
185
+ afterEach(async () => {
186
186
  for (const mod of mockedModules) {
187
187
  vi.doUnmock(mod);
188
188
  }
189
189
  vi.clearAllMocks();
190
190
  _resetAutoInstrumentationsLoader();
191
191
  delete process.env.AUTOTEL_METRICS;
192
+ // Reset global OTel state that can leak between forked test files
193
+ const { trace, context, propagation } = await import('@opentelemetry/api');
194
+ trace.disable();
195
+ context.disable();
196
+ propagation.disable();
192
197
  });
193
198
 
194
199
  it('excludes manual instrumentations from auto-instrumentations when autoInstrumentations: true', async () => {
package/src/init.ts CHANGED
@@ -25,8 +25,8 @@ import {
25
25
  ATTR_SERVICE_NAME,
26
26
  ATTR_SERVICE_VERSION,
27
27
  } from '@opentelemetry/semantic-conventions';
28
- import type { Sampler } from './sampling';
29
- import { AdaptiveSampler } from './sampling';
28
+ import type { Sampler, SamplingPreset } from './sampling';
29
+ import { samplingPresets, resolveSamplingPreset } from './sampling';
30
30
  import type { EventSubscriber } from './event-subscriber';
31
31
  import type { Logger } from './logger';
32
32
  import type { Attributes, Context, SpanKind, Link } from '@opentelemetry/api';
@@ -603,9 +603,17 @@ export interface AutotelConfig {
603
603
  */
604
604
  logs?: boolean | 'auto';
605
605
 
606
- /** Sampling strategy (default: AdaptiveSampler with 10% baseline) */
606
+ /** Sampling strategy - takes precedence over `sampling` preset */
607
607
  sampler?: Sampler;
608
608
 
609
+ /**
610
+ * Sampling preset shorthand — resolves to a pre-configured sampler.
611
+ * If both `sampler` and `sampling` are provided, `sampler` takes precedence.
612
+ *
613
+ * @default 'production'
614
+ */
615
+ sampling?: SamplingPreset;
616
+
609
617
  /** Service version (default: auto-detect from package.json or '1.0.0') */
610
618
  version?: string;
611
619
 
@@ -1592,8 +1600,17 @@ export function init(cfg: AutotelConfig): void {
1592
1600
  }
1593
1601
  }
1594
1602
 
1595
- const autotelSampler = mergedConfig.sampler || getDefaultSampler();
1596
- const sampler: OtelSampler = toOtelSampler(autotelSampler);
1603
+ const autotelSampler =
1604
+ mergedConfig.sampler ??
1605
+ (mergedConfig.sampling
1606
+ ? resolveSamplingPreset(mergedConfig.sampling)
1607
+ : undefined);
1608
+ if (autotelSampler) {
1609
+ mergedConfig.sampler = autotelSampler;
1610
+ }
1611
+ const sampler: OtelSampler = autotelSampler
1612
+ ? toOtelSampler(autotelSampler)
1613
+ : (envConfig.otelSampler ?? toOtelSampler(samplingPresets.production()));
1597
1614
 
1598
1615
  const sdkOptions: Partial<NodeSDKConfiguration> = {
1599
1616
  resource,
@@ -1921,14 +1938,7 @@ export function warnIfNotInitialized(context: string): void {
1921
1938
  * Get default sampler
1922
1939
  */
1923
1940
  export function getDefaultSampler(): Sampler {
1924
- return (
1925
- config?.sampler ||
1926
- new AdaptiveSampler({
1927
- baselineSampleRate: 0.1,
1928
- alwaysSampleErrors: true,
1929
- alwaysSampleSlow: true,
1930
- })
1931
- );
1941
+ return config?.sampler || samplingPresets.production();
1932
1942
  }
1933
1943
 
1934
1944
  /**
@@ -249,7 +249,7 @@ export async function initInstrumentation(
249
249
  headers: otlpHeaders,
250
250
  });
251
251
 
252
- // Enables tail sampling via sampling.tail.keep attribute
252
+ // Enables tail sampling via autotel.sampling.tail.keep attribute
253
253
  const spanProcessor = new TailSamplingSpanProcessor(
254
254
  new BatchSpanProcessor(traceExporter),
255
255
  );
@@ -11,6 +11,8 @@ import {
11
11
  FeatureFlagSampler,
12
12
  createLinkFromHeaders,
13
13
  extractLinksFromBatch,
14
+ samplingPresets,
15
+ resolveSamplingPreset,
14
16
  type SamplingContext,
15
17
  } from './sampling';
16
18
  import { type ILogger } from './logger';
@@ -64,9 +66,9 @@ describe('Sampling', () => {
64
66
  );
65
67
 
66
68
  const sampleCount = results.filter(Boolean).length;
67
- // Allow 10% margin of error
68
- expect(sampleCount).toBeGreaterThan(450);
69
- expect(sampleCount).toBeLessThan(550);
69
+ // Allow 20% margin of error — random sampling is inherently noisy
70
+ expect(sampleCount).toBeGreaterThan(400);
71
+ expect(sampleCount).toBeLessThan(600);
70
72
  });
71
73
  });
72
74
 
@@ -964,4 +966,95 @@ describe('Sampling', () => {
964
966
  });
965
967
  });
966
968
  });
969
+
970
+ describe('samplingPresets', () => {
971
+ it('development() returns AlwaysSampler', () => {
972
+ const sampler = samplingPresets.development();
973
+ expect(sampler).toBeInstanceOf(AlwaysSampler);
974
+ expect(sampler.shouldSample(context)).toBe(true);
975
+ });
976
+
977
+ it('errorsOnly() returns AdaptiveSampler that drops healthy baseline', () => {
978
+ const sampler = samplingPresets.errorsOnly();
979
+ expect(sampler).toBeInstanceOf(AdaptiveSampler);
980
+ sampler.shouldSample(context); // prime the WeakMap baseline decision
981
+ // Baseline is 0, so shouldKeepTrace for successful fast requests = false
982
+ expect(
983
+ sampler.shouldKeepTrace!(context, { success: true, duration: 50 }),
984
+ ).toBe(false);
985
+ });
986
+
987
+ it('errorsOnly() keeps errors', () => {
988
+ const sampler = samplingPresets.errorsOnly();
989
+ sampler.shouldSample(context); // prime the baseline decision
990
+ expect(
991
+ sampler.shouldKeepTrace!(context, {
992
+ success: false,
993
+ duration: 50,
994
+ error: new Error('fail'),
995
+ }),
996
+ ).toBe(true);
997
+ });
998
+
999
+ it('production() returns AdaptiveSampler with 10% baseline', () => {
1000
+ const sampler = samplingPresets.production();
1001
+ expect(sampler).toBeInstanceOf(AdaptiveSampler);
1002
+ expect(sampler.needsTailSampling!()).toBe(true);
1003
+ });
1004
+
1005
+ it('production() keeps errors', () => {
1006
+ const sampler = samplingPresets.production();
1007
+ sampler.shouldSample(context);
1008
+ expect(
1009
+ sampler.shouldKeepTrace!(context, {
1010
+ success: false,
1011
+ duration: 50,
1012
+ error: new Error('fail'),
1013
+ }),
1014
+ ).toBe(true);
1015
+ });
1016
+
1017
+ it('production() accepts overrides', () => {
1018
+ const sampler = samplingPresets.production({ baselineSampleRate: 1.0 });
1019
+ sampler.shouldSample(context);
1020
+ // With 100% baseline, all healthy traffic is kept
1021
+ expect(
1022
+ sampler.shouldKeepTrace!(context, { success: true, duration: 50 }),
1023
+ ).toBe(true);
1024
+ });
1025
+
1026
+ it('off() returns NeverSampler', () => {
1027
+ const sampler = samplingPresets.off();
1028
+ expect(sampler).toBeInstanceOf(NeverSampler);
1029
+ expect(sampler.shouldSample(context)).toBe(false);
1030
+ });
1031
+ });
1032
+
1033
+ describe('resolveSamplingPreset', () => {
1034
+ it('resolves development', () => {
1035
+ const sampler = resolveSamplingPreset('development');
1036
+ expect(sampler).toBeInstanceOf(AlwaysSampler);
1037
+ });
1038
+
1039
+ it('resolves errors-only', () => {
1040
+ const sampler = resolveSamplingPreset('errors-only');
1041
+ expect(sampler).toBeInstanceOf(AdaptiveSampler);
1042
+ });
1043
+
1044
+ it('resolves production', () => {
1045
+ const sampler = resolveSamplingPreset('production');
1046
+ expect(sampler).toBeInstanceOf(AdaptiveSampler);
1047
+ });
1048
+
1049
+ it('resolves off', () => {
1050
+ const sampler = resolveSamplingPreset('off');
1051
+ expect(sampler).toBeInstanceOf(NeverSampler);
1052
+ });
1053
+
1054
+ it('throws on invalid preset with helpful message', () => {
1055
+ expect(() => resolveSamplingPreset('banana' as any)).toThrow(
1056
+ /Unknown sampling preset: "banana".*Valid presets: development, errors-only, production, off/,
1057
+ );
1058
+ });
1059
+ });
967
1060
  });
package/src/sampling.ts CHANGED
@@ -26,6 +26,13 @@ import type { Link, Attributes } from '@opentelemetry/api';
26
26
  import { TraceFlags } from '@opentelemetry/api';
27
27
  import { type Logger } from './logger';
28
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
+
29
36
  /**
30
37
  * Sampler interface - return true to trace, false to skip
31
38
  */
@@ -493,6 +500,89 @@ export class FeatureFlagSampler implements Sampler {
493
500
  }
494
501
  }
495
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
+
496
586
  // ============================================================================
497
587
  // Link Helper Functions
498
588
  // ============================================================================