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.
- package/README.md +26 -1
- package/dist/auto.cjs +2 -2
- package/dist/auto.js +1 -1
- package/dist/correlation-id.cjs +1 -1
- package/dist/correlation-id.js +1 -1
- package/dist/decorators.cjs +1 -1
- package/dist/decorators.js +1 -1
- package/dist/{event-Dlqr4ZNL.cjs → event-BhHREDJk.cjs} +3 -3
- package/dist/{event-Dlqr4ZNL.cjs.map → event-BhHREDJk.cjs.map} +1 -1
- package/dist/{event-_58ryBjh.js → event-ByBTV9M2.js} +3 -3
- package/dist/{event-_58ryBjh.js.map → event-ByBTV9M2.js.map} +1 -1
- package/dist/event.cjs +1 -1
- package/dist/event.js +1 -1
- package/dist/{functional-BGkT8J-h.js → functional-DtI0u4vx.js} +19 -19
- package/dist/functional-DtI0u4vx.js.map +1 -0
- package/dist/{functional-C4CzoVrX.cjs → functional-zpzNLhky.cjs} +4 -4
- package/dist/{functional-C4CzoVrX.cjs.map → functional-zpzNLhky.cjs.map} +1 -1
- package/dist/functional.cjs +1 -1
- package/dist/functional.js +1 -1
- package/dist/http.cjs +1 -1
- package/dist/http.js +1 -1
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -5
- package/dist/{init-DJQOdVlN.d.ts → init-B7u-DjxM.d.ts} +57 -2
- package/dist/init-B7u-DjxM.d.ts.map +1 -0
- package/dist/{init-DvapOXCc.cjs → init-BX7AmFRl.cjs} +40 -21
- package/dist/init-BX7AmFRl.cjs.map +1 -0
- package/dist/{init-Ch6t7MNI.js → init-D-jnNMix.js} +39 -20
- package/dist/init-D-jnNMix.js.map +1 -0
- package/dist/{init-CNp-ee80.d.cts → init-DSrRmVnz.d.cts} +57 -2
- package/dist/init-DSrRmVnz.d.cts.map +1 -0
- package/dist/instrumentation.cjs +1 -1
- package/dist/instrumentation.js +1 -1
- package/dist/logger-D3Ej3DII.js +446 -0
- package/dist/logger-D3Ej3DII.js.map +1 -0
- package/dist/logger-thMPLpOG.cjs +487 -0
- package/dist/logger-thMPLpOG.cjs.map +1 -0
- package/dist/logger.cjs +8 -236
- package/dist/logger.js +2 -204
- package/dist/messaging.cjs +1 -1
- package/dist/messaging.js +1 -1
- package/dist/semantic-helpers.cjs +1 -1
- package/dist/semantic-helpers.js +1 -1
- package/dist/{track-3HY4NGV-.cjs → track-D59FfpL0.cjs} +2 -2
- package/dist/{track-3HY4NGV-.cjs.map → track-D59FfpL0.cjs.map} +1 -1
- package/dist/{track-nsKVy-pj.js → track-wc0HafS_.js} +6 -6
- package/dist/track-wc0HafS_.js.map +1 -0
- package/dist/webhook.cjs +1 -1
- package/dist/webhook.js +1 -1
- package/dist/workflow-distributed.cjs +1 -1
- package/dist/workflow-distributed.js +1 -1
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.js +1 -1
- package/dist/{yaml-config-B3dQ82GR.cjs → yaml-config-Ck2uB0Dp.cjs} +2 -1
- package/dist/yaml-config-Ck2uB0Dp.cjs.map +1 -0
- package/dist/yaml-config.cjs +1 -1
- package/dist/yaml-config.d.cts +7 -1
- package/dist/yaml-config.d.cts.map +1 -1
- package/dist/yaml-config.d.ts +7 -1
- package/dist/yaml-config.d.ts.map +1 -1
- package/dist/yaml-config.js +1 -0
- package/dist/yaml-config.js.map +1 -1
- package/package.json +1 -2
- package/skills/autotel-core/SKILL.md +2 -0
- package/skills/autotel-instrumentation/SKILL.md +25 -0
- package/skills/debug-missing-spans/SKILL.md +3 -1
- package/skills/migrate-to-autotel/SKILL.md +24 -23
- package/skills/review-otel-patterns/SKILL.md +5 -4
- package/dist/functional-BGkT8J-h.js.map +0 -1
- package/dist/init-CNp-ee80.d.cts.map +0 -1
- package/dist/init-Ch6t7MNI.js.map +0 -1
- package/dist/init-DJQOdVlN.d.ts.map +0 -1
- package/dist/init-DvapOXCc.cjs.map +0 -1
- package/dist/logger.cjs.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/track-nsKVy-pj.js.map +0 -1
- package/dist/yaml-config-B3dQ82GR.cjs.map +0 -1
- package/src/attribute-redacting-processor.test.ts +0 -763
- package/src/attribute-redacting-processor.ts +0 -621
- package/src/attributes/attachers.ts +0 -161
- package/src/attributes/builders.ts +0 -529
- package/src/attributes/domains.ts +0 -42
- package/src/attributes/index.ts +0 -81
- package/src/attributes/registry.ts +0 -323
- package/src/attributes/types.ts +0 -211
- package/src/attributes/utils.ts +0 -64
- package/src/attributes/validators.ts +0 -266
- package/src/attributes.test.ts +0 -292
- package/src/auto.ts +0 -67
- package/src/autotel-logger.test.ts +0 -548
- package/src/autotel-logger.ts +0 -364
- package/src/baggage-span-processor.test.ts +0 -202
- package/src/baggage-span-processor.ts +0 -100
- package/src/business-baggage.test.ts +0 -500
- package/src/business-baggage.ts +0 -669
- package/src/circuit-breaker.test.ts +0 -341
- package/src/circuit-breaker.ts +0 -184
- package/src/config.test.ts +0 -94
- package/src/config.ts +0 -172
- package/src/correlated-events.test.ts +0 -151
- package/src/correlated-events.ts +0 -47
- package/src/correlation-id.test.ts +0 -163
- package/src/correlation-id.ts +0 -206
- package/src/db.test.ts +0 -252
- package/src/db.ts +0 -447
- package/src/decorators.test.ts +0 -153
- package/src/decorators.ts +0 -188
- package/src/define-event.test.ts +0 -41
- package/src/define-event.ts +0 -58
- package/src/devtools.ts +0 -60
- package/src/drain-pipeline.test.ts +0 -68
- package/src/drain-pipeline.ts +0 -199
- package/src/drain-toolkit.test.ts +0 -113
- package/src/drain-toolkit.ts +0 -129
- package/src/enricher-toolkit.test.ts +0 -67
- package/src/enricher-toolkit.ts +0 -79
- package/src/enrichers.test.ts +0 -150
- package/src/enrichers.ts +0 -145
- package/src/env-config.test.ts +0 -323
- package/src/env-config.ts +0 -309
- package/src/error-catalog.test.ts +0 -133
- package/src/error-catalog.ts +0 -262
- package/src/event-queue.test.ts +0 -864
- package/src/event-queue.ts +0 -699
- package/src/event-subscriber.ts +0 -262
- package/src/event-testing.ts +0 -197
- package/src/event.test.ts +0 -1104
- package/src/event.ts +0 -988
- package/src/events-config.ts +0 -235
- package/src/exporters.ts +0 -165
- package/src/filtering-span-processor.test.ts +0 -281
- package/src/filtering-span-processor.ts +0 -111
- package/src/flatten-attributes.test.ts +0 -76
- package/src/flatten-attributes.ts +0 -80
- package/src/functional.strict-types.typecheck.ts +0 -53
- package/src/functional.test.ts +0 -1464
- package/src/functional.ts +0 -2539
- package/src/functional.types.test.ts +0 -135
- package/src/hook.mjs +0 -15
- package/src/http.test.ts +0 -485
- package/src/http.ts +0 -424
- package/src/index.ts +0 -433
- package/src/init-auto-redactor.test.ts +0 -53
- package/src/init-redactor.test.ts +0 -8
- package/src/init.customization.test.ts +0 -594
- package/src/init.integrations.test.ts +0 -399
- package/src/init.openllmetry.test.ts +0 -194
- package/src/init.protocol.test.ts +0 -215
- package/src/init.ts +0 -2312
- package/src/instrumentation.test.ts +0 -108
- package/src/instrumentation.ts +0 -319
- package/src/logger.test.ts +0 -125
- package/src/logger.ts +0 -341
- package/src/messaging-adapters.test.ts +0 -595
- package/src/messaging-adapters.ts +0 -583
- package/src/messaging-testing.test.ts +0 -573
- package/src/messaging-testing.ts +0 -935
- package/src/messaging.test.ts +0 -1646
- package/src/messaging.ts +0 -2245
- package/src/metric-helpers.ts +0 -47
- package/src/metric-testing.ts +0 -197
- package/src/metric.ts +0 -446
- package/src/metrics.test.ts +0 -241
- package/src/node-require.ts +0 -123
- package/src/operation-context.ts +0 -93
- package/src/parse-error.test.ts +0 -73
- package/src/parse-error.ts +0 -112
- package/src/posthog-logs.test.ts +0 -115
- package/src/posthog-logs.ts +0 -77
- package/src/pretty-console-exporter.test.ts +0 -545
- package/src/pretty-console-exporter.ts +0 -413
- package/src/pretty-log-formatter.test.ts +0 -123
- package/src/pretty-log-formatter.ts +0 -210
- package/src/processors/canonical-log-line-processor.test.ts +0 -523
- package/src/processors/canonical-log-line-processor.ts +0 -396
- package/src/processors.ts +0 -152
- package/src/rate-limiter.test.ts +0 -199
- package/src/rate-limiter.ts +0 -98
- package/src/redact-values.test.ts +0 -90
- package/src/redact-values.ts +0 -34
- package/src/register.ts +0 -37
- package/src/request-logger.test.ts +0 -545
- package/src/request-logger.ts +0 -342
- package/src/sampling.test.ts +0 -1060
- package/src/sampling.ts +0 -737
- package/src/security-schema.test.ts +0 -45
- package/src/security-schema.ts +0 -107
- package/src/semantic-conventions.ts +0 -15
- package/src/semantic-helpers.test.ts +0 -226
- package/src/semantic-helpers.ts +0 -438
- package/src/shutdown.test.ts +0 -364
- package/src/shutdown.ts +0 -246
- package/src/span-name-normalizer.test.ts +0 -377
- package/src/span-name-normalizer.ts +0 -213
- package/src/stable-hash.ts +0 -27
- package/src/structured-error.test.ts +0 -191
- package/src/structured-error.ts +0 -157
- package/src/stub.integration.test.ts +0 -361
- package/src/tail-sampling-processor.test.ts +0 -230
- package/src/tail-sampling-processor.ts +0 -55
- package/src/test-span-collector.test.ts +0 -234
- package/src/test-span-collector.ts +0 -150
- package/src/testing.ts +0 -705
- package/src/trace-context.test.ts +0 -73
- package/src/trace-context.ts +0 -567
- package/src/trace-helpers.new.test.ts +0 -278
- package/src/trace-helpers.test.ts +0 -290
- package/src/trace-helpers.ts +0 -710
- package/src/trace-hybrid.test.ts +0 -42
- package/src/trace-hybrid.ts +0 -37
- package/src/tracer-provider.test.ts +0 -183
- package/src/tracer-provider.ts +0 -266
- package/src/track.test.ts +0 -154
- package/src/track.ts +0 -216
- package/src/validate.test.ts +0 -287
- package/src/validate.ts +0 -307
- package/src/validation-attributes.ts +0 -43
- package/src/validation.test.ts +0 -330
- package/src/validation.ts +0 -246
- package/src/variable-name-inference.test.ts +0 -178
- package/src/variable-name-inference.ts +0 -242
- package/src/webhook.test.ts +0 -649
- package/src/webhook.ts +0 -637
- package/src/workflow-distributed.test.ts +0 -786
- package/src/workflow-distributed.ts +0 -916
- package/src/workflow.async-safety.integration.test.ts +0 -345
- package/src/workflow.test.ts +0 -647
- package/src/workflow.ts +0 -810
- package/src/yaml-config.test.ts +0 -337
- package/src/yaml-config.ts +0 -342
package/src/sampling.test.ts
DELETED
|
@@ -1,1060 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { TraceFlags } from '@opentelemetry/api';
|
|
3
|
-
import type { Link, SpanContext } from '@opentelemetry/api';
|
|
4
|
-
import {
|
|
5
|
-
RandomSampler,
|
|
6
|
-
AlwaysSampler,
|
|
7
|
-
NeverSampler,
|
|
8
|
-
AdaptiveSampler,
|
|
9
|
-
UserIdSampler,
|
|
10
|
-
CompositeSampler,
|
|
11
|
-
FeatureFlagSampler,
|
|
12
|
-
createLinkFromHeaders,
|
|
13
|
-
extractLinksFromBatch,
|
|
14
|
-
samplingPresets,
|
|
15
|
-
resolveSamplingPreset,
|
|
16
|
-
type SamplingContext,
|
|
17
|
-
} from './sampling';
|
|
18
|
-
import { type ILogger } from './logger';
|
|
19
|
-
|
|
20
|
-
describe('Sampling', () => {
|
|
21
|
-
let mockLogger: ILogger;
|
|
22
|
-
let context: SamplingContext;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
mockLogger = {
|
|
26
|
-
info: vi.fn(),
|
|
27
|
-
warn: vi.fn(),
|
|
28
|
-
error: vi.fn(),
|
|
29
|
-
debug: vi.fn(),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
context = {
|
|
33
|
-
operationName: 'test.operation',
|
|
34
|
-
args: [{ userId: '123', email: 'test@example.com' }],
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('RandomSampler', () => {
|
|
39
|
-
it('should throw error for invalid sample rates', () => {
|
|
40
|
-
expect(() => new RandomSampler(-0.1)).toThrow();
|
|
41
|
-
expect(() => new RandomSampler(1.1)).toThrow();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should sample at 100%', () => {
|
|
45
|
-
const sampler = new RandomSampler(1);
|
|
46
|
-
const results = Array.from({ length: 100 }, () =>
|
|
47
|
-
sampler.shouldSample(context),
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
expect(results.every((r) => r === true)).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should never sample at 0%', () => {
|
|
54
|
-
const sampler = new RandomSampler(0);
|
|
55
|
-
const results = Array.from({ length: 100 }, () =>
|
|
56
|
-
sampler.shouldSample(context),
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
expect(results.every((r) => r === false)).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should sample approximately at the specified rate', () => {
|
|
63
|
-
const sampler = new RandomSampler(0.5);
|
|
64
|
-
const results = Array.from({ length: 1000 }, () =>
|
|
65
|
-
sampler.shouldSample(context),
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const sampleCount = results.filter(Boolean).length;
|
|
69
|
-
// Allow 20% margin of error — random sampling is inherently noisy
|
|
70
|
-
expect(sampleCount).toBeGreaterThan(400);
|
|
71
|
-
expect(sampleCount).toBeLessThan(600);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('AlwaysSampler', () => {
|
|
76
|
-
it('should always sample', () => {
|
|
77
|
-
const sampler = new AlwaysSampler();
|
|
78
|
-
const results = Array.from({ length: 100 }, () =>
|
|
79
|
-
sampler.shouldSample(context),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
expect(results.every((r) => r === true)).toBe(true);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('NeverSampler', () => {
|
|
87
|
-
it('should never sample', () => {
|
|
88
|
-
const sampler = new NeverSampler();
|
|
89
|
-
const results = Array.from({ length: 100 }, () =>
|
|
90
|
-
sampler.shouldSample(context),
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(results.every((r) => r === false)).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('AdaptiveSampler', () => {
|
|
98
|
-
it('should throw error for invalid baseline sample rate', () => {
|
|
99
|
-
expect(
|
|
100
|
-
() =>
|
|
101
|
-
new AdaptiveSampler({
|
|
102
|
-
baselineSampleRate: -0.1,
|
|
103
|
-
}),
|
|
104
|
-
).toThrow();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should indicate it needs tail sampling', () => {
|
|
108
|
-
const sampler = new AdaptiveSampler({
|
|
109
|
-
baselineSampleRate: 0.1,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
expect(sampler.needsTailSampling()).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should always create spans (optimistic sampling)', () => {
|
|
116
|
-
const sampler = new AdaptiveSampler({
|
|
117
|
-
baselineSampleRate: 0, // Even with 0% baseline
|
|
118
|
-
logger: mockLogger,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// FIX: shouldSample now always returns true for tail sampling
|
|
122
|
-
const result = sampler.shouldSample(context);
|
|
123
|
-
expect(result).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should keep error traces even when baseline would drop them', () => {
|
|
127
|
-
const sampler = new AdaptiveSampler({
|
|
128
|
-
baselineSampleRate: 0, // 0% baseline sampling
|
|
129
|
-
alwaysSampleErrors: true,
|
|
130
|
-
logger: mockLogger,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// FIX: Span is created (shouldSample returns true)
|
|
134
|
-
const shouldSample = sampler.shouldSample(context);
|
|
135
|
-
expect(shouldSample).toBe(true);
|
|
136
|
-
|
|
137
|
-
// Tail sampling keeps error traces
|
|
138
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
139
|
-
success: false,
|
|
140
|
-
duration: 100,
|
|
141
|
-
error: new Error('Test error'),
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
expect(shouldKeep).toBe(true);
|
|
145
|
-
// Pino-native: (extra, message)
|
|
146
|
-
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
147
|
-
expect.objectContaining({
|
|
148
|
-
operation: 'test.operation',
|
|
149
|
-
error: 'Test error',
|
|
150
|
-
}),
|
|
151
|
-
'Adaptive sampling: Keeping error trace',
|
|
152
|
-
);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should keep slow traces even when baseline would drop them', () => {
|
|
156
|
-
const sampler = new AdaptiveSampler({
|
|
157
|
-
baselineSampleRate: 0, // 0% baseline sampling
|
|
158
|
-
slowThresholdMs: 1000,
|
|
159
|
-
alwaysSampleSlow: true,
|
|
160
|
-
logger: mockLogger,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// FIX: Span is created (shouldSample returns true)
|
|
164
|
-
const shouldSample = sampler.shouldSample(context);
|
|
165
|
-
expect(shouldSample).toBe(true);
|
|
166
|
-
|
|
167
|
-
// Tail sampling keeps slow traces
|
|
168
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
169
|
-
success: true,
|
|
170
|
-
duration: 1500, // > 1000ms threshold
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
expect(shouldKeep).toBe(true);
|
|
174
|
-
// Pino-native: (extra, message)
|
|
175
|
-
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
176
|
-
expect.objectContaining({
|
|
177
|
-
operation: 'test.operation',
|
|
178
|
-
duration: 1500,
|
|
179
|
-
}),
|
|
180
|
-
'Adaptive sampling: Keeping slow trace',
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should drop fast successful traces when baseline sampling says no', () => {
|
|
185
|
-
const sampler = new AdaptiveSampler({
|
|
186
|
-
baselineSampleRate: 0, // 0% baseline
|
|
187
|
-
slowThresholdMs: 1000,
|
|
188
|
-
logger: mockLogger,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Span is created optimistically
|
|
192
|
-
const shouldSample = sampler.shouldSample(context);
|
|
193
|
-
expect(shouldSample).toBe(true);
|
|
194
|
-
|
|
195
|
-
// Tail sampling drops fast/successful traces
|
|
196
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
197
|
-
success: true,
|
|
198
|
-
duration: 100, // < 1000ms threshold
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
expect(shouldKeep).toBe(false);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('should keep fast successful traces when baseline sampling says yes', () => {
|
|
205
|
-
const sampler = new AdaptiveSampler({
|
|
206
|
-
baselineSampleRate: 1, // 100% baseline
|
|
207
|
-
slowThresholdMs: 1000,
|
|
208
|
-
logger: mockLogger,
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const shouldSample = sampler.shouldSample(context);
|
|
212
|
-
expect(shouldSample).toBe(true);
|
|
213
|
-
|
|
214
|
-
// Baseline sampled it, so keep it
|
|
215
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
216
|
-
success: true,
|
|
217
|
-
duration: 100,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
expect(shouldKeep).toBe(true);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('should respect alwaysSampleErrors flag', () => {
|
|
224
|
-
const sampler = new AdaptiveSampler({
|
|
225
|
-
baselineSampleRate: 0,
|
|
226
|
-
alwaysSampleErrors: false, // Don't force-sample errors
|
|
227
|
-
logger: mockLogger,
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
231
|
-
success: false,
|
|
232
|
-
duration: 100,
|
|
233
|
-
error: new Error('Test error'),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// With alwaysSampleErrors=false and baseline=0, errors are dropped
|
|
237
|
-
expect(shouldKeep).toBe(false);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('should respect alwaysSampleSlow flag', () => {
|
|
241
|
-
const sampler = new AdaptiveSampler({
|
|
242
|
-
baselineSampleRate: 0,
|
|
243
|
-
slowThresholdMs: 1000,
|
|
244
|
-
alwaysSampleSlow: false, // Don't force-sample slow requests
|
|
245
|
-
logger: mockLogger,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
249
|
-
success: true,
|
|
250
|
-
duration: 1500,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// With alwaysSampleSlow=false and baseline=0, slow requests are dropped
|
|
254
|
-
expect(shouldKeep).toBe(false);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe('UserIdSampler', () => {
|
|
259
|
-
const extractUserId = (args: unknown[]) => {
|
|
260
|
-
const firstArg = args[0] as { userId?: string };
|
|
261
|
-
return firstArg?.userId;
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
it('should always sample specific users', () => {
|
|
265
|
-
const sampler = new UserIdSampler({
|
|
266
|
-
baselineSampleRate: 0,
|
|
267
|
-
alwaysSampleUsers: ['vip_123'],
|
|
268
|
-
extractUserId,
|
|
269
|
-
logger: mockLogger,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const vipContext: SamplingContext = {
|
|
273
|
-
operationName: 'test.operation',
|
|
274
|
-
args: [{ userId: 'vip_123' }],
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
expect(sampler.shouldSample(vipContext)).toBe(true);
|
|
278
|
-
// Pino-native: (extra, message)
|
|
279
|
-
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
280
|
-
{
|
|
281
|
-
operation: 'test.operation',
|
|
282
|
-
userId: 'vip_123',
|
|
283
|
-
},
|
|
284
|
-
'Sampling user request',
|
|
285
|
-
);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('should use consistent per-user sampling', () => {
|
|
289
|
-
const sampler = new UserIdSampler({
|
|
290
|
-
baselineSampleRate: 0.5,
|
|
291
|
-
extractUserId,
|
|
292
|
-
logger: mockLogger,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const user123Context: SamplingContext = {
|
|
296
|
-
operationName: 'test.operation',
|
|
297
|
-
args: [{ userId: 'user_123' }],
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Same user should always get same result
|
|
301
|
-
const result1 = sampler.shouldSample(user123Context);
|
|
302
|
-
const result2 = sampler.shouldSample(user123Context);
|
|
303
|
-
const result3 = sampler.shouldSample(user123Context);
|
|
304
|
-
|
|
305
|
-
expect(result1).toBe(result2);
|
|
306
|
-
expect(result2).toBe(result3);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('should add and remove users from always-sample list', () => {
|
|
310
|
-
const sampler = new UserIdSampler({
|
|
311
|
-
baselineSampleRate: 0,
|
|
312
|
-
extractUserId,
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
sampler.addAlwaysSampleUsers('user_1', 'user_2');
|
|
316
|
-
|
|
317
|
-
const user1Context: SamplingContext = {
|
|
318
|
-
operationName: 'test.operation',
|
|
319
|
-
args: [{ userId: 'user_1' }],
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
expect(sampler.shouldSample(user1Context)).toBe(true);
|
|
323
|
-
|
|
324
|
-
sampler.removeAlwaysSampleUsers('user_1');
|
|
325
|
-
expect(sampler.shouldSample(user1Context)).toBe(false);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('should fallback to random sampling when no user ID', () => {
|
|
329
|
-
const sampler = new UserIdSampler({
|
|
330
|
-
baselineSampleRate: 1,
|
|
331
|
-
extractUserId: (args) => (args[0] as { userId?: string })?.userId,
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
const noUserContext: SamplingContext = {
|
|
335
|
-
operationName: 'test.operation',
|
|
336
|
-
args: [{}],
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
expect(sampler.shouldSample(noUserContext)).toBe(true);
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
describe('CompositeSampler', () => {
|
|
344
|
-
it('should throw error with no child samplers', () => {
|
|
345
|
-
expect(() => new CompositeSampler([])).toThrow();
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('should sample if any child sampler returns true', () => {
|
|
349
|
-
const sampler = new CompositeSampler([
|
|
350
|
-
new NeverSampler(),
|
|
351
|
-
new AlwaysSampler(),
|
|
352
|
-
new NeverSampler(),
|
|
353
|
-
]);
|
|
354
|
-
|
|
355
|
-
expect(sampler.shouldSample(context)).toBe(true);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('should not sample if all child samplers return false', () => {
|
|
359
|
-
const sampler = new CompositeSampler([
|
|
360
|
-
new NeverSampler(),
|
|
361
|
-
new NeverSampler(),
|
|
362
|
-
]);
|
|
363
|
-
|
|
364
|
-
expect(sampler.shouldSample(context)).toBe(false);
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
describe('FeatureFlagSampler', () => {
|
|
369
|
-
const extractFlags = (
|
|
370
|
-
args: unknown[],
|
|
371
|
-
metadata?: Record<string, unknown>,
|
|
372
|
-
) => {
|
|
373
|
-
const firstArg = args[0] as { flags?: string[] };
|
|
374
|
-
return firstArg?.flags || (metadata?.featureFlags as string[]);
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
it('should always sample requests with monitored flags', () => {
|
|
378
|
-
const sampler = new FeatureFlagSampler({
|
|
379
|
-
baselineSampleRate: 0,
|
|
380
|
-
alwaysSampleFlags: ['new_checkout', 'experimental_ui'],
|
|
381
|
-
extractFlags,
|
|
382
|
-
logger: mockLogger,
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
const flagContext: SamplingContext = {
|
|
386
|
-
operationName: 'test.operation',
|
|
387
|
-
args: [{ flags: ['new_checkout'] }],
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
expect(sampler.shouldSample(flagContext)).toBe(true);
|
|
391
|
-
// Pino-native: (extra, message)
|
|
392
|
-
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
393
|
-
{
|
|
394
|
-
operation: 'test.operation',
|
|
395
|
-
flags: ['new_checkout'],
|
|
396
|
-
},
|
|
397
|
-
'Sampling feature flag request',
|
|
398
|
-
);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('should use baseline sampling for non-monitored flags', () => {
|
|
402
|
-
const sampler = new FeatureFlagSampler({
|
|
403
|
-
baselineSampleRate: 0,
|
|
404
|
-
alwaysSampleFlags: ['monitored_flag'],
|
|
405
|
-
extractFlags,
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
const flagContext: SamplingContext = {
|
|
409
|
-
operationName: 'test.operation',
|
|
410
|
-
args: [{ flags: ['other_flag'] }],
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
expect(sampler.shouldSample(flagContext)).toBe(false);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it('should add and remove flags', () => {
|
|
417
|
-
const sampler = new FeatureFlagSampler({
|
|
418
|
-
baselineSampleRate: 0,
|
|
419
|
-
extractFlags,
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
sampler.addAlwaysSampleFlags('flag_1', 'flag_2');
|
|
423
|
-
|
|
424
|
-
const flag1Context: SamplingContext = {
|
|
425
|
-
operationName: 'test.operation',
|
|
426
|
-
args: [{ flags: ['flag_1'] }],
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
expect(sampler.shouldSample(flag1Context)).toBe(true);
|
|
430
|
-
|
|
431
|
-
sampler.removeAlwaysSampleFlags('flag_1');
|
|
432
|
-
expect(sampler.shouldSample(flag1Context)).toBe(false);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
describe('Real-world QA in Production scenarios', () => {
|
|
437
|
-
it('should always capture failed email deliveries from article example', () => {
|
|
438
|
-
// From article: "We set up another alert that let us know if our
|
|
439
|
-
// email-sending microservice was unable to process a request"
|
|
440
|
-
const sampler = new AdaptiveSampler({
|
|
441
|
-
baselineSampleRate: 0.1, // 10% baseline
|
|
442
|
-
alwaysSampleErrors: true, // Always capture failures
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
const emailContext: SamplingContext = {
|
|
446
|
-
operationName: 'email.send',
|
|
447
|
-
args: [{ to: 'school@example.com' }],
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
sampler.shouldSample(emailContext);
|
|
451
|
-
|
|
452
|
-
// Email fails due to invalid address
|
|
453
|
-
const shouldKeep = sampler.shouldKeepTrace(emailContext, {
|
|
454
|
-
success: false,
|
|
455
|
-
duration: 100,
|
|
456
|
-
error: new Error('Invalid email address'),
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
expect(shouldKeep).toBe(true);
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it('should always trace slow job applications from article example', () => {
|
|
463
|
-
// From article: Monitor if teachers are able to submit applications
|
|
464
|
-
const sampler = new AdaptiveSampler({
|
|
465
|
-
baselineSampleRate: 0.05, // 5% baseline
|
|
466
|
-
slowThresholdMs: 2000, // Slow if > 2s
|
|
467
|
-
alwaysSampleSlow: true,
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
const applicationContext: SamplingContext = {
|
|
471
|
-
operationName: 'application.submit',
|
|
472
|
-
args: [{ jobId: '123', teacherId: '456' }],
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
sampler.shouldSample(applicationContext);
|
|
476
|
-
|
|
477
|
-
// Application takes too long
|
|
478
|
-
const shouldKeep = sampler.shouldKeepTrace(applicationContext, {
|
|
479
|
-
success: true,
|
|
480
|
-
duration: 3000, // > 2s threshold
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
expect(shouldKeep).toBe(true);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('should always trace VIP users', () => {
|
|
487
|
-
const extractUserId = (args: unknown[]) => {
|
|
488
|
-
const firstArg = args[0] as { userId?: string };
|
|
489
|
-
return firstArg?.userId;
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
const sampler = new UserIdSampler({
|
|
493
|
-
baselineSampleRate: 0.01, // 1% of normal users
|
|
494
|
-
alwaysSampleUsers: ['vip_school_123'], // Always trace VIP schools
|
|
495
|
-
extractUserId,
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
const vipContext: SamplingContext = {
|
|
499
|
-
operationName: 'application.receive',
|
|
500
|
-
args: [{ userId: 'vip_school_123' }],
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
expect(sampler.shouldSample(vipContext)).toBe(true);
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it('should always trace A/B test variants for correlation', () => {
|
|
507
|
-
const extractFlags = (args: unknown[]) => {
|
|
508
|
-
const firstArg = args[0] as { experimentFlags?: string[] };
|
|
509
|
-
return firstArg?.experimentFlags;
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
const sampler = new FeatureFlagSampler({
|
|
513
|
-
baselineSampleRate: 0.05,
|
|
514
|
-
alwaysSampleFlags: ['new_application_form'], // Always trace experiment
|
|
515
|
-
extractFlags,
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
const experimentContext: SamplingContext = {
|
|
519
|
-
operationName: 'application.submit',
|
|
520
|
-
args: [{ experimentFlags: ['new_application_form'] }],
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
expect(sampler.shouldSample(experimentContext)).toBe(true);
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
describe('AdaptiveSampler - Links-based sampling', () => {
|
|
528
|
-
let mockLogger: ILogger;
|
|
529
|
-
|
|
530
|
-
beforeEach(() => {
|
|
531
|
-
mockLogger = {
|
|
532
|
-
info: vi.fn(),
|
|
533
|
-
warn: vi.fn(),
|
|
534
|
-
error: vi.fn(),
|
|
535
|
-
debug: vi.fn(),
|
|
536
|
-
};
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Helper to create a SpanContext
|
|
540
|
-
const createSpanContext = (sampled: boolean): SpanContext => ({
|
|
541
|
-
traceId: '0af7651916cd43dd8448eb211c80319c',
|
|
542
|
-
spanId: 'b7ad6b7169203331',
|
|
543
|
-
traceFlags: sampled ? TraceFlags.SAMPLED : TraceFlags.NONE,
|
|
544
|
-
isRemote: true,
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
// Helper to create a Link
|
|
548
|
-
const createLink = (
|
|
549
|
-
sampled: boolean,
|
|
550
|
-
attributes?: Record<string, unknown>,
|
|
551
|
-
): Link => ({
|
|
552
|
-
context: createSpanContext(sampled),
|
|
553
|
-
attributes: attributes ?? {},
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it('should have linksBased disabled by default', () => {
|
|
557
|
-
const sampler = new AdaptiveSampler({
|
|
558
|
-
baselineSampleRate: 0.1,
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
const context: SamplingContext = {
|
|
562
|
-
operationName: 'test.operation',
|
|
563
|
-
args: [],
|
|
564
|
-
links: [createLink(true)], // Sampled link
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
sampler.shouldSample(context);
|
|
568
|
-
|
|
569
|
-
// With linksBased=false (default), sampled links don't affect decision
|
|
570
|
-
// With baselineSampleRate=0.1 and random chance, we can't deterministically test
|
|
571
|
-
// So we test with 0% baseline - the link should NOT cause it to be kept
|
|
572
|
-
const samplerStrict = new AdaptiveSampler({
|
|
573
|
-
baselineSampleRate: 0,
|
|
574
|
-
linksBased: false, // Explicitly disabled
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
samplerStrict.shouldSample(context);
|
|
578
|
-
const shouldKeep = samplerStrict.shouldKeepTrace(context, {
|
|
579
|
-
success: true,
|
|
580
|
-
duration: 100,
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
expect(shouldKeep).toBe(false);
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
it('should throw error for invalid linksRate', () => {
|
|
587
|
-
expect(
|
|
588
|
-
() =>
|
|
589
|
-
new AdaptiveSampler({
|
|
590
|
-
linksRate: -0.1,
|
|
591
|
-
}),
|
|
592
|
-
).toThrow('Links rate must be between 0 and 1');
|
|
593
|
-
|
|
594
|
-
expect(
|
|
595
|
-
() =>
|
|
596
|
-
new AdaptiveSampler({
|
|
597
|
-
linksRate: 1.5,
|
|
598
|
-
}),
|
|
599
|
-
).toThrow('Links rate must be between 0 and 1');
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it('should keep traces with sampled links when linksBased=true', () => {
|
|
603
|
-
const sampler = new AdaptiveSampler({
|
|
604
|
-
baselineSampleRate: 0, // 0% baseline
|
|
605
|
-
linksBased: true,
|
|
606
|
-
linksRate: 1, // 100% of linked spans kept
|
|
607
|
-
logger: mockLogger,
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
const context: SamplingContext = {
|
|
611
|
-
operationName: 'consumer.process',
|
|
612
|
-
args: [],
|
|
613
|
-
links: [createLink(true)], // Sampled link
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
sampler.shouldSample(context);
|
|
617
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
618
|
-
success: true,
|
|
619
|
-
duration: 100,
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
expect(shouldKeep).toBe(true);
|
|
623
|
-
// Pino-native: (extra, message)
|
|
624
|
-
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
625
|
-
expect.objectContaining({
|
|
626
|
-
operation: 'consumer.process',
|
|
627
|
-
linkCount: 1,
|
|
628
|
-
}),
|
|
629
|
-
'Adaptive sampling: Keeping trace due to sampled link',
|
|
630
|
-
);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it('should drop traces with unsampled links when linksBased=true', () => {
|
|
634
|
-
const sampler = new AdaptiveSampler({
|
|
635
|
-
baselineSampleRate: 0,
|
|
636
|
-
linksBased: true,
|
|
637
|
-
linksRate: 1,
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
const context: SamplingContext = {
|
|
641
|
-
operationName: 'consumer.process',
|
|
642
|
-
args: [],
|
|
643
|
-
links: [createLink(false)], // NOT sampled
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
sampler.shouldSample(context);
|
|
647
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
648
|
-
success: true,
|
|
649
|
-
duration: 100,
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
expect(shouldKeep).toBe(false);
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it('should keep trace if ANY link is sampled (fan-in)', () => {
|
|
656
|
-
const sampler = new AdaptiveSampler({
|
|
657
|
-
baselineSampleRate: 0,
|
|
658
|
-
linksBased: true,
|
|
659
|
-
linksRate: 1,
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
const context: SamplingContext = {
|
|
663
|
-
operationName: 'batch.process',
|
|
664
|
-
args: [],
|
|
665
|
-
links: [
|
|
666
|
-
createLink(false), // Not sampled
|
|
667
|
-
createLink(true), // Sampled
|
|
668
|
-
createLink(false), // Not sampled
|
|
669
|
-
],
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
sampler.shouldSample(context);
|
|
673
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
674
|
-
success: true,
|
|
675
|
-
duration: 100,
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
expect(shouldKeep).toBe(true);
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
it('should respect linksRate for probabilistic link sampling', () => {
|
|
682
|
-
const sampler = new AdaptiveSampler({
|
|
683
|
-
baselineSampleRate: 0,
|
|
684
|
-
linksBased: true,
|
|
685
|
-
linksRate: 0, // 0% - never keep linked spans
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
const context: SamplingContext = {
|
|
689
|
-
operationName: 'consumer.process',
|
|
690
|
-
args: [],
|
|
691
|
-
links: [createLink(true)],
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
sampler.shouldSample(context);
|
|
695
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
696
|
-
success: true,
|
|
697
|
-
duration: 100,
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
expect(shouldKeep).toBe(false);
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
it('should prioritize errors over links-based sampling', () => {
|
|
704
|
-
const sampler = new AdaptiveSampler({
|
|
705
|
-
baselineSampleRate: 0,
|
|
706
|
-
linksBased: true,
|
|
707
|
-
linksRate: 0, // Would drop linked spans
|
|
708
|
-
alwaysSampleErrors: true,
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
const context: SamplingContext = {
|
|
712
|
-
operationName: 'consumer.process',
|
|
713
|
-
args: [],
|
|
714
|
-
links: [createLink(false)], // No sampled links
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
sampler.shouldSample(context);
|
|
718
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
719
|
-
success: false, // Error!
|
|
720
|
-
duration: 100,
|
|
721
|
-
error: new Error('Processing failed'),
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
expect(shouldKeep).toBe(true); // Errors always kept
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
it('should handle empty links array', () => {
|
|
728
|
-
const sampler = new AdaptiveSampler({
|
|
729
|
-
baselineSampleRate: 0,
|
|
730
|
-
linksBased: true,
|
|
731
|
-
linksRate: 1,
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
const context: SamplingContext = {
|
|
735
|
-
operationName: 'test.operation',
|
|
736
|
-
args: [],
|
|
737
|
-
links: [],
|
|
738
|
-
};
|
|
739
|
-
|
|
740
|
-
sampler.shouldSample(context);
|
|
741
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
742
|
-
success: true,
|
|
743
|
-
duration: 100,
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
expect(shouldKeep).toBe(false); // No links, baseline=0
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
it('should handle undefined links', () => {
|
|
750
|
-
const sampler = new AdaptiveSampler({
|
|
751
|
-
baselineSampleRate: 0,
|
|
752
|
-
linksBased: true,
|
|
753
|
-
linksRate: 1,
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
const context: SamplingContext = {
|
|
757
|
-
operationName: 'test.operation',
|
|
758
|
-
args: [],
|
|
759
|
-
// links is undefined
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
sampler.shouldSample(context);
|
|
763
|
-
const shouldKeep = sampler.shouldKeepTrace(context, {
|
|
764
|
-
success: true,
|
|
765
|
-
duration: 100,
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
expect(shouldKeep).toBe(false);
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
describe('hasSampledLink helper', () => {
|
|
772
|
-
it('should return false for empty links', () => {
|
|
773
|
-
const sampler = new AdaptiveSampler({ linksBased: true });
|
|
774
|
-
expect(sampler.hasSampledLink([])).toBe(false);
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
it('should return true if any link is sampled', () => {
|
|
778
|
-
const sampler = new AdaptiveSampler({ linksBased: true });
|
|
779
|
-
const links = [createLink(false), createLink(true)];
|
|
780
|
-
expect(sampler.hasSampledLink(links)).toBe(true);
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
it('should return false if no links are sampled', () => {
|
|
784
|
-
const sampler = new AdaptiveSampler({ linksBased: true });
|
|
785
|
-
const links = [createLink(false), createLink(false)];
|
|
786
|
-
expect(sampler.hasSampledLink(links)).toBe(false);
|
|
787
|
-
});
|
|
788
|
-
});
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
describe('Link Helper Functions', () => {
|
|
792
|
-
describe('createLinkFromHeaders', () => {
|
|
793
|
-
it('should create link from valid W3C traceparent header', () => {
|
|
794
|
-
const headers = {
|
|
795
|
-
traceparent:
|
|
796
|
-
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
|
|
797
|
-
};
|
|
798
|
-
|
|
799
|
-
const link = createLinkFromHeaders(headers);
|
|
800
|
-
|
|
801
|
-
expect(link).not.toBeNull();
|
|
802
|
-
expect(link?.context.traceId).toBe('0af7651916cd43dd8448eb211c80319c');
|
|
803
|
-
expect(link?.context.spanId).toBe('b7ad6b7169203331');
|
|
804
|
-
expect(link?.context.traceFlags).toBe(TraceFlags.SAMPLED);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
it('should create link from unsampled traceparent', () => {
|
|
808
|
-
const headers = {
|
|
809
|
-
traceparent:
|
|
810
|
-
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00',
|
|
811
|
-
};
|
|
812
|
-
|
|
813
|
-
const link = createLinkFromHeaders(headers);
|
|
814
|
-
|
|
815
|
-
expect(link).not.toBeNull();
|
|
816
|
-
expect(link?.context.traceFlags).toBe(TraceFlags.NONE);
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
it('should include custom attributes in link', () => {
|
|
820
|
-
const headers = {
|
|
821
|
-
traceparent:
|
|
822
|
-
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
|
|
823
|
-
};
|
|
824
|
-
const attributes = {
|
|
825
|
-
'messaging.system': 'kafka',
|
|
826
|
-
'custom.attr': 'value',
|
|
827
|
-
};
|
|
828
|
-
|
|
829
|
-
const link = createLinkFromHeaders(headers, attributes);
|
|
830
|
-
|
|
831
|
-
expect(link?.attributes).toEqual(attributes);
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
it('should return null for invalid/missing traceparent', () => {
|
|
835
|
-
expect(createLinkFromHeaders({})).toBeNull();
|
|
836
|
-
expect(createLinkFromHeaders({ traceparent: 'invalid' })).toBeNull();
|
|
837
|
-
expect(createLinkFromHeaders({ traceparent: '' })).toBeNull();
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
it('should return null for all-zero trace IDs', () => {
|
|
841
|
-
// All zeros is an invalid trace context
|
|
842
|
-
const headers = {
|
|
843
|
-
traceparent:
|
|
844
|
-
'00-00000000000000000000000000000000-0000000000000000-01',
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
const link = createLinkFromHeaders(headers);
|
|
848
|
-
expect(link).toBeNull();
|
|
849
|
-
});
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
describe('extractLinksFromBatch', () => {
|
|
853
|
-
it('should extract links from batch of messages', () => {
|
|
854
|
-
const messages = [
|
|
855
|
-
{
|
|
856
|
-
body: 'message1',
|
|
857
|
-
headers: {
|
|
858
|
-
traceparent:
|
|
859
|
-
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
|
|
860
|
-
},
|
|
861
|
-
},
|
|
862
|
-
{
|
|
863
|
-
body: 'message2',
|
|
864
|
-
headers: {
|
|
865
|
-
traceparent:
|
|
866
|
-
'00-1af7651916cd43dd8448eb211c80319c-c7ad6b7169203331-01',
|
|
867
|
-
},
|
|
868
|
-
},
|
|
869
|
-
];
|
|
870
|
-
|
|
871
|
-
const links = extractLinksFromBatch(messages);
|
|
872
|
-
|
|
873
|
-
expect(links).toHaveLength(2);
|
|
874
|
-
expect(links[0].context.traceId).toBe(
|
|
875
|
-
'0af7651916cd43dd8448eb211c80319c',
|
|
876
|
-
);
|
|
877
|
-
expect(links[1].context.traceId).toBe(
|
|
878
|
-
'1af7651916cd43dd8448eb211c80319c',
|
|
879
|
-
);
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
it('should add message index as link attribute', () => {
|
|
883
|
-
const messages = [
|
|
884
|
-
{
|
|
885
|
-
body: 'message1',
|
|
886
|
-
headers: {
|
|
887
|
-
traceparent:
|
|
888
|
-
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
|
|
889
|
-
},
|
|
890
|
-
},
|
|
891
|
-
{
|
|
892
|
-
body: 'message2',
|
|
893
|
-
headers: {
|
|
894
|
-
traceparent:
|
|
895
|
-
'00-1af7651916cd43dd8448eb211c80319c-c7ad6b7169203331-01',
|
|
896
|
-
},
|
|
897
|
-
},
|
|
898
|
-
];
|
|
899
|
-
|
|
900
|
-
const links = extractLinksFromBatch(messages);
|
|
901
|
-
|
|
902
|
-
expect(links[0].attributes?.['messaging.batch.message_index']).toBe(0);
|
|
903
|
-
expect(links[1].attributes?.['messaging.batch.message_index']).toBe(1);
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
it('should use custom headers key', () => {
|
|
907
|
-
const messages = [
|
|
908
|
-
{
|
|
909
|
-
body: 'message1',
|
|
910
|
-
metadata: {
|
|
911
|
-
traceparent:
|
|
912
|
-
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
|
|
913
|
-
},
|
|
914
|
-
},
|
|
915
|
-
];
|
|
916
|
-
|
|
917
|
-
const links = extractLinksFromBatch(messages, 'metadata');
|
|
918
|
-
|
|
919
|
-
expect(links).toHaveLength(1);
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
it('should skip messages without headers', () => {
|
|
923
|
-
const messages = [
|
|
924
|
-
{ body: 'message1' }, // No headers
|
|
925
|
-
{
|
|
926
|
-
body: 'message2',
|
|
927
|
-
headers: {
|
|
928
|
-
traceparent:
|
|
929
|
-
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
|
|
930
|
-
},
|
|
931
|
-
},
|
|
932
|
-
{ body: 'message3', headers: null }, // Null headers
|
|
933
|
-
];
|
|
934
|
-
|
|
935
|
-
const links = extractLinksFromBatch(messages);
|
|
936
|
-
|
|
937
|
-
expect(links).toHaveLength(1);
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
it('should skip messages with invalid trace context', () => {
|
|
941
|
-
const messages = [
|
|
942
|
-
{
|
|
943
|
-
body: 'message1',
|
|
944
|
-
headers: { traceparent: 'invalid-traceparent' },
|
|
945
|
-
},
|
|
946
|
-
{
|
|
947
|
-
body: 'message2',
|
|
948
|
-
headers: {
|
|
949
|
-
traceparent:
|
|
950
|
-
'00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
|
|
951
|
-
},
|
|
952
|
-
},
|
|
953
|
-
];
|
|
954
|
-
|
|
955
|
-
const links = extractLinksFromBatch(messages);
|
|
956
|
-
|
|
957
|
-
expect(links).toHaveLength(1);
|
|
958
|
-
expect(links[0].context.traceId).toBe(
|
|
959
|
-
'0af7651916cd43dd8448eb211c80319c',
|
|
960
|
-
);
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
it('should return empty array for empty batch', () => {
|
|
964
|
-
const links = extractLinksFromBatch([]);
|
|
965
|
-
expect(links).toEqual([]);
|
|
966
|
-
});
|
|
967
|
-
});
|
|
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
|
-
});
|
|
1060
|
-
});
|