flow-debugger 1.0.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/PORTFOLIO_README_SECTION.md +177 -0
- package/README.md +251 -0
- package/dashboard/app.js +339 -0
- package/dashboard/index.html +168 -0
- package/dashboard/style.css +846 -0
- package/dist/cjs/core/Analytics.js +174 -0
- package/dist/cjs/core/Analytics.js.map +1 -0
- package/dist/cjs/core/Classifier.js +66 -0
- package/dist/cjs/core/Classifier.js.map +1 -0
- package/dist/cjs/core/HealthMonitor.js +79 -0
- package/dist/cjs/core/HealthMonitor.js.map +1 -0
- package/dist/cjs/core/RootCause.js +89 -0
- package/dist/cjs/core/RootCause.js.map +1 -0
- package/dist/cjs/core/Sampler.js +34 -0
- package/dist/cjs/core/Sampler.js.map +1 -0
- package/dist/cjs/core/Timeline.js +90 -0
- package/dist/cjs/core/Timeline.js.map +1 -0
- package/dist/cjs/core/TraceEngine.js +222 -0
- package/dist/cjs/core/TraceEngine.js.map +1 -0
- package/dist/cjs/core/types.js +21 -0
- package/dist/cjs/core/types.js.map +1 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/integrations/axios.js +136 -0
- package/dist/cjs/integrations/axios.js.map +1 -0
- package/dist/cjs/integrations/fetch.js +153 -0
- package/dist/cjs/integrations/fetch.js.map +1 -0
- package/dist/cjs/integrations/mongo.js +111 -0
- package/dist/cjs/integrations/mongo.js.map +1 -0
- package/dist/cjs/integrations/mysql.js +212 -0
- package/dist/cjs/integrations/mysql.js.map +1 -0
- package/dist/cjs/integrations/postgres.js +182 -0
- package/dist/cjs/integrations/postgres.js.map +1 -0
- package/dist/cjs/integrations/redis.js +105 -0
- package/dist/cjs/integrations/redis.js.map +1 -0
- package/dist/cjs/middleware/express.js +255 -0
- package/dist/cjs/middleware/express.js.map +1 -0
- package/dist/esm/core/Analytics.js +170 -0
- package/dist/esm/core/Analytics.js.map +1 -0
- package/dist/esm/core/Classifier.js +61 -0
- package/dist/esm/core/Classifier.js.map +1 -0
- package/dist/esm/core/HealthMonitor.js +75 -0
- package/dist/esm/core/HealthMonitor.js.map +1 -0
- package/dist/esm/core/RootCause.js +86 -0
- package/dist/esm/core/RootCause.js.map +1 -0
- package/dist/esm/core/Sampler.js +30 -0
- package/dist/esm/core/Sampler.js.map +1 -0
- package/dist/esm/core/Timeline.js +86 -0
- package/dist/esm/core/Timeline.js.map +1 -0
- package/dist/esm/core/TraceEngine.js +217 -0
- package/dist/esm/core/TraceEngine.js.map +1 -0
- package/dist/esm/core/types.js +18 -0
- package/dist/esm/core/types.js.map +1 -0
- package/dist/esm/index.js +22 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/integrations/axios.js +133 -0
- package/dist/esm/integrations/axios.js.map +1 -0
- package/dist/esm/integrations/fetch.js +149 -0
- package/dist/esm/integrations/fetch.js.map +1 -0
- package/dist/esm/integrations/mongo.js +107 -0
- package/dist/esm/integrations/mongo.js.map +1 -0
- package/dist/esm/integrations/mysql.js +209 -0
- package/dist/esm/integrations/mysql.js.map +1 -0
- package/dist/esm/integrations/postgres.js +179 -0
- package/dist/esm/integrations/postgres.js.map +1 -0
- package/dist/esm/integrations/redis.js +102 -0
- package/dist/esm/integrations/redis.js.map +1 -0
- package/dist/esm/middleware/express.js +219 -0
- package/dist/esm/middleware/express.js.map +1 -0
- package/dist/types/core/Analytics.d.ts +35 -0
- package/dist/types/core/Analytics.d.ts.map +1 -0
- package/dist/types/core/Classifier.d.ts +21 -0
- package/dist/types/core/Classifier.d.ts.map +1 -0
- package/dist/types/core/HealthMonitor.d.ts +14 -0
- package/dist/types/core/HealthMonitor.d.ts.map +1 -0
- package/dist/types/core/RootCause.d.ts +12 -0
- package/dist/types/core/RootCause.d.ts.map +1 -0
- package/dist/types/core/Sampler.d.ts +13 -0
- package/dist/types/core/Sampler.d.ts.map +1 -0
- package/dist/types/core/Timeline.d.ts +22 -0
- package/dist/types/core/Timeline.d.ts.map +1 -0
- package/dist/types/core/TraceEngine.d.ts +47 -0
- package/dist/types/core/TraceEngine.d.ts.map +1 -0
- package/dist/types/core/types.d.ts +118 -0
- package/dist/types/core/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/integrations/axios.d.ts +22 -0
- package/dist/types/integrations/axios.d.ts.map +1 -0
- package/dist/types/integrations/fetch.d.ts +25 -0
- package/dist/types/integrations/fetch.d.ts.map +1 -0
- package/dist/types/integrations/mongo.d.ts +26 -0
- package/dist/types/integrations/mongo.d.ts.map +1 -0
- package/dist/types/integrations/mysql.d.ts +20 -0
- package/dist/types/integrations/mysql.d.ts.map +1 -0
- package/dist/types/integrations/postgres.d.ts +20 -0
- package/dist/types/integrations/postgres.d.ts.map +1 -0
- package/dist/types/integrations/redis.d.ts +20 -0
- package/dist/types/integrations/redis.d.ts.map +1 -0
- package/dist/types/middleware/express.d.ts +39 -0
- package/dist/types/middleware/express.d.ts.map +1 -0
- package/example/server.ts +234 -0
- package/jest.config.js +8 -0
- package/package.json +110 -0
- package/portfolio-repo/APIRESPONSE DASH.png +0 -0
- package/portfolio-repo/PAYLOAD.png +0 -0
- package/portfolio-repo/README.md +182 -0
- package/src/core/Analytics.ts +209 -0
- package/src/core/Classifier.ts +82 -0
- package/src/core/HealthMonitor.ts +92 -0
- package/src/core/RootCause.ts +105 -0
- package/src/core/Sampler.ts +35 -0
- package/src/core/Timeline.ts +108 -0
- package/src/core/TraceEngine.ts +266 -0
- package/src/core/types.ts +170 -0
- package/src/index.ts +42 -0
- package/src/integrations/axios.ts +164 -0
- package/src/integrations/fetch.ts +172 -0
- package/src/integrations/mongo.ts +130 -0
- package/src/integrations/mysql.ts +239 -0
- package/src/integrations/postgres.ts +217 -0
- package/src/integrations/redis.ts +122 -0
- package/src/middleware/express.ts +264 -0
- package/tests/Analytics.test.ts +136 -0
- package/tests/Classifier.test.ts +57 -0
- package/tests/RootCause.test.ts +69 -0
- package/tests/TraceEngine.test.ts +110 -0
- package/tsconfig.cjs.json +9 -0
- package/tsconfig.esm.json +9 -0
- package/tsconfig.json +31 -0
- package/tsconfig.types.json +8 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — Analytics Tests
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import { Analytics } from '../src/core/Analytics';
|
|
6
|
+
import { Trace, TraceStep } from '../src/core/types';
|
|
7
|
+
|
|
8
|
+
function createTrace(overrides: Partial<Trace> = {}): Trace {
|
|
9
|
+
return {
|
|
10
|
+
traceId: 'req_test_' + Math.random().toString(36).substring(2, 6),
|
|
11
|
+
endpoint: '/api/test',
|
|
12
|
+
method: 'GET',
|
|
13
|
+
statusCode: 200,
|
|
14
|
+
steps: [],
|
|
15
|
+
totalDuration: 50,
|
|
16
|
+
classification: 'INFO',
|
|
17
|
+
rootCause: null,
|
|
18
|
+
startTime: 0,
|
|
19
|
+
endTime: 50,
|
|
20
|
+
timestamp: new Date(),
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('Analytics', () => {
|
|
26
|
+
let analytics: Analytics;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
analytics = new Analytics(100);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should record traces', () => {
|
|
33
|
+
analytics.record(createTrace());
|
|
34
|
+
analytics.record(createTrace());
|
|
35
|
+
expect(analytics.getTraceCount()).toBe(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should produce endpoint stats', () => {
|
|
39
|
+
analytics.record(createTrace({ endpoint: '/api/login', method: 'POST', totalDuration: 30 }));
|
|
40
|
+
analytics.record(createTrace({ endpoint: '/api/login', method: 'POST', totalDuration: 50 }));
|
|
41
|
+
analytics.record(createTrace({ endpoint: '/api/orders', method: 'GET', totalDuration: 100 }));
|
|
42
|
+
|
|
43
|
+
const report = analytics.getReport();
|
|
44
|
+
expect(report.totalRequests).toBe(3);
|
|
45
|
+
expect(report.endpoints).toHaveLength(2);
|
|
46
|
+
|
|
47
|
+
const loginStats = report.endpoints.find(e => e.path === '/api/login');
|
|
48
|
+
expect(loginStats).toBeDefined();
|
|
49
|
+
expect(loginStats!.totalRequests).toBe(2);
|
|
50
|
+
expect(loginStats!.avgDuration).toBe(40);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should count errors and slow requests', () => {
|
|
54
|
+
analytics.record(createTrace({ classification: 'ERROR' }));
|
|
55
|
+
analytics.record(createTrace({ classification: 'CRITICAL' }));
|
|
56
|
+
analytics.record(createTrace({ classification: 'WARN' }));
|
|
57
|
+
analytics.record(createTrace({ classification: 'INFO' }));
|
|
58
|
+
|
|
59
|
+
const report = analytics.getReport();
|
|
60
|
+
expect(report.totalErrors).toBe(2);
|
|
61
|
+
expect(report.totalSlow).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should track service failures', () => {
|
|
65
|
+
const failedStep: TraceStep = {
|
|
66
|
+
name: 'Redis get',
|
|
67
|
+
service: 'redis',
|
|
68
|
+
status: 'error',
|
|
69
|
+
classification: 'ERROR',
|
|
70
|
+
startTime: 0,
|
|
71
|
+
endTime: 10,
|
|
72
|
+
duration: 10,
|
|
73
|
+
error: 'Connection refused',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
analytics.record(createTrace({
|
|
77
|
+
steps: [failedStep],
|
|
78
|
+
classification: 'ERROR',
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
const report = analytics.getReport();
|
|
82
|
+
expect(report.topFailures.length).toBeGreaterThan(0);
|
|
83
|
+
expect(report.topFailures[0].service).toBe('redis');
|
|
84
|
+
expect(report.topFailures[0].percentage).toBe(100);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should trim old traces when over max', () => {
|
|
88
|
+
const analytics = new Analytics(5);
|
|
89
|
+
for (let i = 0; i < 10; i++) {
|
|
90
|
+
analytics.record(createTrace());
|
|
91
|
+
}
|
|
92
|
+
expect(analytics.getTraceCount()).toBe(5);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should track common issues', () => {
|
|
96
|
+
analytics.record(createTrace({
|
|
97
|
+
rootCause: { cause: 'Redis failure', step: 'Redis GET', service: 'redis', confidence: 'high' },
|
|
98
|
+
}));
|
|
99
|
+
analytics.record(createTrace({
|
|
100
|
+
rootCause: { cause: 'Redis failure', step: 'Redis GET', service: 'redis', confidence: 'high' },
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
const report = analytics.getReport();
|
|
104
|
+
const ep = report.endpoints[0];
|
|
105
|
+
expect(ep.commonIssues.length).toBeGreaterThan(0);
|
|
106
|
+
expect(ep.commonIssues[0]).toContain('Redis failure');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('should get endpoint-specific report', () => {
|
|
110
|
+
analytics.record(createTrace({ endpoint: '/api/login', method: 'POST' }));
|
|
111
|
+
analytics.record(createTrace({ endpoint: '/api/orders', method: 'GET' }));
|
|
112
|
+
|
|
113
|
+
const loginReport = analytics.getEndpointReport('/api/login');
|
|
114
|
+
expect(loginReport).not.toBeNull();
|
|
115
|
+
expect(loginReport!.path).toBe('/api/login');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should return health monitor data', () => {
|
|
119
|
+
const step: TraceStep = {
|
|
120
|
+
name: 'Mongo find',
|
|
121
|
+
service: 'mongo',
|
|
122
|
+
status: 'success',
|
|
123
|
+
classification: 'INFO',
|
|
124
|
+
startTime: 0,
|
|
125
|
+
endTime: 10,
|
|
126
|
+
duration: 10,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
analytics.record(createTrace({ steps: [step] }));
|
|
130
|
+
|
|
131
|
+
const report = analytics.getReport();
|
|
132
|
+
expect(report.serviceHealth.length).toBeGreaterThan(0);
|
|
133
|
+
expect(report.serviceHealth[0].service).toBe('mongo');
|
|
134
|
+
expect(report.serviceHealth[0].status).toBe('healthy');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — Classifier Tests
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import { classify, classifyTrace, classifyQuery } from '../src/core/Classifier';
|
|
6
|
+
import { TraceStep } from '../src/core/types';
|
|
7
|
+
|
|
8
|
+
const config = { slowThreshold: 300, slowQueryThreshold: 300 };
|
|
9
|
+
|
|
10
|
+
describe('Classifier', () => {
|
|
11
|
+
describe('classify (single step)', () => {
|
|
12
|
+
test('INFO for fast success', () => {
|
|
13
|
+
expect(classify(50, 'success', config)).toBe('INFO');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('WARN for slow success', () => {
|
|
17
|
+
expect(classify(500, 'success', config)).toBe('WARN');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('ERROR for failed step', () => {
|
|
21
|
+
expect(classify(10, 'error', config)).toBe('ERROR');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('CRITICAL for timeout', () => {
|
|
25
|
+
expect(classify(5000, 'timeout', config)).toBe('CRITICAL');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('respects custom threshold', () => {
|
|
29
|
+
expect(classify(150, 'success', { slowThreshold: 100 })).toBe('WARN');
|
|
30
|
+
expect(classify(150, 'success', { slowThreshold: 200 })).toBe('INFO');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('classifyTrace', () => {
|
|
35
|
+
test('inherits highest severity from steps', () => {
|
|
36
|
+
const steps: TraceStep[] = [
|
|
37
|
+
{ name: 'a', service: 'mongo', status: 'success', classification: 'INFO', startTime: 0, endTime: 10, duration: 10 },
|
|
38
|
+
{ name: 'b', service: 'redis', status: 'error', classification: 'ERROR', startTime: 10, endTime: 15, duration: 5 },
|
|
39
|
+
];
|
|
40
|
+
expect(classifyTrace(steps, 20, config)).toBe('ERROR');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('WARN when total duration is slow even if steps are fast', () => {
|
|
44
|
+
const steps: TraceStep[] = [
|
|
45
|
+
{ name: 'a', service: 'internal', status: 'success', classification: 'INFO', startTime: 0, endTime: 10, duration: 10 },
|
|
46
|
+
];
|
|
47
|
+
expect(classifyTrace(steps, 500, config)).toBe('WARN');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('classifyQuery', () => {
|
|
52
|
+
test('uses slowQueryThreshold', () => {
|
|
53
|
+
expect(classifyQuery(100, 'success', { slowQueryThreshold: 300 })).toBe('INFO');
|
|
54
|
+
expect(classifyQuery(400, 'success', { slowQueryThreshold: 300 })).toBe('WARN');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — Root Cause Tests
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import { detectRootCause } from '../src/core/RootCause';
|
|
6
|
+
import { TraceStep } from '../src/core/types';
|
|
7
|
+
|
|
8
|
+
const config = { slowThreshold: 300, slowQueryThreshold: 300 };
|
|
9
|
+
|
|
10
|
+
describe('RootCause', () => {
|
|
11
|
+
test('returns null for empty steps', () => {
|
|
12
|
+
expect(detectRootCause([], 200, config)).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('returns null for all-healthy trace', () => {
|
|
16
|
+
const steps: TraceStep[] = [
|
|
17
|
+
{ name: 'DB query', service: 'mongo', status: 'success', classification: 'INFO', startTime: 0, endTime: 10, duration: 10 },
|
|
18
|
+
{ name: 'Redis get', service: 'redis', status: 'success', classification: 'INFO', startTime: 10, endTime: 15, duration: 5 },
|
|
19
|
+
];
|
|
20
|
+
expect(detectRootCause(steps, 200, config)).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('detects timeout as high-confidence root cause', () => {
|
|
24
|
+
const steps: TraceStep[] = [
|
|
25
|
+
{ name: 'DB query', service: 'mongo', status: 'success', classification: 'INFO', startTime: 0, endTime: 10, duration: 10 },
|
|
26
|
+
{ name: 'Redis SET', service: 'redis', status: 'timeout', classification: 'CRITICAL', startTime: 10, endTime: 5010, duration: 5000, error: 'timeout' },
|
|
27
|
+
];
|
|
28
|
+
const rc = detectRootCause(steps, 500, config);
|
|
29
|
+
expect(rc).not.toBeNull();
|
|
30
|
+
expect(rc!.service).toBe('redis');
|
|
31
|
+
expect(rc!.confidence).toBe('high');
|
|
32
|
+
expect(rc!.cause).toContain('timed out');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('detects first failed step on 500 response', () => {
|
|
36
|
+
const steps: TraceStep[] = [
|
|
37
|
+
{ name: 'DB find', service: 'mongo', status: 'error', classification: 'ERROR', startTime: 0, endTime: 10, duration: 10, error: 'Connection lost' },
|
|
38
|
+
{ name: 'Redis set', service: 'redis', status: 'success', classification: 'INFO', startTime: 10, endTime: 15, duration: 5 },
|
|
39
|
+
];
|
|
40
|
+
const rc = detectRootCause(steps, 500, config);
|
|
41
|
+
expect(rc).not.toBeNull();
|
|
42
|
+
expect(rc!.step).toBe('DB find');
|
|
43
|
+
expect(rc!.service).toBe('mongo');
|
|
44
|
+
expect(rc!.confidence).toBe('high');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('detects slow DB as root cause', () => {
|
|
48
|
+
const steps: TraceStep[] = [
|
|
49
|
+
{ name: 'DB orders query', service: 'postgres', status: 'success', classification: 'WARN', startTime: 0, endTime: 800, duration: 800 },
|
|
50
|
+
{ name: 'Redis cache', service: 'redis', status: 'success', classification: 'INFO', startTime: 800, endTime: 805, duration: 5 },
|
|
51
|
+
];
|
|
52
|
+
const rc = detectRootCause(steps, 200, config);
|
|
53
|
+
expect(rc).not.toBeNull();
|
|
54
|
+
expect(rc!.service).toBe('postgres');
|
|
55
|
+
expect(rc!.cause).toContain('Slow');
|
|
56
|
+
expect(rc!.confidence).toBe('high'); // >50% of total
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('medium confidence when multiple slow steps', () => {
|
|
60
|
+
const steps: TraceStep[] = [
|
|
61
|
+
{ name: 'DB query', service: 'postgres', status: 'success', classification: 'WARN', startTime: 0, endTime: 400, duration: 400 },
|
|
62
|
+
{ name: 'Redis op', service: 'redis', status: 'success', classification: 'WARN', startTime: 400, endTime: 800, duration: 400 },
|
|
63
|
+
];
|
|
64
|
+
const rc = detectRootCause(steps, 200, config);
|
|
65
|
+
expect(rc).not.toBeNull();
|
|
66
|
+
// Both are equally slow, so confidence should be medium
|
|
67
|
+
expect(rc!.confidence).toBe('medium');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — TraceEngine Tests
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
import { TraceEngine } from '../src/core/TraceEngine';
|
|
6
|
+
|
|
7
|
+
describe('TraceEngine', () => {
|
|
8
|
+
let engine: TraceEngine;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
engine = new TraceEngine({
|
|
12
|
+
enableTimeline: false, // suppress console output in tests
|
|
13
|
+
slowThreshold: 300,
|
|
14
|
+
slowQueryThreshold: 300,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should generate unique trace IDs', () => {
|
|
19
|
+
const t1 = engine.startTrace('/api/login', 'POST');
|
|
20
|
+
const t2 = engine.startTrace('/api/orders', 'GET');
|
|
21
|
+
expect(t1.getTraceId()).not.toBe(t2.getTraceId());
|
|
22
|
+
expect(t1.getTraceId()).toMatch(/^req_/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should track steps with timing', async () => {
|
|
26
|
+
const tracer = engine.startTrace('/api/test', 'GET');
|
|
27
|
+
|
|
28
|
+
await tracer.step('DB query', async () => {
|
|
29
|
+
await new Promise(r => setTimeout(r, 10));
|
|
30
|
+
return 'result';
|
|
31
|
+
}, { service: 'mongo' });
|
|
32
|
+
|
|
33
|
+
const trace = tracer.end(200);
|
|
34
|
+
expect(trace.steps).toHaveLength(1);
|
|
35
|
+
expect(trace.steps[0].name).toBe('DB query');
|
|
36
|
+
expect(trace.steps[0].service).toBe('mongo');
|
|
37
|
+
expect(trace.steps[0].status).toBe('success');
|
|
38
|
+
expect(trace.steps[0].duration).toBeGreaterThan(5);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should return step result', async () => {
|
|
42
|
+
const tracer = engine.startTrace('/test', 'GET');
|
|
43
|
+
const result = await tracer.step('compute', async () => {
|
|
44
|
+
return { value: 42 };
|
|
45
|
+
});
|
|
46
|
+
expect(result).toEqual({ value: 42 });
|
|
47
|
+
tracer.end(200);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('should catch step errors', async () => {
|
|
51
|
+
const tracer = engine.startTrace('/test', 'GET');
|
|
52
|
+
|
|
53
|
+
await expect(
|
|
54
|
+
tracer.step('failing step', async () => {
|
|
55
|
+
throw new Error('DB connection failed');
|
|
56
|
+
}, { service: 'postgres' })
|
|
57
|
+
).rejects.toThrow('DB connection failed');
|
|
58
|
+
|
|
59
|
+
const trace = tracer.end(500);
|
|
60
|
+
expect(trace.steps[0].status).toBe('error');
|
|
61
|
+
expect(trace.steps[0].error).toBe('DB connection failed');
|
|
62
|
+
expect(trace.steps[0].stackTrace).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should detect step timeout', async () => {
|
|
66
|
+
const tracer = engine.startTrace('/test', 'GET');
|
|
67
|
+
|
|
68
|
+
await expect(
|
|
69
|
+
tracer.step('slow query', async () => {
|
|
70
|
+
await new Promise(r => setTimeout(r, 500));
|
|
71
|
+
}, { service: 'mysql', timeout: 50 })
|
|
72
|
+
).rejects.toThrow('timed out');
|
|
73
|
+
|
|
74
|
+
const trace = tracer.end(500);
|
|
75
|
+
expect(trace.steps[0].status).toBe('timeout');
|
|
76
|
+
expect(trace.steps[0].classification).toBe('CRITICAL');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should track multiple steps in order', async () => {
|
|
80
|
+
const tracer = engine.startTrace('/api/login', 'POST');
|
|
81
|
+
|
|
82
|
+
await tracer.step('DB find user', async () => 'user', { service: 'mongo' });
|
|
83
|
+
await tracer.step('Redis cache', async () => 'OK', { service: 'redis' });
|
|
84
|
+
await tracer.step('JWT generate', async () => 'token', { service: 'internal' });
|
|
85
|
+
|
|
86
|
+
const trace = tracer.end(200);
|
|
87
|
+
expect(trace.steps).toHaveLength(3);
|
|
88
|
+
expect(trace.steps[0].name).toBe('DB find user');
|
|
89
|
+
expect(trace.steps[1].name).toBe('Redis cache');
|
|
90
|
+
expect(trace.steps[2].name).toBe('JWT generate');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('should emit trace events', (done) => {
|
|
94
|
+
engine.on('trace:end', (trace) => {
|
|
95
|
+
expect(trace.traceId).toMatch(/^req_/);
|
|
96
|
+
expect(trace.endpoint).toBe('/test');
|
|
97
|
+
done();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const tracer = engine.startTrace('/test', 'GET');
|
|
101
|
+
tracer.end(200);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should set total duration', async () => {
|
|
105
|
+
const tracer = engine.startTrace('/test', 'GET');
|
|
106
|
+
await new Promise(r => setTimeout(r, 20));
|
|
107
|
+
const trace = tracer.end(200);
|
|
108
|
+
expect(trace.totalDuration).toBeGreaterThan(15);
|
|
109
|
+
});
|
|
110
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2020"
|
|
7
|
+
],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"outDir": "./dist",
|
|
17
|
+
"rootDir": "./src",
|
|
18
|
+
"moduleResolution": "node",
|
|
19
|
+
"allowSyntheticDefaultImports": true
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"src/**/*"
|
|
23
|
+
],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
"tests",
|
|
28
|
+
"example",
|
|
29
|
+
"dashboard"
|
|
30
|
+
]
|
|
31
|
+
}
|