autotel 2.25.2 → 2.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) 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-BGVKKL2N.cjs → chunk-7FIGORWI.cjs} +46 -2
  7. package/dist/chunk-7FIGORWI.cjs.map +1 -0
  8. package/dist/{chunk-4KGC5N3J.cjs → chunk-A5ZUL2RZ.cjs} +16 -16
  9. package/dist/{chunk-4KGC5N3J.cjs.map → chunk-A5ZUL2RZ.cjs.map} +1 -1
  10. package/dist/{chunk-SR35DG5A.js → chunk-BBBWDIYQ.js} +27 -13
  11. package/dist/chunk-BBBWDIYQ.js.map +1 -0
  12. package/dist/{chunk-QUW4I2OI.js → chunk-CMUM4JQI.js} +3 -3
  13. package/dist/{chunk-QUW4I2OI.js.map → chunk-CMUM4JQI.js.map} +1 -1
  14. package/dist/{chunk-MNMLCLHH.cjs → chunk-EEJGUBWV.cjs} +5 -5
  15. package/dist/{chunk-MNMLCLHH.cjs.map → chunk-EEJGUBWV.cjs.map} +1 -1
  16. package/dist/{chunk-W4EUTSB2.cjs → chunk-HZ3FYBJG.cjs} +27 -12
  17. package/dist/chunk-HZ3FYBJG.cjs.map +1 -0
  18. package/dist/{chunk-L627YSSP.cjs → chunk-I6JPSD4R.cjs} +9 -9
  19. package/dist/{chunk-L627YSSP.cjs.map → chunk-I6JPSD4R.cjs.map} +1 -1
  20. package/dist/{chunk-JVICEM6W.cjs → chunk-ITYASFHQ.cjs} +91 -19
  21. package/dist/chunk-ITYASFHQ.cjs.map +1 -0
  22. package/dist/{chunk-XRKAL7WJ.cjs → chunk-JSNUWSBH.cjs} +6 -6
  23. package/dist/chunk-JSNUWSBH.cjs.map +1 -0
  24. package/dist/{chunk-GVLK7YUU.cjs → chunk-KZEC4CHV.cjs} +6 -4
  25. package/dist/chunk-KZEC4CHV.cjs.map +1 -0
  26. package/dist/{chunk-UKUYBUFQ.cjs → chunk-MN6PZ4AN.cjs} +7 -7
  27. package/dist/{chunk-UKUYBUFQ.cjs.map → chunk-MN6PZ4AN.cjs.map} +1 -1
  28. package/dist/{chunk-VXLEJWLY.js → chunk-MNBAXRVG.js} +89 -17
  29. package/dist/chunk-MNBAXRVG.js.map +1 -0
  30. package/dist/{chunk-6YIDHH2S.cjs → chunk-OFPZULMQ.cjs} +32 -10
  31. package/dist/chunk-OFPZULMQ.cjs.map +1 -0
  32. package/dist/{chunk-IKRHEUS7.js → chunk-OPTGXEVN.js} +370 -351
  33. package/dist/chunk-OPTGXEVN.js.map +1 -0
  34. package/dist/{chunk-77PLEJ54.js → chunk-QDREXAD7.js} +4 -4
  35. package/dist/{chunk-77PLEJ54.js.map → chunk-QDREXAD7.js.map} +1 -1
  36. package/dist/{chunk-RWOVNF3V.cjs → chunk-QQLP4M6W.cjs} +400 -381
  37. package/dist/chunk-QQLP4M6W.cjs.map +1 -0
  38. package/dist/{chunk-YTGF4L2C.js → chunk-RUD7KS4R.js} +27 -5
  39. package/dist/chunk-RUD7KS4R.js.map +1 -0
  40. package/dist/{chunk-USSL3D6L.js → chunk-S4OFEXLA.js} +6 -6
  41. package/dist/chunk-S4OFEXLA.js.map +1 -0
  42. package/dist/{chunk-XND7WBVX.js → chunk-VYA6QDNA.js} +43 -3
  43. package/dist/chunk-VYA6QDNA.js.map +1 -0
  44. package/dist/{chunk-KUSYIHW7.js → chunk-WYP6OOCT.js} +3 -3
  45. package/dist/{chunk-KUSYIHW7.js.map → chunk-WYP6OOCT.js.map} +1 -1
  46. package/dist/{chunk-4SCBD22Z.js → chunk-XB2GITM5.js} +4 -4
  47. package/dist/{chunk-4SCBD22Z.js.map → chunk-XB2GITM5.js.map} +1 -1
  48. package/dist/{chunk-X4RMFFMR.js → chunk-XDKK53OL.js} +6 -4
  49. package/dist/chunk-XDKK53OL.js.map +1 -0
  50. package/dist/correlation-id.cjs +10 -9
  51. package/dist/correlation-id.d.cts +4 -1
  52. package/dist/correlation-id.d.ts +4 -1
  53. package/dist/correlation-id.js +2 -1
  54. package/dist/decorators.cjs +8 -8
  55. package/dist/decorators.js +7 -7
  56. package/dist/event.cjs +10 -9
  57. package/dist/event.js +7 -6
  58. package/dist/functional.cjs +14 -14
  59. package/dist/functional.js +7 -7
  60. package/dist/http.cjs +2 -2
  61. package/dist/http.js +1 -1
  62. package/dist/index.cjs +85 -65
  63. package/dist/index.d.cts +4 -4
  64. package/dist/index.d.ts +4 -4
  65. package/dist/index.js +16 -16
  66. package/dist/{init-Q4uIQKbq.d.cts → init-CIzpC5kZ.d.cts} +9 -2
  67. package/dist/{init-ls4xSZe5.d.ts → init-C_PiC_Su.d.ts} +9 -2
  68. package/dist/instrumentation.cjs +12 -12
  69. package/dist/instrumentation.cjs.map +1 -1
  70. package/dist/instrumentation.js +4 -4
  71. package/dist/instrumentation.js.map +1 -1
  72. package/dist/messaging-adapters.d.cts +1 -1
  73. package/dist/messaging-adapters.d.ts +1 -1
  74. package/dist/messaging.cjs +11 -11
  75. package/dist/messaging.d.cts +1 -1
  76. package/dist/messaging.d.ts +1 -1
  77. package/dist/messaging.js +8 -8
  78. package/dist/metric-helpers.d.cts +1 -1
  79. package/dist/metric-helpers.d.ts +1 -1
  80. package/dist/sampling.cjs +26 -10
  81. package/dist/sampling.d.cts +49 -1
  82. package/dist/sampling.d.ts +49 -1
  83. package/dist/sampling.js +1 -1
  84. package/dist/semantic-helpers.cjs +12 -12
  85. package/dist/semantic-helpers.js +8 -8
  86. package/dist/tail-sampling-processor.cjs +6 -2
  87. package/dist/tail-sampling-processor.d.cts +2 -2
  88. package/dist/tail-sampling-processor.d.ts +2 -2
  89. package/dist/tail-sampling-processor.js +5 -1
  90. package/dist/trace-helpers.d.cts +1 -1
  91. package/dist/trace-helpers.d.ts +1 -1
  92. package/dist/{utils-D1trOLNm.d.ts → utils-Buel3cj0.d.ts} +1 -1
  93. package/dist/{utils-DuNJfXSH.d.cts → utils-CbUkl8r1.d.cts} +1 -1
  94. package/dist/webhook.cjs +8 -8
  95. package/dist/webhook.js +7 -7
  96. package/dist/workflow-distributed.cjs +9 -9
  97. package/dist/workflow-distributed.js +7 -7
  98. package/dist/workflow.cjs +12 -12
  99. package/dist/workflow.js +8 -8
  100. package/dist/yaml-config.cjs +5 -5
  101. package/dist/yaml-config.d.cts +5 -4
  102. package/dist/yaml-config.d.ts +5 -4
  103. package/dist/yaml-config.js +2 -2
  104. package/package.json +1 -1
  105. package/src/correlation-id.ts +10 -5
  106. package/src/env-config.test.ts +77 -0
  107. package/src/env-config.ts +106 -0
  108. package/src/functional.ts +447 -421
  109. package/src/index.ts +6 -0
  110. package/src/init.customization.test.ts +61 -0
  111. package/src/init.integrations.test.ts +6 -1
  112. package/src/init.ts +23 -13
  113. package/src/instrumentation.ts +1 -1
  114. package/src/sampling.test.ts +96 -3
  115. package/src/sampling.ts +90 -0
  116. package/src/tail-sampling-processor.test.ts +26 -22
  117. package/src/tail-sampling-processor.ts +8 -4
  118. package/src/trace-context.test.ts +73 -0
  119. package/src/trace-context.ts +44 -12
  120. package/src/yaml-config.test.ts +71 -0
  121. package/src/yaml-config.ts +32 -2
  122. package/dist/chunk-6YIDHH2S.cjs.map +0 -1
  123. package/dist/chunk-BGVKKL2N.cjs.map +0 -1
  124. package/dist/chunk-GVLK7YUU.cjs.map +0 -1
  125. package/dist/chunk-IKRHEUS7.js.map +0 -1
  126. package/dist/chunk-JVICEM6W.cjs.map +0 -1
  127. package/dist/chunk-RWOVNF3V.cjs.map +0 -1
  128. package/dist/chunk-SR35DG5A.js.map +0 -1
  129. package/dist/chunk-USSL3D6L.js.map +0 -1
  130. package/dist/chunk-VXLEJWLY.js.map +0 -1
  131. package/dist/chunk-W4EUTSB2.cjs.map +0 -1
  132. package/dist/chunk-X4RMFFMR.js.map +0 -1
  133. package/dist/chunk-XND7WBVX.js.map +0 -1
  134. package/dist/chunk-XRKAL7WJ.cjs.map +0 -1
  135. package/dist/chunk-YTGF4L2C.js.map +0 -1
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
  // ============================================================================
@@ -4,6 +4,10 @@
4
4
 
5
5
  import { describe, it, expect, beforeEach } from 'vitest';
6
6
  import { TailSamplingSpanProcessor } from './tail-sampling-processor';
7
+ import {
8
+ AUTOTEL_SAMPLING_TAIL_KEEP,
9
+ AUTOTEL_SAMPLING_TAIL_EVALUATED,
10
+ } from './sampling';
7
11
  import type {
8
12
  SpanProcessor,
9
13
  ReadableSpan,
@@ -86,8 +90,8 @@ describe('TailSamplingSpanProcessor', () => {
86
90
 
87
91
  it('should forward spans marked to keep (sampling.tail.keep = true)', () => {
88
92
  const span = createMockSpan({
89
- 'sampling.tail.evaluated': true,
90
- 'sampling.tail.keep': true,
93
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
94
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: true,
91
95
  });
92
96
 
93
97
  tailSamplingProcessor.onEnd(span);
@@ -100,8 +104,8 @@ describe('TailSamplingSpanProcessor', () => {
100
104
  describe('Span dropping', () => {
101
105
  it('should drop spans marked to drop (sampling.tail.keep = false)', () => {
102
106
  const span = createMockSpan({
103
- 'sampling.tail.evaluated': true,
104
- 'sampling.tail.keep': false,
107
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
108
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: false,
105
109
  });
106
110
 
107
111
  tailSamplingProcessor.onEnd(span);
@@ -112,13 +116,13 @@ describe('TailSamplingSpanProcessor', () => {
112
116
 
113
117
  it('should drop multiple spans marked as false', () => {
114
118
  const span1 = createMockSpan({
115
- 'sampling.tail.evaluated': true,
116
- 'sampling.tail.keep': false,
119
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
120
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: false,
117
121
  });
118
122
 
119
123
  const span2 = createMockSpan({
120
- 'sampling.tail.evaluated': true,
121
- 'sampling.tail.keep': false,
124
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
125
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: false,
122
126
  });
123
127
 
124
128
  tailSamplingProcessor.onEnd(span1);
@@ -131,8 +135,8 @@ describe('TailSamplingSpanProcessor', () => {
131
135
  describe('Edge cases', () => {
132
136
  it('should forward spans when only evaluated but no keep attribute', () => {
133
137
  const span = createMockSpan({
134
- 'sampling.tail.evaluated': true,
135
- // Missing 'sampling.tail.keep'
138
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
139
+ // Missing AUTOTEL_SAMPLING_TAIL_KEEP
136
140
  });
137
141
 
138
142
  tailSamplingProcessor.onEnd(span);
@@ -143,8 +147,8 @@ describe('TailSamplingSpanProcessor', () => {
143
147
 
144
148
  it('should forward spans when only keep but not evaluated', () => {
145
149
  const span = createMockSpan({
146
- // Missing 'sampling.tail.evaluated'
147
- 'sampling.tail.keep': false,
150
+ // Missing AUTOTEL_SAMPLING_TAIL_EVALUATED
151
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: false,
148
152
  });
149
153
 
150
154
  tailSamplingProcessor.onEnd(span);
@@ -156,12 +160,12 @@ describe('TailSamplingSpanProcessor', () => {
156
160
  it('should handle mixed spans (some kept, some dropped)', () => {
157
161
  const keptSpan1 = createMockSpan({ foo: 'bar' });
158
162
  const droppedSpan = createMockSpan({
159
- 'sampling.tail.evaluated': true,
160
- 'sampling.tail.keep': false,
163
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
164
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: false,
161
165
  });
162
166
  const keptSpan2 = createMockSpan({
163
- 'sampling.tail.evaluated': true,
164
- 'sampling.tail.keep': true,
167
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
168
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: true,
165
169
  });
166
170
 
167
171
  tailSamplingProcessor.onEnd(keptSpan1);
@@ -196,20 +200,20 @@ describe('TailSamplingSpanProcessor', () => {
196
200
  // 3. Error request (keep)
197
201
 
198
202
  const fastSpan = createMockSpan({
199
- 'sampling.tail.evaluated': true,
200
- 'sampling.tail.keep': false, // Fast, no errors
203
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
204
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: false, // Fast, no errors
201
205
  duration: 50,
202
206
  });
203
207
 
204
208
  const slowSpan = createMockSpan({
205
- 'sampling.tail.evaluated': true,
206
- 'sampling.tail.keep': true, // Slow request
209
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
210
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: true, // Slow request
207
211
  duration: 1000,
208
212
  });
209
213
 
210
214
  const errorSpan = createMockSpan({
211
- 'sampling.tail.evaluated': true,
212
- 'sampling.tail.keep': true, // Had error
215
+ [AUTOTEL_SAMPLING_TAIL_EVALUATED]: true,
216
+ [AUTOTEL_SAMPLING_TAIL_KEEP]: true, // Had error
213
217
  error: true,
214
218
  });
215
219
 
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Tail Sampling Span Processor
3
3
  *
4
- * Filters spans based on the `sampling.tail.keep` attribute set during execution.
4
+ * Filters spans based on the `autotel.sampling.tail.keep` attribute set during execution.
5
5
  * This enables adaptive sampling where we decide whether to keep a span AFTER
6
6
  * the operation completes, based on criteria like errors, duration, etc.
7
7
  *
8
8
  * How it works:
9
9
  * 1. Decorator creates span optimistically (head sampling returns true)
10
10
  * 2. Operation executes and completes
11
- * 3. Decorator calls shouldKeepTrace() and sets sampling.tail.keep attribute
11
+ * 3. Decorator calls shouldKeepTrace() and sets autotel.sampling.tail.keep attribute
12
12
  * 4. This processor checks the attribute and drops spans marked as false
13
13
  */
14
14
 
@@ -18,6 +18,10 @@ import type {
18
18
  } from '@opentelemetry/sdk-trace-base';
19
19
  import type { Context } from '@opentelemetry/api';
20
20
  import type { Span } from '@opentelemetry/sdk-trace-base';
21
+ import {
22
+ AUTOTEL_SAMPLING_TAIL_KEEP,
23
+ AUTOTEL_SAMPLING_TAIL_EVALUATED,
24
+ } from './sampling';
21
25
 
22
26
  export class TailSamplingSpanProcessor implements SpanProcessor {
23
27
  private wrappedProcessor: SpanProcessor;
@@ -31,8 +35,8 @@ export class TailSamplingSpanProcessor implements SpanProcessor {
31
35
  }
32
36
 
33
37
  onEnd(span: ReadableSpan): void {
34
- const tailEvaluated = span.attributes['sampling.tail.evaluated'];
35
- const shouldKeep = span.attributes['sampling.tail.keep'];
38
+ const tailEvaluated = span.attributes[AUTOTEL_SAMPLING_TAIL_EVALUATED];
39
+ const shouldKeep = span.attributes[AUTOTEL_SAMPLING_TAIL_KEEP];
36
40
 
37
41
  if (tailEvaluated === true && shouldKeep === false) {
38
42
  return;
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { enterOrRun } from './trace-context';
3
+
4
+ type Box<T> = { value: T };
5
+
6
+ function createFakeStorage<T>(initialValue?: T) {
7
+ let currentStore =
8
+ initialValue === undefined ? undefined : { value: initialValue };
9
+ const runCalls: Array<Box<T>> = [];
10
+ const enterWithCalls: Array<Box<T>> = [];
11
+
12
+ const storage = {
13
+ getStore() {
14
+ return currentStore;
15
+ },
16
+ run(store: Box<T>, fn: () => void) {
17
+ runCalls.push(store);
18
+ const previousStore = currentStore;
19
+ currentStore = store;
20
+ try {
21
+ fn();
22
+ } finally {
23
+ currentStore = previousStore;
24
+ }
25
+ },
26
+ enterWith(store: Box<T>) {
27
+ enterWithCalls.push(store);
28
+ currentStore = store;
29
+ },
30
+ };
31
+
32
+ return {
33
+ enterWithCalls,
34
+ runCalls,
35
+ storage: storage as unknown as {
36
+ enterWith?: (store: Box<T>) => void;
37
+ getStore: () => Box<T> | undefined;
38
+ run: (store: Box<T>, fn: () => void) => void;
39
+ },
40
+ };
41
+ }
42
+
43
+ describe('enterOrRun', () => {
44
+ it('mutates the existing store when already inside a run scope', () => {
45
+ const { storage } = createFakeStorage('outer');
46
+
47
+ enterOrRun(storage as never, 'updated');
48
+
49
+ expect(storage.getStore()?.value).toBe('updated');
50
+ });
51
+
52
+ it('falls back to run() when enterWith throws', () => {
53
+ const { runCalls, storage } = createFakeStorage<string>();
54
+ storage.enterWith = () => {
55
+ throw new Error('enterWith not supported');
56
+ };
57
+
58
+ enterOrRun(storage as never, 'worker-value');
59
+
60
+ expect(runCalls).toHaveLength(1);
61
+ expect(runCalls[0]?.value).toBe('worker-value');
62
+ });
63
+
64
+ it('prefers enterWith() when no store exists and the runtime supports it', () => {
65
+ const { enterWithCalls, storage } = createFakeStorage<string>();
66
+
67
+ enterOrRun(storage as never, 'node-value');
68
+
69
+ expect(enterWithCalls).toHaveLength(1);
70
+ expect(enterWithCalls[0]?.value).toBe('node-value');
71
+ expect(storage.getStore()?.value).toBe('node-value');
72
+ });
73
+ });