autotel 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1946 -0
  3. package/dist/chunk-2LNRY4QK.js +273 -0
  4. package/dist/chunk-2LNRY4QK.js.map +1 -0
  5. package/dist/chunk-3HENGDW2.js +587 -0
  6. package/dist/chunk-3HENGDW2.js.map +1 -0
  7. package/dist/chunk-4OAT42CA.cjs +73 -0
  8. package/dist/chunk-4OAT42CA.cjs.map +1 -0
  9. package/dist/chunk-5GWX5LFW.js +70 -0
  10. package/dist/chunk-5GWX5LFW.js.map +1 -0
  11. package/dist/chunk-5R2M36QB.js +195 -0
  12. package/dist/chunk-5R2M36QB.js.map +1 -0
  13. package/dist/chunk-5ZN622AO.js +73 -0
  14. package/dist/chunk-5ZN622AO.js.map +1 -0
  15. package/dist/chunk-77MSMAUQ.cjs +498 -0
  16. package/dist/chunk-77MSMAUQ.cjs.map +1 -0
  17. package/dist/chunk-ABPEQ6RK.cjs +596 -0
  18. package/dist/chunk-ABPEQ6RK.cjs.map +1 -0
  19. package/dist/chunk-BWYGJKRB.js +95 -0
  20. package/dist/chunk-BWYGJKRB.js.map +1 -0
  21. package/dist/chunk-BZHG5IZ4.js +73 -0
  22. package/dist/chunk-BZHG5IZ4.js.map +1 -0
  23. package/dist/chunk-G7VZBCD6.cjs +35 -0
  24. package/dist/chunk-G7VZBCD6.cjs.map +1 -0
  25. package/dist/chunk-GVLK7YUU.cjs +30 -0
  26. package/dist/chunk-GVLK7YUU.cjs.map +1 -0
  27. package/dist/chunk-HCCXC7XG.js +205 -0
  28. package/dist/chunk-HCCXC7XG.js.map +1 -0
  29. package/dist/chunk-HE6T6FIX.cjs +203 -0
  30. package/dist/chunk-HE6T6FIX.cjs.map +1 -0
  31. package/dist/chunk-KIXWPOCO.cjs +100 -0
  32. package/dist/chunk-KIXWPOCO.cjs.map +1 -0
  33. package/dist/chunk-KVGNW3FC.js +87 -0
  34. package/dist/chunk-KVGNW3FC.js.map +1 -0
  35. package/dist/chunk-LITNXTTT.js +3 -0
  36. package/dist/chunk-LITNXTTT.js.map +1 -0
  37. package/dist/chunk-M4ANN7RL.js +114 -0
  38. package/dist/chunk-M4ANN7RL.js.map +1 -0
  39. package/dist/chunk-NC52UBR2.cjs +32 -0
  40. package/dist/chunk-NC52UBR2.cjs.map +1 -0
  41. package/dist/chunk-NHCNRQD3.cjs +212 -0
  42. package/dist/chunk-NHCNRQD3.cjs.map +1 -0
  43. package/dist/chunk-NZ72VDNY.cjs +4 -0
  44. package/dist/chunk-NZ72VDNY.cjs.map +1 -0
  45. package/dist/chunk-P6JUDYNO.js +57 -0
  46. package/dist/chunk-P6JUDYNO.js.map +1 -0
  47. package/dist/chunk-RJYY7BWX.js +1349 -0
  48. package/dist/chunk-RJYY7BWX.js.map +1 -0
  49. package/dist/chunk-TRI4V5BF.cjs +126 -0
  50. package/dist/chunk-TRI4V5BF.cjs.map +1 -0
  51. package/dist/chunk-UL33I6IS.js +139 -0
  52. package/dist/chunk-UL33I6IS.js.map +1 -0
  53. package/dist/chunk-URRW6M2C.cjs +61 -0
  54. package/dist/chunk-URRW6M2C.cjs.map +1 -0
  55. package/dist/chunk-UY3UYPBZ.cjs +77 -0
  56. package/dist/chunk-UY3UYPBZ.cjs.map +1 -0
  57. package/dist/chunk-W3253FGB.cjs +277 -0
  58. package/dist/chunk-W3253FGB.cjs.map +1 -0
  59. package/dist/chunk-W7LHZVQF.js +26 -0
  60. package/dist/chunk-W7LHZVQF.js.map +1 -0
  61. package/dist/chunk-WBWNM6LB.cjs +1360 -0
  62. package/dist/chunk-WBWNM6LB.cjs.map +1 -0
  63. package/dist/chunk-WFJ7L2RV.js +494 -0
  64. package/dist/chunk-WFJ7L2RV.js.map +1 -0
  65. package/dist/chunk-X4RMFFMR.js +28 -0
  66. package/dist/chunk-X4RMFFMR.js.map +1 -0
  67. package/dist/chunk-Y4Y2S7BM.cjs +92 -0
  68. package/dist/chunk-Y4Y2S7BM.cjs.map +1 -0
  69. package/dist/chunk-YLPNXZFI.cjs +143 -0
  70. package/dist/chunk-YLPNXZFI.cjs.map +1 -0
  71. package/dist/chunk-YTXEZ4SD.cjs +77 -0
  72. package/dist/chunk-YTXEZ4SD.cjs.map +1 -0
  73. package/dist/chunk-Z6ZWNWWR.js +30 -0
  74. package/dist/chunk-Z6ZWNWWR.js.map +1 -0
  75. package/dist/config.cjs +26 -0
  76. package/dist/config.cjs.map +1 -0
  77. package/dist/config.d.cts +75 -0
  78. package/dist/config.d.ts +75 -0
  79. package/dist/config.js +5 -0
  80. package/dist/config.js.map +1 -0
  81. package/dist/db.cjs +233 -0
  82. package/dist/db.cjs.map +1 -0
  83. package/dist/db.d.cts +123 -0
  84. package/dist/db.d.ts +123 -0
  85. package/dist/db.js +228 -0
  86. package/dist/db.js.map +1 -0
  87. package/dist/decorators.cjs +67 -0
  88. package/dist/decorators.cjs.map +1 -0
  89. package/dist/decorators.d.cts +91 -0
  90. package/dist/decorators.d.ts +91 -0
  91. package/dist/decorators.js +65 -0
  92. package/dist/decorators.js.map +1 -0
  93. package/dist/event-subscriber.cjs +6 -0
  94. package/dist/event-subscriber.cjs.map +1 -0
  95. package/dist/event-subscriber.d.cts +116 -0
  96. package/dist/event-subscriber.d.ts +116 -0
  97. package/dist/event-subscriber.js +3 -0
  98. package/dist/event-subscriber.js.map +1 -0
  99. package/dist/event-testing.cjs +21 -0
  100. package/dist/event-testing.cjs.map +1 -0
  101. package/dist/event-testing.d.cts +110 -0
  102. package/dist/event-testing.d.ts +110 -0
  103. package/dist/event-testing.js +4 -0
  104. package/dist/event-testing.js.map +1 -0
  105. package/dist/event.cjs +30 -0
  106. package/dist/event.cjs.map +1 -0
  107. package/dist/event.d.cts +282 -0
  108. package/dist/event.d.ts +282 -0
  109. package/dist/event.js +13 -0
  110. package/dist/event.js.map +1 -0
  111. package/dist/exporters.cjs +17 -0
  112. package/dist/exporters.cjs.map +1 -0
  113. package/dist/exporters.d.cts +1 -0
  114. package/dist/exporters.d.ts +1 -0
  115. package/dist/exporters.js +4 -0
  116. package/dist/exporters.js.map +1 -0
  117. package/dist/functional.cjs +46 -0
  118. package/dist/functional.cjs.map +1 -0
  119. package/dist/functional.d.cts +478 -0
  120. package/dist/functional.d.ts +478 -0
  121. package/dist/functional.js +13 -0
  122. package/dist/functional.js.map +1 -0
  123. package/dist/http.cjs +189 -0
  124. package/dist/http.cjs.map +1 -0
  125. package/dist/http.d.cts +169 -0
  126. package/dist/http.d.ts +169 -0
  127. package/dist/http.js +184 -0
  128. package/dist/http.js.map +1 -0
  129. package/dist/index.cjs +333 -0
  130. package/dist/index.cjs.map +1 -0
  131. package/dist/index.d.cts +758 -0
  132. package/dist/index.d.ts +758 -0
  133. package/dist/index.js +143 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/instrumentation.cjs +182 -0
  136. package/dist/instrumentation.cjs.map +1 -0
  137. package/dist/instrumentation.d.cts +49 -0
  138. package/dist/instrumentation.d.ts +49 -0
  139. package/dist/instrumentation.js +179 -0
  140. package/dist/instrumentation.js.map +1 -0
  141. package/dist/logger.cjs +19 -0
  142. package/dist/logger.cjs.map +1 -0
  143. package/dist/logger.d.cts +146 -0
  144. package/dist/logger.d.ts +146 -0
  145. package/dist/logger.js +6 -0
  146. package/dist/logger.js.map +1 -0
  147. package/dist/metric-helpers.cjs +31 -0
  148. package/dist/metric-helpers.cjs.map +1 -0
  149. package/dist/metric-helpers.d.cts +13 -0
  150. package/dist/metric-helpers.d.ts +13 -0
  151. package/dist/metric-helpers.js +6 -0
  152. package/dist/metric-helpers.js.map +1 -0
  153. package/dist/metric-testing.cjs +21 -0
  154. package/dist/metric-testing.cjs.map +1 -0
  155. package/dist/metric-testing.d.cts +110 -0
  156. package/dist/metric-testing.d.ts +110 -0
  157. package/dist/metric-testing.js +4 -0
  158. package/dist/metric-testing.js.map +1 -0
  159. package/dist/metric.cjs +26 -0
  160. package/dist/metric.cjs.map +1 -0
  161. package/dist/metric.d.cts +240 -0
  162. package/dist/metric.d.ts +240 -0
  163. package/dist/metric.js +9 -0
  164. package/dist/metric.js.map +1 -0
  165. package/dist/processors.cjs +17 -0
  166. package/dist/processors.cjs.map +1 -0
  167. package/dist/processors.d.cts +1 -0
  168. package/dist/processors.d.ts +1 -0
  169. package/dist/processors.js +4 -0
  170. package/dist/processors.js.map +1 -0
  171. package/dist/sampling.cjs +40 -0
  172. package/dist/sampling.cjs.map +1 -0
  173. package/dist/sampling.d.cts +260 -0
  174. package/dist/sampling.d.ts +260 -0
  175. package/dist/sampling.js +7 -0
  176. package/dist/sampling.js.map +1 -0
  177. package/dist/semantic-helpers.cjs +35 -0
  178. package/dist/semantic-helpers.cjs.map +1 -0
  179. package/dist/semantic-helpers.d.cts +442 -0
  180. package/dist/semantic-helpers.d.ts +442 -0
  181. package/dist/semantic-helpers.js +14 -0
  182. package/dist/semantic-helpers.js.map +1 -0
  183. package/dist/tail-sampling-processor.cjs +13 -0
  184. package/dist/tail-sampling-processor.cjs.map +1 -0
  185. package/dist/tail-sampling-processor.d.cts +27 -0
  186. package/dist/tail-sampling-processor.d.ts +27 -0
  187. package/dist/tail-sampling-processor.js +4 -0
  188. package/dist/tail-sampling-processor.js.map +1 -0
  189. package/dist/testing.cjs +286 -0
  190. package/dist/testing.cjs.map +1 -0
  191. package/dist/testing.d.cts +291 -0
  192. package/dist/testing.d.ts +291 -0
  193. package/dist/testing.js +263 -0
  194. package/dist/testing.js.map +1 -0
  195. package/dist/trace-context-DRZdUvVY.d.cts +181 -0
  196. package/dist/trace-context-DRZdUvVY.d.ts +181 -0
  197. package/dist/trace-helpers.cjs +54 -0
  198. package/dist/trace-helpers.cjs.map +1 -0
  199. package/dist/trace-helpers.d.cts +524 -0
  200. package/dist/trace-helpers.d.ts +524 -0
  201. package/dist/trace-helpers.js +5 -0
  202. package/dist/trace-helpers.js.map +1 -0
  203. package/dist/tracer-provider.cjs +21 -0
  204. package/dist/tracer-provider.cjs.map +1 -0
  205. package/dist/tracer-provider.d.cts +169 -0
  206. package/dist/tracer-provider.d.ts +169 -0
  207. package/dist/tracer-provider.js +4 -0
  208. package/dist/tracer-provider.js.map +1 -0
  209. package/package.json +280 -0
  210. package/src/baggage-span-processor.test.ts +202 -0
  211. package/src/baggage-span-processor.ts +98 -0
  212. package/src/circuit-breaker.test.ts +341 -0
  213. package/src/circuit-breaker.ts +184 -0
  214. package/src/config.test.ts +94 -0
  215. package/src/config.ts +169 -0
  216. package/src/db.test.ts +252 -0
  217. package/src/db.ts +447 -0
  218. package/src/decorators.test.ts +203 -0
  219. package/src/decorators.ts +188 -0
  220. package/src/env-config.test.ts +246 -0
  221. package/src/env-config.ts +158 -0
  222. package/src/event-queue.test.ts +222 -0
  223. package/src/event-queue.ts +203 -0
  224. package/src/event-subscriber.ts +136 -0
  225. package/src/event-testing.ts +197 -0
  226. package/src/event.test.ts +718 -0
  227. package/src/event.ts +556 -0
  228. package/src/exporters.ts +96 -0
  229. package/src/functional.test.ts +1059 -0
  230. package/src/functional.ts +2295 -0
  231. package/src/http.test.ts +487 -0
  232. package/src/http.ts +424 -0
  233. package/src/index.ts +158 -0
  234. package/src/init.customization.test.ts +210 -0
  235. package/src/init.integrations.test.ts +366 -0
  236. package/src/init.openllmetry.test.ts +282 -0
  237. package/src/init.protocol.test.ts +215 -0
  238. package/src/init.ts +1426 -0
  239. package/src/instrumentation.test.ts +108 -0
  240. package/src/instrumentation.ts +308 -0
  241. package/src/logger.test.ts +117 -0
  242. package/src/logger.ts +246 -0
  243. package/src/metric-helpers.ts +47 -0
  244. package/src/metric-testing.ts +197 -0
  245. package/src/metric.ts +434 -0
  246. package/src/metrics.test.ts +205 -0
  247. package/src/operation-context.ts +93 -0
  248. package/src/processors.ts +106 -0
  249. package/src/rate-limiter.test.ts +199 -0
  250. package/src/rate-limiter.ts +98 -0
  251. package/src/sampling.test.ts +513 -0
  252. package/src/sampling.ts +428 -0
  253. package/src/semantic-helpers.test.ts +311 -0
  254. package/src/semantic-helpers.ts +584 -0
  255. package/src/shutdown.test.ts +311 -0
  256. package/src/shutdown.ts +222 -0
  257. package/src/stub.integration.test.ts +361 -0
  258. package/src/tail-sampling-processor.test.ts +226 -0
  259. package/src/tail-sampling-processor.ts +51 -0
  260. package/src/testing.ts +670 -0
  261. package/src/trace-context.ts +470 -0
  262. package/src/trace-helpers.new.test.ts +278 -0
  263. package/src/trace-helpers.test.ts +242 -0
  264. package/src/trace-helpers.ts +690 -0
  265. package/src/tracer-provider.test.ts +183 -0
  266. package/src/tracer-provider.ts +266 -0
  267. package/src/track.test.ts +153 -0
  268. package/src/track.ts +120 -0
  269. package/src/validation.test.ts +306 -0
  270. package/src/validation.ts +239 -0
  271. package/src/variable-name-inference.test.ts +178 -0
  272. package/src/variable-name-inference.ts +242 -0
@@ -0,0 +1,513 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import {
3
+ RandomSampler,
4
+ AlwaysSampler,
5
+ NeverSampler,
6
+ AdaptiveSampler,
7
+ UserIdSampler,
8
+ CompositeSampler,
9
+ FeatureFlagSampler,
10
+ type SamplingContext,
11
+ } from './sampling';
12
+ import { type ILogger } from './logger';
13
+
14
+ describe('Sampling', () => {
15
+ let mockLogger: ILogger;
16
+ let context: SamplingContext;
17
+
18
+ beforeEach(() => {
19
+ mockLogger = {
20
+ info: vi.fn(),
21
+ warn: vi.fn(),
22
+ error: vi.fn(),
23
+ debug: vi.fn(),
24
+ };
25
+
26
+ context = {
27
+ operationName: 'test.operation',
28
+ args: [{ userId: '123', email: 'test@example.com' }],
29
+ };
30
+ });
31
+
32
+ describe('RandomSampler', () => {
33
+ it('should throw error for invalid sample rates', () => {
34
+ expect(() => new RandomSampler(-0.1)).toThrow();
35
+ expect(() => new RandomSampler(1.1)).toThrow();
36
+ });
37
+
38
+ it('should sample at 100%', () => {
39
+ const sampler = new RandomSampler(1);
40
+ const results = Array.from({ length: 100 }, () =>
41
+ sampler.shouldSample(context),
42
+ );
43
+
44
+ expect(results.every((r) => r === true)).toBe(true);
45
+ });
46
+
47
+ it('should never sample at 0%', () => {
48
+ const sampler = new RandomSampler(0);
49
+ const results = Array.from({ length: 100 }, () =>
50
+ sampler.shouldSample(context),
51
+ );
52
+
53
+ expect(results.every((r) => r === false)).toBe(true);
54
+ });
55
+
56
+ it('should sample approximately at the specified rate', () => {
57
+ const sampler = new RandomSampler(0.5);
58
+ const results = Array.from({ length: 1000 }, () =>
59
+ sampler.shouldSample(context),
60
+ );
61
+
62
+ const sampleCount = results.filter(Boolean).length;
63
+ // Allow 10% margin of error
64
+ expect(sampleCount).toBeGreaterThan(450);
65
+ expect(sampleCount).toBeLessThan(550);
66
+ });
67
+ });
68
+
69
+ describe('AlwaysSampler', () => {
70
+ it('should always sample', () => {
71
+ const sampler = new AlwaysSampler();
72
+ const results = Array.from({ length: 100 }, () =>
73
+ sampler.shouldSample(context),
74
+ );
75
+
76
+ expect(results.every((r) => r === true)).toBe(true);
77
+ });
78
+ });
79
+
80
+ describe('NeverSampler', () => {
81
+ it('should never sample', () => {
82
+ const sampler = new NeverSampler();
83
+ const results = Array.from({ length: 100 }, () =>
84
+ sampler.shouldSample(context),
85
+ );
86
+
87
+ expect(results.every((r) => r === false)).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe('AdaptiveSampler', () => {
92
+ it('should throw error for invalid baseline sample rate', () => {
93
+ expect(
94
+ () =>
95
+ new AdaptiveSampler({
96
+ baselineSampleRate: -0.1,
97
+ }),
98
+ ).toThrow();
99
+ });
100
+
101
+ it('should indicate it needs tail sampling', () => {
102
+ const sampler = new AdaptiveSampler({
103
+ baselineSampleRate: 0.1,
104
+ });
105
+
106
+ expect(sampler.needsTailSampling()).toBe(true);
107
+ });
108
+
109
+ it('should always create spans (optimistic sampling)', () => {
110
+ const sampler = new AdaptiveSampler({
111
+ baselineSampleRate: 0, // Even with 0% baseline
112
+ logger: mockLogger,
113
+ });
114
+
115
+ // FIX: shouldSample now always returns true for tail sampling
116
+ const result = sampler.shouldSample(context);
117
+ expect(result).toBe(true);
118
+ });
119
+
120
+ it('should keep error traces even when baseline would drop them', () => {
121
+ const sampler = new AdaptiveSampler({
122
+ baselineSampleRate: 0, // 0% baseline sampling
123
+ alwaysSampleErrors: true,
124
+ logger: mockLogger,
125
+ });
126
+
127
+ // FIX: Span is created (shouldSample returns true)
128
+ const shouldSample = sampler.shouldSample(context);
129
+ expect(shouldSample).toBe(true);
130
+
131
+ // Tail sampling keeps error traces
132
+ const shouldKeep = sampler.shouldKeepTrace(context, {
133
+ success: false,
134
+ duration: 100,
135
+ error: new Error('Test error'),
136
+ });
137
+
138
+ expect(shouldKeep).toBe(true);
139
+ expect(mockLogger.debug).toHaveBeenCalledWith(
140
+ 'Adaptive sampling: Keeping error trace',
141
+ expect.objectContaining({
142
+ operation: 'test.operation',
143
+ error: 'Test error',
144
+ }),
145
+ );
146
+ });
147
+
148
+ it('should keep slow traces even when baseline would drop them', () => {
149
+ const sampler = new AdaptiveSampler({
150
+ baselineSampleRate: 0, // 0% baseline sampling
151
+ slowThresholdMs: 1000,
152
+ alwaysSampleSlow: true,
153
+ logger: mockLogger,
154
+ });
155
+
156
+ // FIX: Span is created (shouldSample returns true)
157
+ const shouldSample = sampler.shouldSample(context);
158
+ expect(shouldSample).toBe(true);
159
+
160
+ // Tail sampling keeps slow traces
161
+ const shouldKeep = sampler.shouldKeepTrace(context, {
162
+ success: true,
163
+ duration: 1500, // > 1000ms threshold
164
+ });
165
+
166
+ expect(shouldKeep).toBe(true);
167
+ expect(mockLogger.debug).toHaveBeenCalledWith(
168
+ 'Adaptive sampling: Keeping slow trace',
169
+ expect.objectContaining({
170
+ operation: 'test.operation',
171
+ duration: 1500,
172
+ }),
173
+ );
174
+ });
175
+
176
+ it('should drop fast successful traces when baseline sampling says no', () => {
177
+ const sampler = new AdaptiveSampler({
178
+ baselineSampleRate: 0, // 0% baseline
179
+ slowThresholdMs: 1000,
180
+ logger: mockLogger,
181
+ });
182
+
183
+ // Span is created optimistically
184
+ const shouldSample = sampler.shouldSample(context);
185
+ expect(shouldSample).toBe(true);
186
+
187
+ // Tail sampling drops fast/successful traces
188
+ const shouldKeep = sampler.shouldKeepTrace(context, {
189
+ success: true,
190
+ duration: 100, // < 1000ms threshold
191
+ });
192
+
193
+ expect(shouldKeep).toBe(false);
194
+ });
195
+
196
+ it('should keep fast successful traces when baseline sampling says yes', () => {
197
+ const sampler = new AdaptiveSampler({
198
+ baselineSampleRate: 1, // 100% baseline
199
+ slowThresholdMs: 1000,
200
+ logger: mockLogger,
201
+ });
202
+
203
+ const shouldSample = sampler.shouldSample(context);
204
+ expect(shouldSample).toBe(true);
205
+
206
+ // Baseline sampled it, so keep it
207
+ const shouldKeep = sampler.shouldKeepTrace(context, {
208
+ success: true,
209
+ duration: 100,
210
+ });
211
+
212
+ expect(shouldKeep).toBe(true);
213
+ });
214
+
215
+ it('should respect alwaysSampleErrors flag', () => {
216
+ const sampler = new AdaptiveSampler({
217
+ baselineSampleRate: 0,
218
+ alwaysSampleErrors: false, // Don't force-sample errors
219
+ logger: mockLogger,
220
+ });
221
+
222
+ const shouldKeep = sampler.shouldKeepTrace(context, {
223
+ success: false,
224
+ duration: 100,
225
+ error: new Error('Test error'),
226
+ });
227
+
228
+ // With alwaysSampleErrors=false and baseline=0, errors are dropped
229
+ expect(shouldKeep).toBe(false);
230
+ });
231
+
232
+ it('should respect alwaysSampleSlow flag', () => {
233
+ const sampler = new AdaptiveSampler({
234
+ baselineSampleRate: 0,
235
+ slowThresholdMs: 1000,
236
+ alwaysSampleSlow: false, // Don't force-sample slow requests
237
+ logger: mockLogger,
238
+ });
239
+
240
+ const shouldKeep = sampler.shouldKeepTrace(context, {
241
+ success: true,
242
+ duration: 1500,
243
+ });
244
+
245
+ // With alwaysSampleSlow=false and baseline=0, slow requests are dropped
246
+ expect(shouldKeep).toBe(false);
247
+ });
248
+ });
249
+
250
+ describe('UserIdSampler', () => {
251
+ const extractUserId = (args: unknown[]) => {
252
+ const firstArg = args[0] as { userId?: string };
253
+ return firstArg?.userId;
254
+ };
255
+
256
+ it('should always sample specific users', () => {
257
+ const sampler = new UserIdSampler({
258
+ baselineSampleRate: 0,
259
+ alwaysSampleUsers: ['vip_123'],
260
+ extractUserId,
261
+ logger: mockLogger,
262
+ });
263
+
264
+ const vipContext: SamplingContext = {
265
+ operationName: 'test.operation',
266
+ args: [{ userId: 'vip_123' }],
267
+ };
268
+
269
+ expect(sampler.shouldSample(vipContext)).toBe(true);
270
+ expect(mockLogger.debug).toHaveBeenCalledWith('Sampling user request', {
271
+ operation: 'test.operation',
272
+ userId: 'vip_123',
273
+ });
274
+ });
275
+
276
+ it('should use consistent per-user sampling', () => {
277
+ const sampler = new UserIdSampler({
278
+ baselineSampleRate: 0.5,
279
+ extractUserId,
280
+ logger: mockLogger,
281
+ });
282
+
283
+ const user123Context: SamplingContext = {
284
+ operationName: 'test.operation',
285
+ args: [{ userId: 'user_123' }],
286
+ };
287
+
288
+ // Same user should always get same result
289
+ const result1 = sampler.shouldSample(user123Context);
290
+ const result2 = sampler.shouldSample(user123Context);
291
+ const result3 = sampler.shouldSample(user123Context);
292
+
293
+ expect(result1).toBe(result2);
294
+ expect(result2).toBe(result3);
295
+ });
296
+
297
+ it('should add and remove users from always-sample list', () => {
298
+ const sampler = new UserIdSampler({
299
+ baselineSampleRate: 0,
300
+ extractUserId,
301
+ });
302
+
303
+ sampler.addAlwaysSampleUsers('user_1', 'user_2');
304
+
305
+ const user1Context: SamplingContext = {
306
+ operationName: 'test.operation',
307
+ args: [{ userId: 'user_1' }],
308
+ };
309
+
310
+ expect(sampler.shouldSample(user1Context)).toBe(true);
311
+
312
+ sampler.removeAlwaysSampleUsers('user_1');
313
+ expect(sampler.shouldSample(user1Context)).toBe(false);
314
+ });
315
+
316
+ it('should fallback to random sampling when no user ID', () => {
317
+ const sampler = new UserIdSampler({
318
+ baselineSampleRate: 1,
319
+ extractUserId: (args) => (args[0] as { userId?: string })?.userId,
320
+ });
321
+
322
+ const noUserContext: SamplingContext = {
323
+ operationName: 'test.operation',
324
+ args: [{}],
325
+ };
326
+
327
+ expect(sampler.shouldSample(noUserContext)).toBe(true);
328
+ });
329
+ });
330
+
331
+ describe('CompositeSampler', () => {
332
+ it('should throw error with no child samplers', () => {
333
+ expect(() => new CompositeSampler([])).toThrow();
334
+ });
335
+
336
+ it('should sample if any child sampler returns true', () => {
337
+ const sampler = new CompositeSampler([
338
+ new NeverSampler(),
339
+ new AlwaysSampler(),
340
+ new NeverSampler(),
341
+ ]);
342
+
343
+ expect(sampler.shouldSample(context)).toBe(true);
344
+ });
345
+
346
+ it('should not sample if all child samplers return false', () => {
347
+ const sampler = new CompositeSampler([
348
+ new NeverSampler(),
349
+ new NeverSampler(),
350
+ ]);
351
+
352
+ expect(sampler.shouldSample(context)).toBe(false);
353
+ });
354
+ });
355
+
356
+ describe('FeatureFlagSampler', () => {
357
+ const extractFlags = (
358
+ args: unknown[],
359
+ metadata?: Record<string, unknown>,
360
+ ) => {
361
+ const firstArg = args[0] as { flags?: string[] };
362
+ return firstArg?.flags || (metadata?.featureFlags as string[]);
363
+ };
364
+
365
+ it('should always sample requests with monitored flags', () => {
366
+ const sampler = new FeatureFlagSampler({
367
+ baselineSampleRate: 0,
368
+ alwaysSampleFlags: ['new_checkout', 'experimental_ui'],
369
+ extractFlags,
370
+ logger: mockLogger,
371
+ });
372
+
373
+ const flagContext: SamplingContext = {
374
+ operationName: 'test.operation',
375
+ args: [{ flags: ['new_checkout'] }],
376
+ };
377
+
378
+ expect(sampler.shouldSample(flagContext)).toBe(true);
379
+ expect(mockLogger.debug).toHaveBeenCalledWith(
380
+ 'Sampling feature flag request',
381
+ {
382
+ operation: 'test.operation',
383
+ flags: ['new_checkout'],
384
+ },
385
+ );
386
+ });
387
+
388
+ it('should use baseline sampling for non-monitored flags', () => {
389
+ const sampler = new FeatureFlagSampler({
390
+ baselineSampleRate: 0,
391
+ alwaysSampleFlags: ['monitored_flag'],
392
+ extractFlags,
393
+ });
394
+
395
+ const flagContext: SamplingContext = {
396
+ operationName: 'test.operation',
397
+ args: [{ flags: ['other_flag'] }],
398
+ };
399
+
400
+ expect(sampler.shouldSample(flagContext)).toBe(false);
401
+ });
402
+
403
+ it('should add and remove flags', () => {
404
+ const sampler = new FeatureFlagSampler({
405
+ baselineSampleRate: 0,
406
+ extractFlags,
407
+ });
408
+
409
+ sampler.addAlwaysSampleFlags('flag_1', 'flag_2');
410
+
411
+ const flag1Context: SamplingContext = {
412
+ operationName: 'test.operation',
413
+ args: [{ flags: ['flag_1'] }],
414
+ };
415
+
416
+ expect(sampler.shouldSample(flag1Context)).toBe(true);
417
+
418
+ sampler.removeAlwaysSampleFlags('flag_1');
419
+ expect(sampler.shouldSample(flag1Context)).toBe(false);
420
+ });
421
+ });
422
+
423
+ describe('Real-world QA in Production scenarios', () => {
424
+ it('should always capture failed email deliveries from article example', () => {
425
+ // From article: "We set up another alert that let us know if our
426
+ // email-sending microservice was unable to process a request"
427
+ const sampler = new AdaptiveSampler({
428
+ baselineSampleRate: 0.1, // 10% baseline
429
+ alwaysSampleErrors: true, // Always capture failures
430
+ });
431
+
432
+ const emailContext: SamplingContext = {
433
+ operationName: 'email.send',
434
+ args: [{ to: 'school@example.com' }],
435
+ };
436
+
437
+ sampler.shouldSample(emailContext);
438
+
439
+ // Email fails due to invalid address
440
+ const shouldKeep = sampler.shouldKeepTrace(emailContext, {
441
+ success: false,
442
+ duration: 100,
443
+ error: new Error('Invalid email address'),
444
+ });
445
+
446
+ expect(shouldKeep).toBe(true);
447
+ });
448
+
449
+ it('should always trace slow job applications from article example', () => {
450
+ // From article: Monitor if teachers are able to submit applications
451
+ const sampler = new AdaptiveSampler({
452
+ baselineSampleRate: 0.05, // 5% baseline
453
+ slowThresholdMs: 2000, // Slow if > 2s
454
+ alwaysSampleSlow: true,
455
+ });
456
+
457
+ const applicationContext: SamplingContext = {
458
+ operationName: 'application.submit',
459
+ args: [{ jobId: '123', teacherId: '456' }],
460
+ };
461
+
462
+ sampler.shouldSample(applicationContext);
463
+
464
+ // Application takes too long
465
+ const shouldKeep = sampler.shouldKeepTrace(applicationContext, {
466
+ success: true,
467
+ duration: 3000, // > 2s threshold
468
+ });
469
+
470
+ expect(shouldKeep).toBe(true);
471
+ });
472
+
473
+ it('should always trace VIP users', () => {
474
+ const extractUserId = (args: unknown[]) => {
475
+ const firstArg = args[0] as { userId?: string };
476
+ return firstArg?.userId;
477
+ };
478
+
479
+ const sampler = new UserIdSampler({
480
+ baselineSampleRate: 0.01, // 1% of normal users
481
+ alwaysSampleUsers: ['vip_school_123'], // Always trace VIP schools
482
+ extractUserId,
483
+ });
484
+
485
+ const vipContext: SamplingContext = {
486
+ operationName: 'application.receive',
487
+ args: [{ userId: 'vip_school_123' }],
488
+ };
489
+
490
+ expect(sampler.shouldSample(vipContext)).toBe(true);
491
+ });
492
+
493
+ it('should always trace A/B test variants for correlation', () => {
494
+ const extractFlags = (args: unknown[]) => {
495
+ const firstArg = args[0] as { experimentFlags?: string[] };
496
+ return firstArg?.experimentFlags;
497
+ };
498
+
499
+ const sampler = new FeatureFlagSampler({
500
+ baselineSampleRate: 0.05,
501
+ alwaysSampleFlags: ['new_application_form'], // Always trace experiment
502
+ extractFlags,
503
+ });
504
+
505
+ const experimentContext: SamplingContext = {
506
+ operationName: 'application.submit',
507
+ args: [{ experimentFlags: ['new_application_form'] }],
508
+ };
509
+
510
+ expect(sampler.shouldSample(experimentContext)).toBe(true);
511
+ });
512
+ });
513
+ });