@lov3kaizen/agentsea-evaluate 0.5.1
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/LICENSE +21 -0
- package/README.md +339 -0
- package/dist/annotation/index.d.mts +3 -0
- package/dist/annotation/index.d.ts +3 -0
- package/dist/annotation/index.js +630 -0
- package/dist/annotation/index.mjs +22 -0
- package/dist/chunk-5JRYKRSE.mjs +2791 -0
- package/dist/chunk-EUXXIZK3.mjs +676 -0
- package/dist/chunk-NBMUSATK.mjs +596 -0
- package/dist/chunk-PAQ2TTJJ.mjs +1105 -0
- package/dist/chunk-TUMNJN2S.mjs +416 -0
- package/dist/continuous/index.d.mts +2 -0
- package/dist/continuous/index.d.ts +2 -0
- package/dist/continuous/index.js +707 -0
- package/dist/continuous/index.mjs +16 -0
- package/dist/datasets/index.d.mts +1 -0
- package/dist/datasets/index.d.ts +1 -0
- package/dist/datasets/index.js +456 -0
- package/dist/datasets/index.mjs +14 -0
- package/dist/evaluation/index.d.mts +1 -0
- package/dist/evaluation/index.d.ts +1 -0
- package/dist/evaluation/index.js +2853 -0
- package/dist/evaluation/index.mjs +78 -0
- package/dist/feedback/index.d.mts +2 -0
- package/dist/feedback/index.d.ts +2 -0
- package/dist/feedback/index.js +1158 -0
- package/dist/feedback/index.mjs +40 -0
- package/dist/index-6Pbiq7ny.d.mts +234 -0
- package/dist/index-6Pbiq7ny.d.ts +234 -0
- package/dist/index-BNTycFEA.d.mts +479 -0
- package/dist/index-BNTycFEA.d.ts +479 -0
- package/dist/index-CTYCfWfH.d.mts +543 -0
- package/dist/index-CTYCfWfH.d.ts +543 -0
- package/dist/index-Cq5LwG_3.d.mts +322 -0
- package/dist/index-Cq5LwG_3.d.ts +322 -0
- package/dist/index-bPghFsfP.d.mts +315 -0
- package/dist/index-bPghFsfP.d.ts +315 -0
- package/dist/index.d.mts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +5962 -0
- package/dist/index.mjs +429 -0
- package/package.json +102 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
// src/continuous/ContinuousEval.ts
|
|
2
|
+
import { EventEmitter as EventEmitter2 } from "eventemitter3";
|
|
3
|
+
|
|
4
|
+
// src/continuous/ABTestRunner.ts
|
|
5
|
+
import { EventEmitter } from "eventemitter3";
|
|
6
|
+
import { nanoid } from "nanoid";
|
|
7
|
+
var ABTestRunner = class extends EventEmitter {
|
|
8
|
+
id;
|
|
9
|
+
name;
|
|
10
|
+
config;
|
|
11
|
+
status = "draft";
|
|
12
|
+
startedAt;
|
|
13
|
+
completedAt;
|
|
14
|
+
controlSamples = /* @__PURE__ */ new Map();
|
|
15
|
+
treatmentSamples = /* @__PURE__ */ new Map();
|
|
16
|
+
sampleCount = 0;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
super();
|
|
19
|
+
this.id = nanoid();
|
|
20
|
+
this.name = config.name;
|
|
21
|
+
this.config = config;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start the A/B test
|
|
25
|
+
*/
|
|
26
|
+
start() {
|
|
27
|
+
if (this.status !== "draft") {
|
|
28
|
+
throw new Error(`Cannot start test in ${this.status} status`);
|
|
29
|
+
}
|
|
30
|
+
this.status = "running";
|
|
31
|
+
this.startedAt = Date.now();
|
|
32
|
+
this.emit("test:started");
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Stop the A/B test
|
|
37
|
+
*/
|
|
38
|
+
stop() {
|
|
39
|
+
this.status = "completed";
|
|
40
|
+
this.completedAt = Date.now();
|
|
41
|
+
const results = this.getResults();
|
|
42
|
+
this.emit("test:completed", results);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pause the test
|
|
47
|
+
*/
|
|
48
|
+
pause() {
|
|
49
|
+
if (this.status === "running") {
|
|
50
|
+
this.status = "paused";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resume the test
|
|
55
|
+
*/
|
|
56
|
+
resume() {
|
|
57
|
+
if (this.status === "paused") {
|
|
58
|
+
this.status = "running";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Assign a sample to a variant
|
|
63
|
+
*/
|
|
64
|
+
assignVariant() {
|
|
65
|
+
if (this.status !== "running") {
|
|
66
|
+
throw new Error("Test is not running");
|
|
67
|
+
}
|
|
68
|
+
const isControl = Math.random() >= this.config.trafficSplit;
|
|
69
|
+
const variant = isControl ? "control" : "treatment";
|
|
70
|
+
const assignment = {
|
|
71
|
+
variant,
|
|
72
|
+
testId: this.id,
|
|
73
|
+
assignedAt: Date.now()
|
|
74
|
+
};
|
|
75
|
+
this.emit("sample:assigned", assignment);
|
|
76
|
+
return variant;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Record sample result
|
|
80
|
+
*/
|
|
81
|
+
recordSample(variant, sampleId, scores) {
|
|
82
|
+
if (this.status !== "running") {
|
|
83
|
+
throw new Error("Test is not running");
|
|
84
|
+
}
|
|
85
|
+
const samples = variant === "control" ? this.controlSamples : this.treatmentSamples;
|
|
86
|
+
samples.set(sampleId, scores);
|
|
87
|
+
this.sampleCount++;
|
|
88
|
+
if (this.sampleCount >= this.config.minSamples) {
|
|
89
|
+
this.checkSignificance();
|
|
90
|
+
}
|
|
91
|
+
if (this.config.maxDuration && this.startedAt && Date.now() - this.startedAt > this.config.maxDuration) {
|
|
92
|
+
this.stop();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get current results
|
|
97
|
+
*/
|
|
98
|
+
getResults() {
|
|
99
|
+
const metrics = {};
|
|
100
|
+
for (const metric of this.config.metrics) {
|
|
101
|
+
metrics[metric] = this.calculateMetricResult(metric);
|
|
102
|
+
}
|
|
103
|
+
let controlWins = 0;
|
|
104
|
+
let treatmentWins = 0;
|
|
105
|
+
for (const result of Object.values(metrics)) {
|
|
106
|
+
if (result.isSignificant) {
|
|
107
|
+
if (result.winner === "control") controlWins++;
|
|
108
|
+
else if (result.winner === "treatment") treatmentWins++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let winner = "none";
|
|
112
|
+
if (controlWins > treatmentWins) winner = "control";
|
|
113
|
+
else if (treatmentWins > controlWins) winner = "treatment";
|
|
114
|
+
const isSignificant = Object.values(metrics).some((m) => m.isSignificant);
|
|
115
|
+
const confidence = isSignificant ? Math.max(...Object.values(metrics).map((m) => 1 - m.pValue)) : 0;
|
|
116
|
+
return {
|
|
117
|
+
controlSamples: this.controlSamples.size,
|
|
118
|
+
treatmentSamples: this.treatmentSamples.size,
|
|
119
|
+
metrics,
|
|
120
|
+
winner,
|
|
121
|
+
isSignificant,
|
|
122
|
+
confidence,
|
|
123
|
+
recommendation: this.generateRecommendation(
|
|
124
|
+
winner,
|
|
125
|
+
isSignificant,
|
|
126
|
+
metrics
|
|
127
|
+
)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Calculate result for a single metric
|
|
132
|
+
*/
|
|
133
|
+
calculateMetricResult(metric) {
|
|
134
|
+
const controlScores = this.getScoresForMetric(this.controlSamples, metric);
|
|
135
|
+
const treatmentScores = this.getScoresForMetric(
|
|
136
|
+
this.treatmentSamples,
|
|
137
|
+
metric
|
|
138
|
+
);
|
|
139
|
+
const controlSummary = this.calculateSummary(controlScores);
|
|
140
|
+
const treatmentSummary = this.calculateSummary(treatmentScores);
|
|
141
|
+
const difference = treatmentSummary.mean - controlSummary.mean;
|
|
142
|
+
const differencePercent = controlSummary.mean !== 0 ? difference / controlSummary.mean * 100 : 0;
|
|
143
|
+
const pValue = this.calculatePValue(controlScores, treatmentScores);
|
|
144
|
+
const isSignificant = pValue < (this.config.significanceLevel ?? 0.05);
|
|
145
|
+
let winner = "none";
|
|
146
|
+
if (isSignificant) {
|
|
147
|
+
winner = difference > 0 ? "treatment" : "control";
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
control: controlSummary,
|
|
151
|
+
treatment: treatmentSummary,
|
|
152
|
+
difference,
|
|
153
|
+
differencePercent,
|
|
154
|
+
pValue,
|
|
155
|
+
isSignificant,
|
|
156
|
+
winner
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get scores for a metric from samples
|
|
161
|
+
*/
|
|
162
|
+
getScoresForMetric(samples, metric) {
|
|
163
|
+
const scores = [];
|
|
164
|
+
for (const sampleScores of samples.values()) {
|
|
165
|
+
if (metric in sampleScores) {
|
|
166
|
+
scores.push(sampleScores[metric]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return scores;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Calculate summary statistics
|
|
173
|
+
*/
|
|
174
|
+
calculateSummary(scores) {
|
|
175
|
+
if (scores.length === 0) {
|
|
176
|
+
return {
|
|
177
|
+
mean: 0,
|
|
178
|
+
std: 0,
|
|
179
|
+
sampleCount: 0,
|
|
180
|
+
confidenceInterval: [0, 0]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const mean = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
184
|
+
const variance = scores.reduce((sum, s) => sum + Math.pow(s - mean, 2), 0) / scores.length;
|
|
185
|
+
const std = Math.sqrt(variance);
|
|
186
|
+
const se = std / Math.sqrt(scores.length);
|
|
187
|
+
const margin = 1.96 * se;
|
|
188
|
+
return {
|
|
189
|
+
mean,
|
|
190
|
+
std,
|
|
191
|
+
sampleCount: scores.length,
|
|
192
|
+
confidenceInterval: [mean - margin, mean + margin]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Calculate p-value using Welch's t-test approximation
|
|
197
|
+
*/
|
|
198
|
+
calculatePValue(control, treatment) {
|
|
199
|
+
if (control.length < 2 || treatment.length < 2) return 1;
|
|
200
|
+
const n1 = control.length;
|
|
201
|
+
const n2 = treatment.length;
|
|
202
|
+
const mean1 = control.reduce((a, b) => a + b, 0) / n1;
|
|
203
|
+
const mean2 = treatment.reduce((a, b) => a + b, 0) / n2;
|
|
204
|
+
const var1 = control.reduce((sum, x) => sum + Math.pow(x - mean1, 2), 0) / (n1 - 1);
|
|
205
|
+
const var2 = treatment.reduce((sum, x) => sum + Math.pow(x - mean2, 2), 0) / (n2 - 1);
|
|
206
|
+
const se = Math.sqrt(var1 / n1 + var2 / n2);
|
|
207
|
+
if (se === 0) return 1;
|
|
208
|
+
const t = Math.abs(mean1 - mean2) / se;
|
|
209
|
+
const pValue = 2 * (1 - this.normalCDF(t));
|
|
210
|
+
return Math.max(0, Math.min(1, pValue));
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Normal CDF approximation
|
|
214
|
+
*/
|
|
215
|
+
normalCDF(x) {
|
|
216
|
+
const a1 = 0.254829592;
|
|
217
|
+
const a2 = -0.284496736;
|
|
218
|
+
const a3 = 1.421413741;
|
|
219
|
+
const a4 = -1.453152027;
|
|
220
|
+
const a5 = 1.061405429;
|
|
221
|
+
const p = 0.3275911;
|
|
222
|
+
const sign = x < 0 ? -1 : 1;
|
|
223
|
+
x = Math.abs(x) / Math.sqrt(2);
|
|
224
|
+
const t = 1 / (1 + p * x);
|
|
225
|
+
const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
|
|
226
|
+
return 0.5 * (1 + sign * y);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Check for statistical significance
|
|
230
|
+
*/
|
|
231
|
+
checkSignificance() {
|
|
232
|
+
for (const metric of this.config.metrics) {
|
|
233
|
+
const result = this.calculateMetricResult(metric);
|
|
234
|
+
if (result.isSignificant && result.winner !== "none") {
|
|
235
|
+
this.emit("test:significant", metric, result.winner);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Generate recommendation
|
|
241
|
+
*/
|
|
242
|
+
generateRecommendation(winner, isSignificant, metrics) {
|
|
243
|
+
if (!isSignificant) {
|
|
244
|
+
const totalSamples = this.controlSamples.size + this.treatmentSamples.size;
|
|
245
|
+
if (totalSamples < this.config.minSamples) {
|
|
246
|
+
return `Not enough samples yet. Need ${this.config.minSamples - totalSamples} more.`;
|
|
247
|
+
}
|
|
248
|
+
return "No significant difference detected. Consider running longer or adjusting variants.";
|
|
249
|
+
}
|
|
250
|
+
if (winner === "treatment") {
|
|
251
|
+
const improvements = Object.entries(metrics).filter(([, r]) => r.winner === "treatment").map(([m, r]) => `${m}: +${r.differencePercent.toFixed(1)}%`);
|
|
252
|
+
return `Treatment variant wins. Improvements: ${improvements.join(", ")}`;
|
|
253
|
+
}
|
|
254
|
+
if (winner === "control") {
|
|
255
|
+
return "Control variant performs better. Consider keeping current configuration.";
|
|
256
|
+
}
|
|
257
|
+
return "Mixed results. Review individual metrics for details.";
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get test status
|
|
261
|
+
*/
|
|
262
|
+
getStatus() {
|
|
263
|
+
return this.status;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get test configuration
|
|
267
|
+
*/
|
|
268
|
+
getConfig() {
|
|
269
|
+
return { ...this.config };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get test timestamps
|
|
273
|
+
*/
|
|
274
|
+
getTimestamps() {
|
|
275
|
+
return {
|
|
276
|
+
startedAt: this.startedAt,
|
|
277
|
+
completedAt: this.completedAt
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
function createABTestRunner(config) {
|
|
282
|
+
return new ABTestRunner(config);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/continuous/ContinuousEval.ts
|
|
286
|
+
var ContinuousEval = class extends EventEmitter2 {
|
|
287
|
+
pipeline;
|
|
288
|
+
sampleRate;
|
|
289
|
+
status = "stopped";
|
|
290
|
+
startedAt;
|
|
291
|
+
lastEvalAt;
|
|
292
|
+
totalEvaluations = 0;
|
|
293
|
+
passedCount = 0;
|
|
294
|
+
scoreHistory = {};
|
|
295
|
+
alertManager;
|
|
296
|
+
abTests = /* @__PURE__ */ new Map();
|
|
297
|
+
intervalId;
|
|
298
|
+
constructor(config) {
|
|
299
|
+
super();
|
|
300
|
+
this.pipeline = config.pipeline;
|
|
301
|
+
this.sampleRate = config.sampleRate;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Set alert manager
|
|
305
|
+
*/
|
|
306
|
+
setAlerts(alertManager, rules) {
|
|
307
|
+
this.alertManager = alertManager;
|
|
308
|
+
for (const [metric, rule] of Object.entries(rules)) {
|
|
309
|
+
alertManager.addRule({
|
|
310
|
+
metric,
|
|
311
|
+
threshold: rule.threshold,
|
|
312
|
+
direction: rule.direction
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Start monitoring
|
|
318
|
+
*/
|
|
319
|
+
start() {
|
|
320
|
+
if (this.status === "running") return;
|
|
321
|
+
this.status = "running";
|
|
322
|
+
this.startedAt = Date.now();
|
|
323
|
+
this.emit("status:changed", this.status);
|
|
324
|
+
this.emit("eval:started");
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Stop monitoring
|
|
328
|
+
*/
|
|
329
|
+
stop() {
|
|
330
|
+
this.status = "stopped";
|
|
331
|
+
if (this.intervalId) {
|
|
332
|
+
clearInterval(this.intervalId);
|
|
333
|
+
this.intervalId = void 0;
|
|
334
|
+
}
|
|
335
|
+
this.emit("status:changed", this.status);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Pause monitoring
|
|
339
|
+
*/
|
|
340
|
+
pause() {
|
|
341
|
+
if (this.status === "running") {
|
|
342
|
+
this.status = "paused";
|
|
343
|
+
this.emit("status:changed", this.status);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Resume monitoring
|
|
348
|
+
*/
|
|
349
|
+
resume() {
|
|
350
|
+
if (this.status === "paused") {
|
|
351
|
+
this.status = "running";
|
|
352
|
+
this.emit("status:changed", this.status);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Evaluate a sample
|
|
357
|
+
*/
|
|
358
|
+
async evaluate(input) {
|
|
359
|
+
if (Math.random() > this.sampleRate) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
if (this.status !== "running") {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const result = await this.pipeline.evaluate(input);
|
|
367
|
+
this.totalEvaluations++;
|
|
368
|
+
this.lastEvalAt = Date.now();
|
|
369
|
+
if (result.passed) {
|
|
370
|
+
this.passedCount++;
|
|
371
|
+
}
|
|
372
|
+
for (const [metric, score] of Object.entries(result.scores)) {
|
|
373
|
+
if (!this.scoreHistory[metric]) {
|
|
374
|
+
this.scoreHistory[metric] = [];
|
|
375
|
+
}
|
|
376
|
+
this.scoreHistory[metric].push(score);
|
|
377
|
+
if (this.scoreHistory[metric].length > 1e3) {
|
|
378
|
+
this.scoreHistory[metric].shift();
|
|
379
|
+
}
|
|
380
|
+
if (this.alertManager) {
|
|
381
|
+
this.alertManager.check(metric, score);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
this.emit("eval:completed", result);
|
|
385
|
+
return result;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
this.status = "error";
|
|
388
|
+
this.emit("eval:error", error);
|
|
389
|
+
this.emit("status:changed", this.status);
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get statistics
|
|
395
|
+
*/
|
|
396
|
+
getStats() {
|
|
397
|
+
const avgScores = {};
|
|
398
|
+
for (const [metric, scores] of Object.entries(this.scoreHistory)) {
|
|
399
|
+
if (scores.length > 0) {
|
|
400
|
+
avgScores[metric] = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
status: this.status,
|
|
405
|
+
startedAt: this.startedAt,
|
|
406
|
+
lastEvalAt: this.lastEvalAt,
|
|
407
|
+
totalEvaluations: this.totalEvaluations,
|
|
408
|
+
passRate: this.totalEvaluations > 0 ? this.passedCount / this.totalEvaluations : 0,
|
|
409
|
+
avgScores,
|
|
410
|
+
alertsTriggered: this.alertManager?.getAlertCount() ?? 0
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Create A/B test
|
|
415
|
+
*/
|
|
416
|
+
createABTest(config) {
|
|
417
|
+
const test = new ABTestRunner(config);
|
|
418
|
+
this.abTests.set(test.id, test);
|
|
419
|
+
return test;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Get A/B test
|
|
423
|
+
*/
|
|
424
|
+
getABTest(id) {
|
|
425
|
+
return this.abTests.get(id);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Get all A/B tests
|
|
429
|
+
*/
|
|
430
|
+
getABTests() {
|
|
431
|
+
return Array.from(this.abTests.values());
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Get score history for a metric
|
|
435
|
+
*/
|
|
436
|
+
getScoreHistory(metric) {
|
|
437
|
+
return this.scoreHistory[metric] ?? [];
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Reset statistics
|
|
441
|
+
*/
|
|
442
|
+
reset() {
|
|
443
|
+
this.totalEvaluations = 0;
|
|
444
|
+
this.passedCount = 0;
|
|
445
|
+
this.scoreHistory = {};
|
|
446
|
+
this.lastEvalAt = void 0;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
function createContinuousEval(config) {
|
|
450
|
+
return new ContinuousEval(config);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/continuous/AlertManager.ts
|
|
454
|
+
import { EventEmitter as EventEmitter3 } from "eventemitter3";
|
|
455
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
456
|
+
var AlertManager = class extends EventEmitter3 {
|
|
457
|
+
channels;
|
|
458
|
+
rules = /* @__PURE__ */ new Map();
|
|
459
|
+
activeAlerts = /* @__PURE__ */ new Map();
|
|
460
|
+
cooldownMs;
|
|
461
|
+
lastAlertTime = /* @__PURE__ */ new Map();
|
|
462
|
+
alertCount = 0;
|
|
463
|
+
constructor(config) {
|
|
464
|
+
super();
|
|
465
|
+
this.channels = config.channels;
|
|
466
|
+
this.cooldownMs = config.cooldownMs ?? 3e5;
|
|
467
|
+
if (config.rules) {
|
|
468
|
+
for (const [metric, rule] of Object.entries(config.rules)) {
|
|
469
|
+
this.rules.set(metric, rule);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Add an alert rule
|
|
475
|
+
*/
|
|
476
|
+
addRule(rule) {
|
|
477
|
+
this.rules.set(rule.metric, rule);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Remove an alert rule
|
|
481
|
+
*/
|
|
482
|
+
removeRule(metric) {
|
|
483
|
+
return this.rules.delete(metric);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Check value against rules
|
|
487
|
+
*/
|
|
488
|
+
check(metric, value) {
|
|
489
|
+
const rule = this.rules.get(metric);
|
|
490
|
+
if (!rule) return null;
|
|
491
|
+
const shouldAlert = rule.direction === "above" && value > rule.threshold || rule.direction === "below" && value < rule.threshold;
|
|
492
|
+
if (shouldAlert) {
|
|
493
|
+
return this.triggerAlert(rule, metric, value);
|
|
494
|
+
} else {
|
|
495
|
+
const existingAlert = this.activeAlerts.get(metric);
|
|
496
|
+
if (existingAlert) {
|
|
497
|
+
this.resolveAlert(metric);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Trigger an alert
|
|
504
|
+
*/
|
|
505
|
+
triggerAlert(rule, metric, value) {
|
|
506
|
+
const lastTime = this.lastAlertTime.get(metric);
|
|
507
|
+
if (lastTime && Date.now() - lastTime < this.cooldownMs) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const alert = {
|
|
511
|
+
id: nanoid2(),
|
|
512
|
+
rule,
|
|
513
|
+
metric,
|
|
514
|
+
currentValue: value,
|
|
515
|
+
threshold: rule.threshold,
|
|
516
|
+
severity: rule.severity ?? "warning",
|
|
517
|
+
message: this.formatMessage(rule, metric, value),
|
|
518
|
+
triggeredAt: Date.now()
|
|
519
|
+
};
|
|
520
|
+
this.activeAlerts.set(metric, alert);
|
|
521
|
+
this.lastAlertTime.set(metric, Date.now());
|
|
522
|
+
this.alertCount++;
|
|
523
|
+
this.emit("alert:triggered", alert);
|
|
524
|
+
void this.sendNotifications(alert);
|
|
525
|
+
return alert;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Resolve an alert
|
|
529
|
+
*/
|
|
530
|
+
resolveAlert(metric) {
|
|
531
|
+
const alert = this.activeAlerts.get(metric);
|
|
532
|
+
if (alert) {
|
|
533
|
+
alert.resolvedAt = Date.now();
|
|
534
|
+
this.activeAlerts.delete(metric);
|
|
535
|
+
this.emit("alert:resolved", alert);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Acknowledge an alert
|
|
540
|
+
*/
|
|
541
|
+
acknowledgeAlert(alertId) {
|
|
542
|
+
for (const alert of this.activeAlerts.values()) {
|
|
543
|
+
if (alert.id === alertId) {
|
|
544
|
+
alert.acknowledged = true;
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Send notifications
|
|
552
|
+
*/
|
|
553
|
+
async sendNotifications(alert) {
|
|
554
|
+
for (const channel of this.channels) {
|
|
555
|
+
try {
|
|
556
|
+
await this.sendToChannel(channel, alert);
|
|
557
|
+
const notification = {
|
|
558
|
+
alertId: alert.id,
|
|
559
|
+
channel: channel.type,
|
|
560
|
+
sentAt: Date.now(),
|
|
561
|
+
success: true
|
|
562
|
+
};
|
|
563
|
+
this.emit("notification:sent", notification);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const notification = {
|
|
566
|
+
alertId: alert.id,
|
|
567
|
+
channel: channel.type,
|
|
568
|
+
sentAt: Date.now(),
|
|
569
|
+
success: false,
|
|
570
|
+
error: error.message
|
|
571
|
+
};
|
|
572
|
+
this.emit("notification:sent", notification);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Send to a specific channel
|
|
578
|
+
*/
|
|
579
|
+
async sendToChannel(channel, alert) {
|
|
580
|
+
switch (channel.type) {
|
|
581
|
+
case "webhook":
|
|
582
|
+
if (channel.webhook) {
|
|
583
|
+
await fetch(channel.webhook, {
|
|
584
|
+
method: "POST",
|
|
585
|
+
headers: { "Content-Type": "application/json" },
|
|
586
|
+
body: JSON.stringify({
|
|
587
|
+
alert: {
|
|
588
|
+
id: alert.id,
|
|
589
|
+
metric: alert.metric,
|
|
590
|
+
severity: alert.severity,
|
|
591
|
+
message: alert.message,
|
|
592
|
+
value: alert.currentValue,
|
|
593
|
+
threshold: alert.threshold,
|
|
594
|
+
triggeredAt: new Date(alert.triggeredAt).toISOString()
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
case "slack":
|
|
601
|
+
if (channel.webhook) {
|
|
602
|
+
await fetch(channel.webhook, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { "Content-Type": "application/json" },
|
|
605
|
+
body: JSON.stringify({
|
|
606
|
+
text: `\u{1F6A8} *${alert.severity.toUpperCase()}*: ${alert.message}`,
|
|
607
|
+
attachments: [
|
|
608
|
+
{
|
|
609
|
+
color: alert.severity === "critical" ? "danger" : "warning",
|
|
610
|
+
fields: [
|
|
611
|
+
{ title: "Metric", value: alert.metric, short: true },
|
|
612
|
+
{
|
|
613
|
+
title: "Value",
|
|
614
|
+
value: alert.currentValue.toFixed(4),
|
|
615
|
+
short: true
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
title: "Threshold",
|
|
619
|
+
value: alert.threshold.toString(),
|
|
620
|
+
short: true
|
|
621
|
+
}
|
|
622
|
+
]
|
|
623
|
+
}
|
|
624
|
+
]
|
|
625
|
+
})
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
case "email":
|
|
630
|
+
console.log(`[Email Alert] To: ${channel.to?.join(", ")}`);
|
|
631
|
+
console.log(`Subject: Alert: ${alert.metric} - ${alert.severity}`);
|
|
632
|
+
console.log(`Body: ${alert.message}`);
|
|
633
|
+
break;
|
|
634
|
+
case "pagerduty":
|
|
635
|
+
console.log(`[PagerDuty Alert] ${alert.severity}: ${alert.message}`);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Format alert message
|
|
641
|
+
*/
|
|
642
|
+
formatMessage(rule, metric, value) {
|
|
643
|
+
const direction = rule.direction === "above" ? "exceeded" : "dropped below";
|
|
644
|
+
return `${metric} ${direction} threshold: ${value.toFixed(4)} (threshold: ${rule.threshold})`;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get active alerts
|
|
648
|
+
*/
|
|
649
|
+
getActiveAlerts() {
|
|
650
|
+
return Array.from(this.activeAlerts.values());
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Get alert count
|
|
654
|
+
*/
|
|
655
|
+
getAlertCount() {
|
|
656
|
+
return this.alertCount;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Get rules
|
|
660
|
+
*/
|
|
661
|
+
getRules() {
|
|
662
|
+
return Array.from(this.rules.values());
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
function createAlertManager(config) {
|
|
666
|
+
return new AlertManager(config);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export {
|
|
670
|
+
ABTestRunner,
|
|
671
|
+
createABTestRunner,
|
|
672
|
+
ContinuousEval,
|
|
673
|
+
createContinuousEval,
|
|
674
|
+
AlertManager,
|
|
675
|
+
createAlertManager
|
|
676
|
+
};
|