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.
Files changed (131) hide show
  1. package/PORTFOLIO_README_SECTION.md +177 -0
  2. package/README.md +251 -0
  3. package/dashboard/app.js +339 -0
  4. package/dashboard/index.html +168 -0
  5. package/dashboard/style.css +846 -0
  6. package/dist/cjs/core/Analytics.js +174 -0
  7. package/dist/cjs/core/Analytics.js.map +1 -0
  8. package/dist/cjs/core/Classifier.js +66 -0
  9. package/dist/cjs/core/Classifier.js.map +1 -0
  10. package/dist/cjs/core/HealthMonitor.js +79 -0
  11. package/dist/cjs/core/HealthMonitor.js.map +1 -0
  12. package/dist/cjs/core/RootCause.js +89 -0
  13. package/dist/cjs/core/RootCause.js.map +1 -0
  14. package/dist/cjs/core/Sampler.js +34 -0
  15. package/dist/cjs/core/Sampler.js.map +1 -0
  16. package/dist/cjs/core/Timeline.js +90 -0
  17. package/dist/cjs/core/Timeline.js.map +1 -0
  18. package/dist/cjs/core/TraceEngine.js +222 -0
  19. package/dist/cjs/core/TraceEngine.js.map +1 -0
  20. package/dist/cjs/core/types.js +21 -0
  21. package/dist/cjs/core/types.js.map +1 -0
  22. package/dist/cjs/index.js +46 -0
  23. package/dist/cjs/index.js.map +1 -0
  24. package/dist/cjs/integrations/axios.js +136 -0
  25. package/dist/cjs/integrations/axios.js.map +1 -0
  26. package/dist/cjs/integrations/fetch.js +153 -0
  27. package/dist/cjs/integrations/fetch.js.map +1 -0
  28. package/dist/cjs/integrations/mongo.js +111 -0
  29. package/dist/cjs/integrations/mongo.js.map +1 -0
  30. package/dist/cjs/integrations/mysql.js +212 -0
  31. package/dist/cjs/integrations/mysql.js.map +1 -0
  32. package/dist/cjs/integrations/postgres.js +182 -0
  33. package/dist/cjs/integrations/postgres.js.map +1 -0
  34. package/dist/cjs/integrations/redis.js +105 -0
  35. package/dist/cjs/integrations/redis.js.map +1 -0
  36. package/dist/cjs/middleware/express.js +255 -0
  37. package/dist/cjs/middleware/express.js.map +1 -0
  38. package/dist/esm/core/Analytics.js +170 -0
  39. package/dist/esm/core/Analytics.js.map +1 -0
  40. package/dist/esm/core/Classifier.js +61 -0
  41. package/dist/esm/core/Classifier.js.map +1 -0
  42. package/dist/esm/core/HealthMonitor.js +75 -0
  43. package/dist/esm/core/HealthMonitor.js.map +1 -0
  44. package/dist/esm/core/RootCause.js +86 -0
  45. package/dist/esm/core/RootCause.js.map +1 -0
  46. package/dist/esm/core/Sampler.js +30 -0
  47. package/dist/esm/core/Sampler.js.map +1 -0
  48. package/dist/esm/core/Timeline.js +86 -0
  49. package/dist/esm/core/Timeline.js.map +1 -0
  50. package/dist/esm/core/TraceEngine.js +217 -0
  51. package/dist/esm/core/TraceEngine.js.map +1 -0
  52. package/dist/esm/core/types.js +18 -0
  53. package/dist/esm/core/types.js.map +1 -0
  54. package/dist/esm/index.js +22 -0
  55. package/dist/esm/index.js.map +1 -0
  56. package/dist/esm/integrations/axios.js +133 -0
  57. package/dist/esm/integrations/axios.js.map +1 -0
  58. package/dist/esm/integrations/fetch.js +149 -0
  59. package/dist/esm/integrations/fetch.js.map +1 -0
  60. package/dist/esm/integrations/mongo.js +107 -0
  61. package/dist/esm/integrations/mongo.js.map +1 -0
  62. package/dist/esm/integrations/mysql.js +209 -0
  63. package/dist/esm/integrations/mysql.js.map +1 -0
  64. package/dist/esm/integrations/postgres.js +179 -0
  65. package/dist/esm/integrations/postgres.js.map +1 -0
  66. package/dist/esm/integrations/redis.js +102 -0
  67. package/dist/esm/integrations/redis.js.map +1 -0
  68. package/dist/esm/middleware/express.js +219 -0
  69. package/dist/esm/middleware/express.js.map +1 -0
  70. package/dist/types/core/Analytics.d.ts +35 -0
  71. package/dist/types/core/Analytics.d.ts.map +1 -0
  72. package/dist/types/core/Classifier.d.ts +21 -0
  73. package/dist/types/core/Classifier.d.ts.map +1 -0
  74. package/dist/types/core/HealthMonitor.d.ts +14 -0
  75. package/dist/types/core/HealthMonitor.d.ts.map +1 -0
  76. package/dist/types/core/RootCause.d.ts +12 -0
  77. package/dist/types/core/RootCause.d.ts.map +1 -0
  78. package/dist/types/core/Sampler.d.ts +13 -0
  79. package/dist/types/core/Sampler.d.ts.map +1 -0
  80. package/dist/types/core/Timeline.d.ts +22 -0
  81. package/dist/types/core/Timeline.d.ts.map +1 -0
  82. package/dist/types/core/TraceEngine.d.ts +47 -0
  83. package/dist/types/core/TraceEngine.d.ts.map +1 -0
  84. package/dist/types/core/types.d.ts +118 -0
  85. package/dist/types/core/types.d.ts.map +1 -0
  86. package/dist/types/index.d.ts +18 -0
  87. package/dist/types/index.d.ts.map +1 -0
  88. package/dist/types/integrations/axios.d.ts +22 -0
  89. package/dist/types/integrations/axios.d.ts.map +1 -0
  90. package/dist/types/integrations/fetch.d.ts +25 -0
  91. package/dist/types/integrations/fetch.d.ts.map +1 -0
  92. package/dist/types/integrations/mongo.d.ts +26 -0
  93. package/dist/types/integrations/mongo.d.ts.map +1 -0
  94. package/dist/types/integrations/mysql.d.ts +20 -0
  95. package/dist/types/integrations/mysql.d.ts.map +1 -0
  96. package/dist/types/integrations/postgres.d.ts +20 -0
  97. package/dist/types/integrations/postgres.d.ts.map +1 -0
  98. package/dist/types/integrations/redis.d.ts +20 -0
  99. package/dist/types/integrations/redis.d.ts.map +1 -0
  100. package/dist/types/middleware/express.d.ts +39 -0
  101. package/dist/types/middleware/express.d.ts.map +1 -0
  102. package/example/server.ts +234 -0
  103. package/jest.config.js +8 -0
  104. package/package.json +110 -0
  105. package/portfolio-repo/APIRESPONSE DASH.png +0 -0
  106. package/portfolio-repo/PAYLOAD.png +0 -0
  107. package/portfolio-repo/README.md +182 -0
  108. package/src/core/Analytics.ts +209 -0
  109. package/src/core/Classifier.ts +82 -0
  110. package/src/core/HealthMonitor.ts +92 -0
  111. package/src/core/RootCause.ts +105 -0
  112. package/src/core/Sampler.ts +35 -0
  113. package/src/core/Timeline.ts +108 -0
  114. package/src/core/TraceEngine.ts +266 -0
  115. package/src/core/types.ts +170 -0
  116. package/src/index.ts +42 -0
  117. package/src/integrations/axios.ts +164 -0
  118. package/src/integrations/fetch.ts +172 -0
  119. package/src/integrations/mongo.ts +130 -0
  120. package/src/integrations/mysql.ts +239 -0
  121. package/src/integrations/postgres.ts +217 -0
  122. package/src/integrations/redis.ts +122 -0
  123. package/src/middleware/express.ts +264 -0
  124. package/tests/Analytics.test.ts +136 -0
  125. package/tests/Classifier.test.ts +57 -0
  126. package/tests/RootCause.test.ts +69 -0
  127. package/tests/TraceEngine.test.ts +110 -0
  128. package/tsconfig.cjs.json +9 -0
  129. package/tsconfig.esm.json +9 -0
  130. package/tsconfig.json +31 -0
  131. 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
+ }