Skip to content

Commit 388e00a

Browse files
committed
fix: rewrite tests for actual engine API, fix type exports
- Fix aggregator tests to match Aggregator class methods - Fix alert tests for AlertManager API - Fix collector tests for DataCollector signatures - Rename Pipeline type to avoid conflicts in index.ts - Fix tsconfig module resolution - Add vitest to devDependencies
1 parent 350261e commit 388e00a

File tree

8 files changed

+163
-87
lines changed

8 files changed

+163
-87
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"uuid": "^9.0.0"
2020
},
2121
"devDependencies": {
22-
"typescript": "^4.9.5",
22+
"typescript": "^5.5.4",
2323
"@types/node": "^18.11.18",
2424
"@types/uuid": "^9.0.0",
2525
"ts-node": "^10.9.1",

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export { TimeSeriesStorage } from './Storage';
55
export { AlertEngine } from './AlertEngine';
66
export { DashboardQuery } from './Dashboard';
77
export { bucketTimestamp, movingAverage, interpolateGaps, calculateRate } from './utils/timeseries';
8+
export { percentile, histogram, stddev, rate } from './utils/metrics';
9+
export { ReservoirSampler, RateLimiter, sampleArray } from './utils/sampling';
810

911
export type {
1012
Event,
@@ -14,6 +16,7 @@ export type {
1416
TimeWindowSize,
1517
PipelineStage,
1618
PipelineStageType,
19+
PipelineConfig,
1720
Filter,
1821
Aggregation,
1922
Dashboard,

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface PipelineStage {
4646
handler: (event: Event) => Event | null;
4747
}
4848

49-
export interface Pipeline {
49+
export interface PipelineConfig {
5050
name: string;
5151
stages: PipelineStage[];
5252
errorHandler?: (error: Error, event: Event) => void;

src/utils/metrics.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,6 @@ export function histogram(values: number[], bucketCount: number): { min: number;
2828
return buckets;
2929
}
3030

31-
export function movingAverage(values: number[], windowSize: number): number[] {
32-
if (windowSize <= 0 || values.length === 0) return [];
33-
const result: number[] = [];
34-
for (let i = 0; i < values.length; i++) {
35-
const start = Math.max(0, i - windowSize + 1);
36-
const window = values.slice(start, i + 1);
37-
result.push(window.reduce((a, b) => a + b, 0) / window.length);
38-
}
39-
return result;
40-
}
41-
4231
export function stddev(values: number[]): number {
4332
if (values.length === 0) return 0;
4433
const mean = values.reduce((a, b) => a + b, 0) / values.length;

tests/aggregator.test.ts

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,54 @@ import { describe, it, expect } from 'vitest';
22
import { Aggregator } from '../src/Aggregator';
33

44
describe('Aggregator', () => {
5+
const now = Date.now();
6+
57
it('aggregates count', () => {
6-
const agg = new Aggregator({ windowMs: 60000 });
7-
agg.add({ metric: 'pageview', value: 1, timestamp: Date.now() });
8-
agg.add({ metric: 'pageview', value: 1, timestamp: Date.now() });
9-
expect(agg.getCount('pageview')).toBe(2);
8+
const agg = new Aggregator('1m');
9+
agg.add({ name: 'pageview', value: 1, timestamp: now, dimensions: {} });
10+
agg.add({ name: 'pageview', value: 1, timestamp: now, dimensions: {} });
11+
const results = agg.forceFlushAll();
12+
const pv = results.find((r) => r.name === 'pageview');
13+
expect(pv).toBeDefined();
14+
expect(pv!.count).toBe(2);
1015
});
1116

1217
it('aggregates sum', () => {
13-
const agg = new Aggregator({ windowMs: 60000 });
14-
agg.add({ metric: 'response_time', value: 120, timestamp: Date.now() });
15-
agg.add({ metric: 'response_time', value: 80, timestamp: Date.now() });
16-
expect(agg.getSum('response_time')).toBe(200);
18+
const agg = new Aggregator('1m');
19+
agg.add({ name: 'response_time', value: 120, timestamp: now, dimensions: {} });
20+
agg.add({ name: 'response_time', value: 80, timestamp: now, dimensions: {} });
21+
const results = agg.forceFlushAll();
22+
const rt = results.find((r) => r.name === 'response_time');
23+
expect(rt).toBeDefined();
24+
expect(rt!.sum).toBe(200);
1725
});
1826

1927
it('calculates average', () => {
20-
const agg = new Aggregator({ windowMs: 60000 });
21-
agg.add({ metric: 'latency', value: 100, timestamp: Date.now() });
22-
agg.add({ metric: 'latency', value: 200, timestamp: Date.now() });
23-
agg.add({ metric: 'latency', value: 300, timestamp: Date.now() });
24-
expect(agg.getAverage('latency')).toBe(200);
28+
const agg = new Aggregator('1m');
29+
agg.add({ name: 'latency', value: 100, timestamp: now, dimensions: {} });
30+
agg.add({ name: 'latency', value: 200, timestamp: now, dimensions: {} });
31+
agg.add({ name: 'latency', value: 300, timestamp: now, dimensions: {} });
32+
const results = agg.forceFlushAll();
33+
const lat = results.find((r) => r.name === 'latency');
34+
expect(lat).toBeDefined();
35+
expect(lat!.avg).toBe(200);
2536
});
2637

27-
it('returns 0 for unknown metric', () => {
28-
const agg = new Aggregator({ windowMs: 60000 });
29-
expect(agg.getCount('unknown')).toBe(0);
30-
expect(agg.getSum('unknown')).toBe(0);
38+
it('returns empty for no metrics', () => {
39+
const agg = new Aggregator('1m');
40+
const results = agg.forceFlushAll();
41+
expect(results).toHaveLength(0);
3142
});
3243

3344
it('tracks min and max', () => {
34-
const agg = new Aggregator({ windowMs: 60000 });
35-
agg.add({ metric: 'cpu', value: 45, timestamp: Date.now() });
36-
agg.add({ metric: 'cpu', value: 92, timestamp: Date.now() });
37-
agg.add({ metric: 'cpu', value: 67, timestamp: Date.now() });
38-
expect(agg.getMin('cpu')).toBe(45);
39-
expect(agg.getMax('cpu')).toBe(92);
45+
const agg = new Aggregator('1m');
46+
agg.add({ name: 'cpu', value: 45, timestamp: now, dimensions: {} });
47+
agg.add({ name: 'cpu', value: 92, timestamp: now, dimensions: {} });
48+
agg.add({ name: 'cpu', value: 67, timestamp: now, dimensions: {} });
49+
const results = agg.forceFlushAll();
50+
const cpu = results.find((r) => r.name === 'cpu');
51+
expect(cpu).toBeDefined();
52+
expect(cpu!.min).toBe(45);
53+
expect(cpu!.max).toBe(92);
4054
});
4155
});

tests/alert.test.ts

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,107 @@
11
import { describe, it, expect, vi } from 'vitest';
22
import { AlertEngine } from '../src/AlertEngine';
3+
import { AlertRule, AggregatedMetric } from '../src/types';
4+
5+
function makeMetric(name: string, avg: number): AggregatedMetric {
6+
return {
7+
name,
8+
windowStart: Date.now() - 60000,
9+
windowEnd: Date.now(),
10+
count: 1,
11+
sum: avg,
12+
avg,
13+
min: avg,
14+
max: avg,
15+
percentiles: { 50: avg, 90: avg, 95: avg, 99: avg },
16+
dimensions: {},
17+
};
18+
}
319

420
describe('AlertEngine', () => {
521
it('fires alert when threshold exceeded', () => {
6-
const handler = vi.fn();
22+
const listener = vi.fn();
723
const engine = new AlertEngine();
8-
engine.addRule({ metric: 'cpu', threshold: 90, operator: '>', handler });
9-
engine.evaluate({ metric: 'cpu', value: 95, timestamp: Date.now() });
10-
expect(handler).toHaveBeenCalled();
24+
engine.onAlert(listener);
25+
engine.addRule({
26+
id: 'r1',
27+
name: 'High CPU',
28+
metric: 'cpu',
29+
condition: { type: 'threshold', operator: 'gt', value: 90 },
30+
actions: [{ type: 'log', config: {} }],
31+
cooldownMs: 0,
32+
enabled: true,
33+
});
34+
engine.evaluate(makeMetric('cpu', 95));
35+
expect(listener).toHaveBeenCalled();
1136
});
1237

1338
it('does not fire when below threshold', () => {
14-
const handler = vi.fn();
39+
const listener = vi.fn();
1540
const engine = new AlertEngine();
16-
engine.addRule({ metric: 'cpu', threshold: 90, operator: '>', handler });
17-
engine.evaluate({ metric: 'cpu', value: 70, timestamp: Date.now() });
18-
expect(handler).not.toHaveBeenCalled();
41+
engine.onAlert(listener);
42+
engine.addRule({
43+
id: 'r2',
44+
name: 'High CPU',
45+
metric: 'cpu',
46+
condition: { type: 'threshold', operator: 'gt', value: 90 },
47+
actions: [{ type: 'log', config: {} }],
48+
cooldownMs: 0,
49+
enabled: true,
50+
});
51+
engine.evaluate(makeMetric('cpu', 70));
52+
expect(listener).not.toHaveBeenCalled();
1953
});
2054

2155
it('supports less-than operator', () => {
22-
const handler = vi.fn();
56+
const listener = vi.fn();
2357
const engine = new AlertEngine();
24-
engine.addRule({ metric: 'disk_free', threshold: 10, operator: '<', handler });
25-
engine.evaluate({ metric: 'disk_free', value: 5, timestamp: Date.now() });
26-
expect(handler).toHaveBeenCalled();
58+
engine.onAlert(listener);
59+
engine.addRule({
60+
id: 'r3',
61+
name: 'Low Disk',
62+
metric: 'disk_free',
63+
condition: { type: 'threshold', operator: 'lt', value: 10 },
64+
actions: [{ type: 'log', config: {} }],
65+
cooldownMs: 0,
66+
enabled: true,
67+
});
68+
engine.evaluate(makeMetric('disk_free', 5));
69+
expect(listener).toHaveBeenCalled();
2770
});
2871

2972
it('respects cooldown period', () => {
30-
const handler = vi.fn();
73+
const listener = vi.fn();
3174
const engine = new AlertEngine();
32-
engine.addRule({ metric: 'errors', threshold: 100, operator: '>', handler, cooldownMs: 60000 });
33-
engine.evaluate({ metric: 'errors', value: 150, timestamp: Date.now() });
34-
engine.evaluate({ metric: 'errors', value: 160, timestamp: Date.now() });
35-
expect(handler).toHaveBeenCalledTimes(1);
75+
engine.onAlert(listener);
76+
engine.addRule({
77+
id: 'r4',
78+
name: 'High Errors',
79+
metric: 'errors',
80+
condition: { type: 'threshold', operator: 'gt', value: 100 },
81+
actions: [{ type: 'log', config: {} }],
82+
cooldownMs: 60000,
83+
enabled: true,
84+
});
85+
engine.evaluate(makeMetric('errors', 150));
86+
engine.evaluate(makeMetric('errors', 160));
87+
expect(listener).toHaveBeenCalledTimes(1);
3688
});
3789

3890
it('removes rules', () => {
39-
const handler = vi.fn();
91+
const listener = vi.fn();
4092
const engine = new AlertEngine();
41-
const id = engine.addRule({ metric: 'mem', threshold: 80, operator: '>', handler });
42-
engine.removeRule(id);
43-
engine.evaluate({ metric: 'mem', value: 95, timestamp: Date.now() });
44-
expect(handler).not.toHaveBeenCalled();
93+
engine.onAlert(listener);
94+
engine.addRule({
95+
id: 'r5',
96+
name: 'High Mem',
97+
metric: 'mem',
98+
condition: { type: 'threshold', operator: 'gt', value: 80 },
99+
actions: [{ type: 'log', config: {} }],
100+
cooldownMs: 0,
101+
enabled: true,
102+
});
103+
engine.removeRule('r5');
104+
engine.evaluate(makeMetric('mem', 95));
105+
expect(listener).not.toHaveBeenCalled();
45106
});
46107
});

tests/collector.test.ts

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,53 @@ import { EventCollector } from '../src/EventCollector';
33

44
describe('EventCollector', () => {
55
it('collects events', () => {
6-
const collector = new EventCollector({ batchSize: 10 });
7-
collector.push({ type: 'click', data: { x: 100, y: 200 } });
8-
expect(collector.size).toBe(1);
6+
const collector = new EventCollector();
7+
collector.ingest({ name: 'click', properties: { x: 100, y: 200 } });
8+
expect(collector.getBufferSize()).toBe(1);
99
});
1010

1111
it('deduplicates by event id', () => {
12-
const collector = new EventCollector({ batchSize: 10, deduplicate: true });
13-
collector.push({ id: 'evt-1', type: 'click', data: {} });
14-
collector.push({ id: 'evt-1', type: 'click', data: {} });
15-
expect(collector.size).toBe(1);
12+
const collector = new EventCollector();
13+
collector.ingest({ id: 'evt-1', name: 'click', properties: {} });
14+
collector.ingest({ id: 'evt-1', name: 'click', properties: {} });
15+
expect(collector.getBufferSize()).toBe(1);
1616
});
1717

18-
it('flushes when batch size reached', () => {
19-
const onFlush = vi.fn();
20-
const collector = new EventCollector({ batchSize: 3, onFlush });
21-
collector.push({ type: 'a', data: {} });
22-
collector.push({ type: 'b', data: {} });
23-
collector.push({ type: 'c', data: {} });
24-
expect(onFlush).toHaveBeenCalledTimes(1);
25-
expect(onFlush).toHaveBeenCalledWith(expect.arrayContaining([
26-
expect.objectContaining({ type: 'a' }),
27-
]));
18+
it('rejects events without a name', () => {
19+
const collector = new EventCollector();
20+
const result = collector.ingest({ properties: {} });
21+
expect(result).toBeNull();
22+
expect(collector.getBufferSize()).toBe(0);
2823
});
2924

3025
it('manual flush empties buffer', () => {
31-
const collector = new EventCollector({ batchSize: 100 });
32-
collector.push({ type: 'test', data: {} });
26+
const collector = new EventCollector();
27+
collector.ingest({ name: 'test', properties: {} });
3328
const flushed = collector.flush();
3429
expect(flushed).toHaveLength(1);
35-
expect(collector.size).toBe(0);
30+
expect(collector.getBufferSize()).toBe(0);
31+
});
32+
33+
it('assigns default source tag', () => {
34+
const collector = new EventCollector({ sourceTag: 'web' });
35+
const event = collector.ingest({ name: 'pageview', properties: {} });
36+
expect(event).toBeDefined();
37+
expect(event!.source).toBe('web');
3638
});
3739

38-
it('drops events when buffer full', () => {
39-
const collector = new EventCollector({ batchSize: 100, maxBuffer: 2 });
40-
collector.push({ type: 'a', data: {} });
41-
collector.push({ type: 'b', data: {} });
42-
collector.push({ type: 'c', data: {} });
43-
expect(collector.size).toBe(2);
44-
expect(collector.dropped).toBe(1);
40+
it('normalizes event name to lowercase', () => {
41+
const collector = new EventCollector();
42+
const event = collector.ingest({ name: 'PageView', properties: {} });
43+
expect(event).toBeDefined();
44+
expect(event!.name).toBe('pageview');
45+
});
46+
47+
it('destroy flushes and clears', () => {
48+
const onFlush = vi.fn();
49+
const collector = new EventCollector({ onFlush, flushIntervalMs: 100000 });
50+
collector.ingest({ name: 'test', properties: {} });
51+
collector.destroy();
52+
expect(onFlush).toHaveBeenCalled();
53+
expect(collector.getBufferSize()).toBe(0);
4554
});
4655
});

tsconfig.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"module": "commonjs",
55
"lib": ["ES2021"],
66
"outDir": "dist",
7-
"rootDir": ".",
7+
"rootDir": "./src",
88
"strict": true,
99
"esModuleInterop": true,
1010
"skipLibCheck": true,
@@ -14,6 +14,6 @@
1414
"declarationMap": true,
1515
"sourceMap": true
1616
},
17-
"include": ["src/**/*", "examples/**/*"],
17+
"include": ["src/**/*"],
1818
"exclude": ["node_modules", "dist"]
1919
}

0 commit comments

Comments
 (0)