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,266 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — Trace Engine
|
|
3
|
+
// Manages trace lifecycle: create → add steps → end
|
|
4
|
+
// ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import {
|
|
8
|
+
Trace,
|
|
9
|
+
TraceStep,
|
|
10
|
+
StepOptions,
|
|
11
|
+
StepStatus,
|
|
12
|
+
ClassificationLevel,
|
|
13
|
+
DebuggerConfig,
|
|
14
|
+
DEFAULT_CONFIG,
|
|
15
|
+
ServiceTag,
|
|
16
|
+
} from './types';
|
|
17
|
+
import { classify, classifyTrace } from './Classifier';
|
|
18
|
+
import { detectRootCause } from './RootCause';
|
|
19
|
+
import { renderTimeline } from './Timeline';
|
|
20
|
+
|
|
21
|
+
let idCounter = 0;
|
|
22
|
+
|
|
23
|
+
/** Generate a unique trace ID */
|
|
24
|
+
function generateTraceId(): string {
|
|
25
|
+
const ts = Date.now().toString(36);
|
|
26
|
+
const rand = Math.random().toString(36).substring(2, 8);
|
|
27
|
+
idCounter++;
|
|
28
|
+
return `req_${ts}_${rand}_${idCounter}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract file and line number from stack trace for dashboard preview.
|
|
33
|
+
* Example: "at Object.<anonymous> (d:\\project\\auth.service.ts:42:15)"
|
|
34
|
+
* Returns: { file: "auth.service.ts", line: 42 }
|
|
35
|
+
*/
|
|
36
|
+
function extractErrorLocation(stackTrace?: string): { file?: string; line?: number } {
|
|
37
|
+
if (!stackTrace) return {};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Match patterns like:
|
|
41
|
+
// at functionName (file.ts:42:15)
|
|
42
|
+
// at file.ts:42:15
|
|
43
|
+
const match = stackTrace.match(/\(([^)]+):(\d+):\d+\)/) || stackTrace.match(/at\s+([^:]+):(\d+):\d+/);
|
|
44
|
+
if (match) {
|
|
45
|
+
const fullPath = match[1];
|
|
46
|
+
const lineNum = parseInt(match[2], 10);
|
|
47
|
+
|
|
48
|
+
// Extract just the filename from full path
|
|
49
|
+
const fileName = fullPath.split(/[/\\]/).pop() || fullPath;
|
|
50
|
+
|
|
51
|
+
return { file: fileName, line: lineNum };
|
|
52
|
+
}
|
|
53
|
+
} catch (_) {
|
|
54
|
+
// ignore parse errors
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* TraceEngine — manages the lifecycle of a single request trace.
|
|
63
|
+
*
|
|
64
|
+
* Usage:
|
|
65
|
+
* const engine = new TraceEngine(config);
|
|
66
|
+
* const tracer = engine.startTrace('/login', 'POST');
|
|
67
|
+
* await tracer.step('DB find user', async () => User.findOne(), { service: 'mongo' });
|
|
68
|
+
* tracer.end(200);
|
|
69
|
+
*/
|
|
70
|
+
export class TraceEngine extends EventEmitter {
|
|
71
|
+
private config: Required<DebuggerConfig>;
|
|
72
|
+
|
|
73
|
+
constructor(config?: DebuggerConfig) {
|
|
74
|
+
super();
|
|
75
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Update config at runtime */
|
|
79
|
+
updateConfig(patch: Partial<DebuggerConfig>): void {
|
|
80
|
+
Object.assign(this.config, patch);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getConfig(): Required<DebuggerConfig> {
|
|
84
|
+
return { ...this.config };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Start a new request trace */
|
|
88
|
+
startTrace(endpoint: string, method: string): RequestTracer {
|
|
89
|
+
const tracer = new RequestTracer(endpoint, method, this.config, this);
|
|
90
|
+
this.safeEmit('trace:start', { traceId: tracer.getTraceId(), endpoint, method });
|
|
91
|
+
return tracer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Safe emit — never throws */
|
|
95
|
+
safeEmit(event: string, data: unknown): void {
|
|
96
|
+
try {
|
|
97
|
+
this.emit(event, data);
|
|
98
|
+
} catch (_) {
|
|
99
|
+
// debugger never crashes the app
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* RequestTracer — the object attached to a single request.
|
|
106
|
+
* Provides `step()` for manual instrumentation and `addStep()` for auto-instrumentation.
|
|
107
|
+
*/
|
|
108
|
+
export class RequestTracer {
|
|
109
|
+
private traceId: string;
|
|
110
|
+
private endpoint: string;
|
|
111
|
+
private method: string;
|
|
112
|
+
private steps: TraceStep[] = [];
|
|
113
|
+
private startTime: number;
|
|
114
|
+
private config: Required<DebuggerConfig>;
|
|
115
|
+
private engine: TraceEngine;
|
|
116
|
+
private ended = false;
|
|
117
|
+
|
|
118
|
+
constructor(
|
|
119
|
+
endpoint: string,
|
|
120
|
+
method: string,
|
|
121
|
+
config: Required<DebuggerConfig>,
|
|
122
|
+
engine: TraceEngine,
|
|
123
|
+
) {
|
|
124
|
+
this.traceId = generateTraceId();
|
|
125
|
+
this.endpoint = endpoint;
|
|
126
|
+
this.method = method;
|
|
127
|
+
this.startTime = performance.now();
|
|
128
|
+
this.config = config;
|
|
129
|
+
this.engine = engine;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getTraceId(): string {
|
|
133
|
+
return this.traceId;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getSteps(): TraceStep[] {
|
|
137
|
+
return [...this.steps];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Manually run & trace an async step */
|
|
141
|
+
async step<T>(name: string, fn: () => Promise<T> | T, options?: StepOptions): Promise<T> {
|
|
142
|
+
const service: ServiceTag = options?.service ?? 'internal';
|
|
143
|
+
const timeout = options?.timeout ?? this.config.defaultTimeout;
|
|
144
|
+
const stepStart = performance.now();
|
|
145
|
+
let status: StepStatus = 'success';
|
|
146
|
+
let error: string | undefined;
|
|
147
|
+
let stackTrace: string | undefined;
|
|
148
|
+
let result: T;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Race the function against a timeout
|
|
152
|
+
result = await Promise.race([
|
|
153
|
+
Promise.resolve(fn()),
|
|
154
|
+
new Promise<never>((_, reject) =>
|
|
155
|
+
setTimeout(() => reject(new Error(`Step "${name}" timed out after ${timeout}ms`)), timeout),
|
|
156
|
+
),
|
|
157
|
+
]);
|
|
158
|
+
} catch (err: unknown) {
|
|
159
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
160
|
+
if (e.message.includes('timed out')) {
|
|
161
|
+
status = 'timeout';
|
|
162
|
+
} else {
|
|
163
|
+
status = 'error';
|
|
164
|
+
}
|
|
165
|
+
error = e.message;
|
|
166
|
+
stackTrace = e.stack;
|
|
167
|
+
const { file: errorFile, line: errorLine } = extractErrorLocation(stackTrace);
|
|
168
|
+
|
|
169
|
+
// Re-throw so the caller can handle the error
|
|
170
|
+
const stepEnd = performance.now();
|
|
171
|
+
const duration = stepEnd - stepStart;
|
|
172
|
+
const classification = classify(duration, status, this.config);
|
|
173
|
+
|
|
174
|
+
this.steps.push({
|
|
175
|
+
name,
|
|
176
|
+
service,
|
|
177
|
+
status,
|
|
178
|
+
classification,
|
|
179
|
+
startTime: stepStart - this.startTime,
|
|
180
|
+
endTime: stepEnd - this.startTime,
|
|
181
|
+
duration,
|
|
182
|
+
error,
|
|
183
|
+
stackTrace,
|
|
184
|
+
errorFile,
|
|
185
|
+
errorLine,
|
|
186
|
+
metadata: options?.metadata,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const stepEnd = performance.now();
|
|
193
|
+
const duration = stepEnd - stepStart;
|
|
194
|
+
const classification = classify(duration, status, this.config);
|
|
195
|
+
const { file: errorFile, line: errorLine } = error ? extractErrorLocation(stackTrace) : {};
|
|
196
|
+
|
|
197
|
+
this.steps.push({
|
|
198
|
+
name,
|
|
199
|
+
service,
|
|
200
|
+
status,
|
|
201
|
+
classification,
|
|
202
|
+
startTime: stepStart - this.startTime,
|
|
203
|
+
endTime: stepEnd - this.startTime,
|
|
204
|
+
duration,
|
|
205
|
+
error,
|
|
206
|
+
stackTrace,
|
|
207
|
+
errorFile,
|
|
208
|
+
errorLine,
|
|
209
|
+
metadata: options?.metadata,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Add a pre-recorded step (used by auto-integrations) */
|
|
216
|
+
addStep(step: TraceStep): void {
|
|
217
|
+
try {
|
|
218
|
+
this.steps.push(step);
|
|
219
|
+
} catch (_) {
|
|
220
|
+
// never crash
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** End the trace and produce the final result */
|
|
225
|
+
end(statusCode?: number): Trace {
|
|
226
|
+
if (this.ended) {
|
|
227
|
+
return this.buildTrace(statusCode);
|
|
228
|
+
}
|
|
229
|
+
this.ended = true;
|
|
230
|
+
|
|
231
|
+
const trace = this.buildTrace(statusCode);
|
|
232
|
+
|
|
233
|
+
// Render timeline to console
|
|
234
|
+
if (this.config.enableTimeline) {
|
|
235
|
+
try {
|
|
236
|
+
renderTimeline(trace, this.config.logger);
|
|
237
|
+
} catch (_) {
|
|
238
|
+
// never crash
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.engine.safeEmit('trace:end', trace);
|
|
243
|
+
return trace;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private buildTrace(statusCode?: number): Trace {
|
|
247
|
+
const endTime = performance.now();
|
|
248
|
+
const totalDuration = endTime - this.startTime;
|
|
249
|
+
const traceClassification = classifyTrace(this.steps, totalDuration, this.config);
|
|
250
|
+
const rootCause = detectRootCause(this.steps, statusCode, this.config);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
traceId: this.traceId,
|
|
254
|
+
endpoint: this.endpoint,
|
|
255
|
+
method: this.method,
|
|
256
|
+
statusCode,
|
|
257
|
+
steps: [...this.steps],
|
|
258
|
+
totalDuration,
|
|
259
|
+
classification: traceClassification,
|
|
260
|
+
rootCause,
|
|
261
|
+
startTime: 0,
|
|
262
|
+
endTime: totalDuration,
|
|
263
|
+
timestamp: new Date(),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — Core Type Definitions
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/** Classification severity levels */
|
|
6
|
+
export type ClassificationLevel = 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
|
|
7
|
+
|
|
8
|
+
/** Known service/dependency types for tagging */
|
|
9
|
+
export type ServiceTag =
|
|
10
|
+
| 'mongo'
|
|
11
|
+
| 'mysql'
|
|
12
|
+
| 'postgres'
|
|
13
|
+
| 'redis'
|
|
14
|
+
| 'axios'
|
|
15
|
+
| 'fetch'
|
|
16
|
+
| 'stripe'
|
|
17
|
+
| 'razorpay'
|
|
18
|
+
| 'sendgrid'
|
|
19
|
+
| 'twilio'
|
|
20
|
+
| 'external'
|
|
21
|
+
| 'internal'
|
|
22
|
+
| 'unknown';
|
|
23
|
+
|
|
24
|
+
/** Health status for a dependency */
|
|
25
|
+
export type HealthState = 'healthy' | 'degraded' | 'down';
|
|
26
|
+
|
|
27
|
+
/** Status of a single traced step */
|
|
28
|
+
export type StepStatus = 'success' | 'error' | 'timeout';
|
|
29
|
+
|
|
30
|
+
// ─── Step & Trace ────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** A single measured step within a trace */
|
|
33
|
+
export interface TraceStep {
|
|
34
|
+
name: string;
|
|
35
|
+
service: ServiceTag;
|
|
36
|
+
status: StepStatus;
|
|
37
|
+
classification: ClassificationLevel;
|
|
38
|
+
startTime: number; // high-res timestamp (ms)
|
|
39
|
+
endTime: number;
|
|
40
|
+
duration: number; // ms
|
|
41
|
+
error?: string;
|
|
42
|
+
stackTrace?: string;
|
|
43
|
+
errorFile?: string; // extracted from stack trace
|
|
44
|
+
errorLine?: number; // extracted from stack trace
|
|
45
|
+
metadata?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Options passed to `step()` */
|
|
49
|
+
export interface StepOptions {
|
|
50
|
+
/** Service tag for grouping (e.g. 'postgres', 'redis') */
|
|
51
|
+
service?: ServiceTag;
|
|
52
|
+
/** Timeout in ms — triggers StepStatus.timeout if exceeded */
|
|
53
|
+
timeout?: number;
|
|
54
|
+
/** Arbitrary metadata attached to the step */
|
|
55
|
+
metadata?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A complete request trace */
|
|
59
|
+
export interface Trace {
|
|
60
|
+
traceId: string;
|
|
61
|
+
endpoint: string;
|
|
62
|
+
method: string;
|
|
63
|
+
statusCode?: number;
|
|
64
|
+
steps: TraceStep[];
|
|
65
|
+
totalDuration: number;
|
|
66
|
+
classification: ClassificationLevel;
|
|
67
|
+
rootCause: RootCauseResult | null;
|
|
68
|
+
startTime: number;
|
|
69
|
+
endTime: number;
|
|
70
|
+
timestamp: Date;
|
|
71
|
+
environment?: string; // dev / staging / production
|
|
72
|
+
payloadSize?: number; // request body size in bytes
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Root Cause ──────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export interface RootCauseResult {
|
|
78
|
+
cause: string;
|
|
79
|
+
step: string;
|
|
80
|
+
service: ServiceTag;
|
|
81
|
+
confidence: 'high' | 'medium' | 'low';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Analytics ───────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export interface ServiceFailureStats {
|
|
87
|
+
service: ServiceTag;
|
|
88
|
+
count: number;
|
|
89
|
+
percentage: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface EndpointStats {
|
|
93
|
+
path: string;
|
|
94
|
+
method: string;
|
|
95
|
+
totalRequests: number;
|
|
96
|
+
errorCount: number;
|
|
97
|
+
slowCount: number;
|
|
98
|
+
avgDuration: number;
|
|
99
|
+
p95Duration: number;
|
|
100
|
+
maxDuration: number;
|
|
101
|
+
commonIssues: string[];
|
|
102
|
+
serviceFailures: ServiceFailureStats[];
|
|
103
|
+
recentTraces: Trace[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AnalyticsReport {
|
|
107
|
+
totalRequests: number;
|
|
108
|
+
totalErrors: number;
|
|
109
|
+
totalSlow: number;
|
|
110
|
+
uptime: number;
|
|
111
|
+
endpoints: EndpointStats[];
|
|
112
|
+
serviceHealth: HealthStatus[];
|
|
113
|
+
topFailures: ServiceFailureStats[];
|
|
114
|
+
recentTraces: Trace[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Health ──────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export interface HealthStatus {
|
|
120
|
+
service: ServiceTag;
|
|
121
|
+
name: string;
|
|
122
|
+
status: HealthState;
|
|
123
|
+
lastCheck: Date;
|
|
124
|
+
successRate: number;
|
|
125
|
+
totalChecks: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Config ──────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export interface DebuggerConfig {
|
|
131
|
+
/** Enable/disable the debugger (default: true) */
|
|
132
|
+
enabled?: boolean;
|
|
133
|
+
/** Environment tag (dev / staging / production) */
|
|
134
|
+
environment?: string;
|
|
135
|
+
/** Threshold in ms to mark a step as slow (default: 300) */
|
|
136
|
+
slowThreshold?: number;
|
|
137
|
+
/** Threshold in ms for slow SQL/DB query detection (default: 300) */
|
|
138
|
+
slowQueryThreshold?: number;
|
|
139
|
+
/** Default step timeout in ms (default: 30000) */
|
|
140
|
+
defaultTimeout?: number;
|
|
141
|
+
/** Sampling rate 0-1 (default: 1 = 100%) */
|
|
142
|
+
samplingRate?: number;
|
|
143
|
+
/** Always sample errors even if sampling drops the request (default: true) */
|
|
144
|
+
alwaysSampleErrors?: boolean;
|
|
145
|
+
/** Max traces to keep in memory for analytics (default: 1000) */
|
|
146
|
+
maxTraces?: number;
|
|
147
|
+
/** Enable console timeline output (default: true) */
|
|
148
|
+
enableTimeline?: boolean;
|
|
149
|
+
/** Enable the /__debugger dashboard (default: true) */
|
|
150
|
+
enableDashboard?: boolean;
|
|
151
|
+
/** Payload size threshold in bytes to warn (default: 1MB) */
|
|
152
|
+
largePayloadThreshold?: number;
|
|
153
|
+
/** Custom logger function */
|
|
154
|
+
logger?: (...args: unknown[]) => void;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const DEFAULT_CONFIG: Required<DebuggerConfig> = {
|
|
158
|
+
enabled: true,
|
|
159
|
+
environment: process.env.NODE_ENV || 'development',
|
|
160
|
+
slowThreshold: 300,
|
|
161
|
+
slowQueryThreshold: 300,
|
|
162
|
+
defaultTimeout: 30000,
|
|
163
|
+
samplingRate: 1,
|
|
164
|
+
alwaysSampleErrors: true,
|
|
165
|
+
maxTraces: 1000,
|
|
166
|
+
enableTimeline: true,
|
|
167
|
+
enableDashboard: true,
|
|
168
|
+
largePayloadThreshold: 1024 * 1024, // 1MB
|
|
169
|
+
logger: console.log,
|
|
170
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — Main Entry Point
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
// Core
|
|
6
|
+
export { TraceEngine, RequestTracer } from './core/TraceEngine';
|
|
7
|
+
export { classify, classifyTrace, classifyQuery } from './core/Classifier';
|
|
8
|
+
export { detectRootCause } from './core/RootCause';
|
|
9
|
+
export { Analytics } from './core/Analytics';
|
|
10
|
+
export { HealthMonitor } from './core/HealthMonitor';
|
|
11
|
+
export { Sampler } from './core/Sampler';
|
|
12
|
+
export { renderTimeline, renderCompact } from './core/Timeline';
|
|
13
|
+
|
|
14
|
+
// Types
|
|
15
|
+
export type {
|
|
16
|
+
ClassificationLevel,
|
|
17
|
+
ServiceTag,
|
|
18
|
+
HealthState,
|
|
19
|
+
StepStatus,
|
|
20
|
+
TraceStep,
|
|
21
|
+
StepOptions,
|
|
22
|
+
Trace,
|
|
23
|
+
RootCauseResult,
|
|
24
|
+
ServiceFailureStats,
|
|
25
|
+
EndpointStats,
|
|
26
|
+
AnalyticsReport,
|
|
27
|
+
HealthStatus,
|
|
28
|
+
DebuggerConfig,
|
|
29
|
+
} from './core/types';
|
|
30
|
+
export { DEFAULT_CONFIG } from './core/types';
|
|
31
|
+
|
|
32
|
+
// Integrations
|
|
33
|
+
export { mongoTracer, removeMongoTracer } from './integrations/mongo';
|
|
34
|
+
export { mysqlTracer } from './integrations/mysql';
|
|
35
|
+
export { pgTracer } from './integrations/postgres';
|
|
36
|
+
export { redisTracer } from './integrations/redis';
|
|
37
|
+
export { fetchTracer, removeFetchTracer } from './integrations/fetch';
|
|
38
|
+
export { axiosTracer } from './integrations/axios';
|
|
39
|
+
|
|
40
|
+
// Middleware
|
|
41
|
+
export { flowDebugger } from './middleware/express';
|
|
42
|
+
export type { FlowDebuggerMiddleware } from './middleware/express';
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — Axios Auto-Instrument
|
|
3
|
+
// Uses interceptors to trace all Axios requests
|
|
4
|
+
// ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { RequestTracer } from '../core/TraceEngine';
|
|
7
|
+
import { TraceStep, StepStatus, DebuggerConfig, DEFAULT_CONFIG, ServiceTag } from '../core/types';
|
|
8
|
+
import { classifyQuery } from '../core/Classifier';
|
|
9
|
+
|
|
10
|
+
type AnyAxios = any;
|
|
11
|
+
|
|
12
|
+
interface AxiosTracerOptions {
|
|
13
|
+
config?: Partial<DebuggerConfig>;
|
|
14
|
+
getTracer?: () => RequestTracer | null;
|
|
15
|
+
/** Map URL patterns to service tags */
|
|
16
|
+
serviceMap?: Record<string, ServiceTag>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_SERVICE_MAP: Record<string, ServiceTag> = {
|
|
20
|
+
'stripe.com': 'stripe',
|
|
21
|
+
'api.stripe.com': 'stripe',
|
|
22
|
+
'razorpay.com': 'razorpay',
|
|
23
|
+
'api.razorpay.com': 'razorpay',
|
|
24
|
+
'sendgrid.com': 'sendgrid',
|
|
25
|
+
'api.sendgrid.com': 'sendgrid',
|
|
26
|
+
'twilio.com': 'twilio',
|
|
27
|
+
'api.twilio.com': 'twilio',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Auto-instrument Axios instance to trace all HTTP requests.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* axiosTracer(axios, { getTracer: () => currentTracer })
|
|
35
|
+
*
|
|
36
|
+
* Output:
|
|
37
|
+
* Axios POST https://api.stripe.com/v1/charges → 234ms ✔ [stripe]
|
|
38
|
+
* Axios GET https://api.razorpay.com/orders → 502 error ❌ [razorpay]
|
|
39
|
+
*/
|
|
40
|
+
export function axiosTracer(axiosInstance: AnyAxios, options?: AxiosTracerOptions): void {
|
|
41
|
+
try {
|
|
42
|
+
if (axiosInstance.__flowDebuggerPatched) return;
|
|
43
|
+
axiosInstance.__flowDebuggerPatched = true;
|
|
44
|
+
|
|
45
|
+
const config = { ...DEFAULT_CONFIG, ...options?.config };
|
|
46
|
+
const serviceMap = { ...DEFAULT_SERVICE_MAP, ...options?.serviceMap };
|
|
47
|
+
|
|
48
|
+
// Request interceptor — attach start time
|
|
49
|
+
axiosInstance.interceptors.request.use(
|
|
50
|
+
(reqConfig: any) => {
|
|
51
|
+
reqConfig.__flowDebuggerStartTime = performance.now();
|
|
52
|
+
return reqConfig;
|
|
53
|
+
},
|
|
54
|
+
(error: any) => Promise.reject(error),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Response interceptor — record success
|
|
58
|
+
axiosInstance.interceptors.response.use(
|
|
59
|
+
(response: any) => {
|
|
60
|
+
const tracer = options?.getTracer?.();
|
|
61
|
+
if (!tracer) return response;
|
|
62
|
+
|
|
63
|
+
const reqConfig = response.config;
|
|
64
|
+
const startTime = reqConfig.__flowDebuggerStartTime || performance.now();
|
|
65
|
+
const endTime = performance.now();
|
|
66
|
+
const duration = endTime - startTime;
|
|
67
|
+
|
|
68
|
+
const url = reqConfig.url || 'unknown';
|
|
69
|
+
const method = (reqConfig.method || 'GET').toUpperCase();
|
|
70
|
+
const service = getServiceFromUrl(url, serviceMap);
|
|
71
|
+
const stepName = `Axios ${method} ${getDomain(url)}`;
|
|
72
|
+
|
|
73
|
+
const classification = classifyQuery(duration, 'success', config);
|
|
74
|
+
|
|
75
|
+
tracer.addStep({
|
|
76
|
+
name: stepName,
|
|
77
|
+
service,
|
|
78
|
+
status: 'success',
|
|
79
|
+
classification,
|
|
80
|
+
startTime,
|
|
81
|
+
endTime,
|
|
82
|
+
duration,
|
|
83
|
+
metadata: { url, method, statusCode: response.status },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return response;
|
|
87
|
+
},
|
|
88
|
+
(error: any) => {
|
|
89
|
+
const tracer = options?.getTracer?.();
|
|
90
|
+
if (!tracer) return Promise.reject(error);
|
|
91
|
+
|
|
92
|
+
const reqConfig = error.config || {};
|
|
93
|
+
const startTime = reqConfig.__flowDebuggerStartTime || performance.now();
|
|
94
|
+
const endTime = performance.now();
|
|
95
|
+
const duration = endTime - startTime;
|
|
96
|
+
|
|
97
|
+
const url = reqConfig.url || 'unknown';
|
|
98
|
+
const method = (reqConfig.method || 'GET').toUpperCase();
|
|
99
|
+
const service = getServiceFromUrl(url, serviceMap);
|
|
100
|
+
const stepName = `Axios ${method} ${getDomain(url)}`;
|
|
101
|
+
|
|
102
|
+
let status: StepStatus = 'error';
|
|
103
|
+
let errorMsg = error.message || 'Request failed';
|
|
104
|
+
|
|
105
|
+
// Detect timeouts
|
|
106
|
+
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || errorMsg.includes('timeout')) {
|
|
107
|
+
status = 'timeout';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// HTTP error status
|
|
111
|
+
if (error.response) {
|
|
112
|
+
errorMsg = `HTTP ${error.response.status} ${error.response.statusText || ''}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const classification = classifyQuery(duration, status, config);
|
|
116
|
+
|
|
117
|
+
tracer.addStep({
|
|
118
|
+
name: stepName,
|
|
119
|
+
service,
|
|
120
|
+
status,
|
|
121
|
+
classification,
|
|
122
|
+
startTime,
|
|
123
|
+
endTime,
|
|
124
|
+
duration,
|
|
125
|
+
error: errorMsg,
|
|
126
|
+
stackTrace: error.stack,
|
|
127
|
+
metadata: { url, method, statusCode: error.response?.status },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return Promise.reject(error);
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
} catch (_) {
|
|
134
|
+
// Production-safe: never crash
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Extract service tag from URL */
|
|
139
|
+
function getServiceFromUrl(url: string, serviceMap: Record<string, ServiceTag>): ServiceTag {
|
|
140
|
+
try {
|
|
141
|
+
const urlObj = new URL(url);
|
|
142
|
+
const hostname = urlObj.hostname;
|
|
143
|
+
|
|
144
|
+
if (serviceMap[hostname]) return serviceMap[hostname];
|
|
145
|
+
|
|
146
|
+
for (const [pattern, service] of Object.entries(serviceMap)) {
|
|
147
|
+
if (hostname.includes(pattern)) return service;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return 'axios';
|
|
151
|
+
} catch {
|
|
152
|
+
return 'axios';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Extract domain from URL for display */
|
|
157
|
+
function getDomain(url: string): string {
|
|
158
|
+
try {
|
|
159
|
+
const urlObj = new URL(url);
|
|
160
|
+
return urlObj.hostname + urlObj.pathname.substring(0, 30);
|
|
161
|
+
} catch {
|
|
162
|
+
return url.substring(0, 50);
|
|
163
|
+
}
|
|
164
|
+
}
|