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,217 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // flow-debugger — PostgreSQL Auto-Instrument (pg)
3
+ // Patches pg Pool/Client to trace all queries
4
+ // ─────────────────────────────────────────────────────────────
5
+
6
+ import { RequestTracer } from '../core/TraceEngine';
7
+ import { TraceStep, StepStatus, DebuggerConfig, DEFAULT_CONFIG } from '../core/types';
8
+ import { classifyQuery } from '../core/Classifier';
9
+
10
+ type AnyPool = any;
11
+
12
+ interface PgTracerOptions {
13
+ config?: Partial<DebuggerConfig>;
14
+ getTracer?: () => RequestTracer | null;
15
+ }
16
+
17
+ /**
18
+ * Auto-instrument pg Pool/Client to trace all queries.
19
+ *
20
+ * Usage:
21
+ * pgTracer(pool, { getTracer: () => currentTracer })
22
+ *
23
+ * Output:
24
+ * Postgres SELECT orders → 18ms ✔
25
+ * Postgres UPDATE invoice → timeout ❌
26
+ */
27
+ export function pgTracer(pool: AnyPool, options?: PgTracerOptions): void {
28
+ try {
29
+ if (pool.__flowDebuggerPatched) return;
30
+ pool.__flowDebuggerPatched = true;
31
+
32
+ const config = { ...DEFAULT_CONFIG, ...options?.config };
33
+ const originalQuery = pool.query.bind(pool);
34
+
35
+ pool.query = function tracedQuery(...args: any[]) {
36
+ const tracer = options?.getTracer?.();
37
+ if (!tracer) {
38
+ return originalQuery(...args);
39
+ }
40
+
41
+ const sql = extractSql(args);
42
+ const operation = extractPgOperation(sql);
43
+ const stepName = `Postgres ${operation}`;
44
+ const startTime = performance.now();
45
+
46
+ // Detect if callback is provided
47
+ const lastArg = args[args.length - 1];
48
+ if (typeof lastArg === 'function') {
49
+ // Callback style
50
+ const callback = lastArg;
51
+ args[args.length - 1] = function (err: any, result: any) {
52
+ recordStep(tracer, stepName, startTime, err, sql, operation, config);
53
+ callback(err, result);
54
+ };
55
+ return originalQuery(...args);
56
+ }
57
+
58
+ // Promise style
59
+ const promise = originalQuery(...args);
60
+ if (promise && typeof promise.then === 'function') {
61
+ return promise.then(
62
+ (result: any) => {
63
+ recordStep(tracer, stepName, startTime, null, sql, operation, config);
64
+ return result;
65
+ },
66
+ (err: any) => {
67
+ recordStep(tracer, stepName, startTime, err, sql, operation, config);
68
+ throw err;
69
+ },
70
+ );
71
+ }
72
+ return promise;
73
+ };
74
+
75
+ // Also patch connect() to instrument clients from the pool
76
+ if (pool.connect && !pool.__connectPatched) {
77
+ pool.__connectPatched = true;
78
+ const originalConnect = pool.connect.bind(pool);
79
+
80
+ pool.connect = function tracedConnect(...connectArgs: any[]) {
81
+ const lastArg = connectArgs[connectArgs.length - 1];
82
+
83
+ if (typeof lastArg === 'function') {
84
+ // Callback style: connect((err, client, done) => { ... })
85
+ const callback = lastArg;
86
+ connectArgs[connectArgs.length - 1] = function (err: any, client: any, done: any) {
87
+ if (client && !client.__flowDebuggerPatched) {
88
+ patchClient(client, options, config);
89
+ }
90
+ callback(err, client, done);
91
+ };
92
+ return originalConnect(...connectArgs);
93
+ }
94
+
95
+ // Promise style
96
+ const promise = originalConnect(...connectArgs);
97
+ if (promise && typeof promise.then === 'function') {
98
+ return promise.then((client: any) => {
99
+ if (client && !client.__flowDebuggerPatched) {
100
+ patchClient(client, options, config);
101
+ }
102
+ return client;
103
+ });
104
+ }
105
+ return promise;
106
+ };
107
+ }
108
+ } catch (_) {
109
+ // Production-safe: never crash
110
+ }
111
+ }
112
+
113
+ /** Patch an individual pg Client's query method */
114
+ function patchClient(client: any, options: PgTracerOptions | undefined, config: Required<DebuggerConfig>): void {
115
+ try {
116
+ client.__flowDebuggerPatched = true;
117
+ const originalClientQuery = client.query.bind(client);
118
+
119
+ client.query = function tracedClientQuery(...args: any[]) {
120
+ const tracer = options?.getTracer?.();
121
+ if (!tracer) {
122
+ return originalClientQuery(...args);
123
+ }
124
+
125
+ const sql = extractSql(args);
126
+ const operation = extractPgOperation(sql);
127
+ const stepName = `Postgres ${operation}`;
128
+ const startTime = performance.now();
129
+
130
+ const lastArg = args[args.length - 1];
131
+ if (typeof lastArg === 'function') {
132
+ const callback = lastArg;
133
+ args[args.length - 1] = function (err: any, result: any) {
134
+ recordStep(tracer, stepName, startTime, err, sql, operation, config);
135
+ callback(err, result);
136
+ };
137
+ return originalClientQuery(...args);
138
+ }
139
+
140
+ const promise = originalClientQuery(...args);
141
+ if (promise && typeof promise.then === 'function') {
142
+ return promise.then(
143
+ (result: any) => {
144
+ recordStep(tracer, stepName, startTime, null, sql, operation, config);
145
+ return result;
146
+ },
147
+ (err: any) => {
148
+ recordStep(tracer, stepName, startTime, err, sql, operation, config);
149
+ throw err;
150
+ },
151
+ );
152
+ }
153
+ return promise;
154
+ };
155
+ } catch (_) { }
156
+ }
157
+
158
+ /** Record a step from query execution */
159
+ function recordStep(
160
+ tracer: RequestTracer,
161
+ stepName: string,
162
+ startTime: number,
163
+ err: any,
164
+ sql: string,
165
+ operation: string,
166
+ config: Required<DebuggerConfig>,
167
+ ): void {
168
+ try {
169
+ const endTime = performance.now();
170
+ const duration = endTime - startTime;
171
+ const status: StepStatus = err ? 'error' : 'success';
172
+ const classification = classifyQuery(duration, status, config);
173
+
174
+ const step: TraceStep = {
175
+ name: stepName,
176
+ service: 'postgres',
177
+ status,
178
+ classification,
179
+ startTime,
180
+ endTime,
181
+ duration,
182
+ error: err?.message,
183
+ stackTrace: err?.stack,
184
+ metadata: { sql: sql.substring(0, 200), operation },
185
+ };
186
+
187
+ tracer.addStep(step);
188
+
189
+ if (duration > (config.slowQueryThreshold ?? 300)) {
190
+ try {
191
+ (config.logger || console.log)(
192
+ `\x1b[33m⚠ Slow PostgreSQL query: ${stepName} (${Math.round(duration)}ms)\n ${sql.substring(0, 100)}\x1b[0m`
193
+ );
194
+ } catch (_) { }
195
+ }
196
+ } catch (_) { }
197
+ }
198
+
199
+ /** Extract SQL string from query arguments */
200
+ function extractSql(args: any[]): string {
201
+ if (typeof args[0] === 'string') return args[0];
202
+ if (args[0] && typeof args[0] === 'object' && args[0].text) return args[0].text;
203
+ return 'unknown';
204
+ }
205
+
206
+ /** Extract the SQL operation type from a query string */
207
+ function extractPgOperation(sql: string): string {
208
+ const trimmed = sql.trim().toUpperCase();
209
+ const match = trimmed.match(/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE|WITH)\b/);
210
+ if (match) {
211
+ const op = match[1];
212
+ const tableMatch = sql.match(/(?:FROM|INTO|UPDATE|TABLE|JOIN)\s+"?(\w+)"?/i);
213
+ const table = tableMatch ? tableMatch[1] : '';
214
+ return table ? `${op} ${table}` : op;
215
+ }
216
+ return sql.substring(0, 30);
217
+ }
@@ -0,0 +1,122 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // flow-debugger — Redis Auto-Instrument
3
+ // Wraps Redis client commands to trace all operations
4
+ // ─────────────────────────────────────────────────────────────
5
+
6
+ import { RequestTracer } from '../core/TraceEngine';
7
+ import { TraceStep, StepStatus, DebuggerConfig, DEFAULT_CONFIG } from '../core/types';
8
+ import { classifyQuery } from '../core/Classifier';
9
+
10
+ type AnyRedis = any;
11
+
12
+ interface RedisTracerOptions {
13
+ config?: Partial<DebuggerConfig>;
14
+ getTracer?: () => RequestTracer | null;
15
+ }
16
+
17
+ /** Commands to trace */
18
+ const TRACED_COMMANDS = [
19
+ 'get', 'set', 'del', 'exists', 'expire', 'ttl',
20
+ 'hget', 'hset', 'hdel', 'hgetall', 'hmset', 'hmget',
21
+ 'lpush', 'rpush', 'lpop', 'rpop', 'lrange', 'llen',
22
+ 'sadd', 'srem', 'smembers', 'sismember',
23
+ 'zadd', 'zrem', 'zrange', 'zrank', 'zscore',
24
+ 'incr', 'decr', 'incrby', 'decrby',
25
+ 'setex', 'setnx', 'mget', 'mset',
26
+ 'publish', 'subscribe',
27
+ 'eval', 'evalsha',
28
+ ];
29
+
30
+ /**
31
+ * Auto-instrument a Redis client (ioredis or node-redis v4).
32
+ *
33
+ * Usage:
34
+ * redisTracer(redisClient, { getTracer: () => currentTracer })
35
+ *
36
+ * Output:
37
+ * Redis SET session → 3ms ✔
38
+ * Redis GET cache → timeout ❌
39
+ */
40
+ export function redisTracer(client: AnyRedis, options?: RedisTracerOptions): void {
41
+ try {
42
+ if (client.__flowDebuggerPatched) return;
43
+ client.__flowDebuggerPatched = true;
44
+
45
+ const config = { ...DEFAULT_CONFIG, ...options?.config };
46
+
47
+ for (const command of TRACED_COMMANDS) {
48
+ if (typeof client[command] !== 'function') continue;
49
+
50
+ const original = client[command].bind(client);
51
+
52
+ client[command] = async function tracedCommand(...args: any[]) {
53
+ const tracer = options?.getTracer?.();
54
+ if (!tracer) {
55
+ return original(...args);
56
+ }
57
+
58
+ const key = args[0] !== undefined ? String(args[0]).substring(0, 50) : '';
59
+ const stepName = `Redis ${command.toUpperCase()} ${key}`;
60
+ const startTime = performance.now();
61
+
62
+ let status: StepStatus = 'success';
63
+ let error: string | undefined;
64
+ let stackTrace: string | undefined;
65
+
66
+ try {
67
+ const result = await original(...args);
68
+ const endTime = performance.now();
69
+ const duration = endTime - startTime;
70
+ const classification = classifyQuery(duration, status, config);
71
+
72
+ tracer.addStep({
73
+ name: stepName,
74
+ service: 'redis',
75
+ status,
76
+ classification,
77
+ startTime,
78
+ endTime,
79
+ duration,
80
+ metadata: { command: command.toUpperCase(), key },
81
+ });
82
+
83
+ return result;
84
+ } catch (err: unknown) {
85
+ const e = err instanceof Error ? err : new Error(String(err));
86
+ error = e.message;
87
+ stackTrace = e.stack;
88
+
89
+ // Detect connection errors as CRITICAL
90
+ const isConnectionError = error.includes('ECONNREFUSED') ||
91
+ error.includes('ECONNRESET') ||
92
+ error.includes('ETIMEDOUT') ||
93
+ error.includes('connection') ||
94
+ error.includes('Connection');
95
+
96
+ status = isConnectionError ? 'timeout' : 'error';
97
+
98
+ const endTime = performance.now();
99
+ const duration = endTime - startTime;
100
+ const classification = classifyQuery(duration, status, config);
101
+
102
+ tracer.addStep({
103
+ name: stepName,
104
+ service: 'redis',
105
+ status,
106
+ classification,
107
+ startTime,
108
+ endTime,
109
+ duration,
110
+ error,
111
+ stackTrace,
112
+ metadata: { command: command.toUpperCase(), key },
113
+ });
114
+
115
+ throw err;
116
+ }
117
+ };
118
+ }
119
+ } catch (_) {
120
+ // Production-safe: never crash
121
+ }
122
+ }
@@ -0,0 +1,264 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // flow-debugger — Express Middleware
3
+ // Auto-traces every request, provides /__debugger API + dashboard
4
+ // ─────────────────────────────────────────────────────────────
5
+
6
+ import { TraceEngine, RequestTracer } from '../core/TraceEngine';
7
+ import { Analytics } from '../core/Analytics';
8
+ import { Sampler } from '../core/Sampler';
9
+ import { DebuggerConfig, DEFAULT_CONFIG } from '../core/types';
10
+ import * as path from 'path';
11
+ import * as fs from 'fs';
12
+
13
+ type ExpressApp = any;
14
+ type Request = any;
15
+ type Response = any;
16
+ type NextFunction = any;
17
+
18
+ // AsyncLocalStorage for automatic tracer context propagation
19
+ let asyncLocalStorage: any;
20
+ try {
21
+ const { AsyncLocalStorage } = require('async_hooks');
22
+ asyncLocalStorage = new AsyncLocalStorage();
23
+ } catch (_) {
24
+ // Fallback for environments without async_hooks
25
+ asyncLocalStorage = null;
26
+ }
27
+
28
+ /** Per-request extended request */
29
+ declare global {
30
+ namespace Express {
31
+ interface Request {
32
+ tracer?: RequestTracer;
33
+ traceId?: string;
34
+ }
35
+ }
36
+ }
37
+
38
+ export interface FlowDebuggerMiddleware {
39
+ middleware: (req: Request, res: Response, next: NextFunction) => void;
40
+ engine: TraceEngine;
41
+ analytics: Analytics;
42
+ /** Get current active tracer (for use in integrations) */
43
+ getTracer: () => RequestTracer | null;
44
+ }
45
+
46
+ /**
47
+ * Create the flow-debugger Express middleware.
48
+ *
49
+ * Usage:
50
+ * const debugger = flowDebugger({ slowThreshold: 500 });
51
+ * app.use(debugger.middleware);
52
+ *
53
+ * // Auto-instrument databases
54
+ * mongoTracer(mongoose, { getTracer: debugger.getTracer });
55
+ * mysqlTracer(pool, { getTracer: debugger.getTracer });
56
+ *
57
+ * // Dashboard at: GET /__debugger/dashboard
58
+ * // API at: GET /__debugger
59
+ */
60
+ export function flowDebugger(config?: DebuggerConfig): FlowDebuggerMiddleware {
61
+ const mergedConfig = { ...DEFAULT_CONFIG, ...config };
62
+ const engine = new TraceEngine(mergedConfig);
63
+ const analytics = new Analytics(mergedConfig.maxTraces);
64
+ const sampler = new Sampler(mergedConfig.samplingRate, mergedConfig.alwaysSampleErrors);
65
+
66
+ // Current tracer per async context
67
+ let fallbackTracer: RequestTracer | null = null;
68
+
69
+ const getTracer = (): RequestTracer | null => {
70
+ if (asyncLocalStorage) {
71
+ try {
72
+ const store = asyncLocalStorage.getStore();
73
+ return store?.tracer || null;
74
+ } catch (_) { }
75
+ }
76
+ return fallbackTracer;
77
+ };
78
+
79
+ const middleware = (req: Request, res: Response, next: NextFunction): void => {
80
+ try {
81
+ // Skip debugger endpoints
82
+ if (req.path?.startsWith('/__debugger')) {
83
+ return handleDebuggerRoute(req, res, next, analytics, mergedConfig);
84
+ }
85
+
86
+ if (!mergedConfig.enabled) return next();
87
+
88
+ // Sampling
89
+ if (!sampler.shouldSample()) return next();
90
+
91
+ const tracer = engine.startTrace(req.path || req.url, req.method);
92
+ req.tracer = tracer;
93
+ req.traceId = tracer.getTraceId();
94
+
95
+ // Set header for client correlation
96
+ res.setHeader('X-Trace-Id', tracer.getTraceId());
97
+
98
+ const run = () => {
99
+ fallbackTracer = tracer;
100
+
101
+ // Calculate payload size if body exists
102
+ let payloadSize: number | undefined;
103
+ try {
104
+ if (req.body) {
105
+ const bodyStr = JSON.stringify(req.body);
106
+ payloadSize = Buffer.byteLength(bodyStr, 'utf8');
107
+ }
108
+ } catch (_) {
109
+ // ignore
110
+ }
111
+
112
+ // Hook into response finish
113
+ const originalEnd = res.end;
114
+ res.end = function (...args: any[]) {
115
+ try {
116
+ const trace = tracer.end(res.statusCode);
117
+
118
+ // Add environment and payload size to trace
119
+ trace.environment = mergedConfig.environment;
120
+ trace.payloadSize = payloadSize;
121
+
122
+ analytics.record(trace);
123
+
124
+ // If error and we had skipped sampling, re-sample
125
+ } catch (_) {
126
+ // never crash
127
+ }
128
+
129
+ fallbackTracer = null;
130
+ return originalEnd.apply(res, args);
131
+ };
132
+
133
+ next();
134
+ };
135
+
136
+ // Use AsyncLocalStorage if available
137
+ if (asyncLocalStorage) {
138
+ asyncLocalStorage.run({ tracer }, run);
139
+ } else {
140
+ run();
141
+ }
142
+ } catch (_) {
143
+ // Production-safe: if debugger fails, pass through
144
+ next();
145
+ }
146
+ };
147
+
148
+ return { middleware, engine, analytics, getTracer };
149
+ }
150
+
151
+ /**
152
+ * Handle /__debugger routes
153
+ */
154
+ function handleDebuggerRoute(
155
+ req: Request,
156
+ res: Response,
157
+ next: NextFunction,
158
+ analytics: Analytics,
159
+ config: Required<DebuggerConfig>,
160
+ ): void {
161
+ if (!config.enableDashboard) return next();
162
+
163
+ try {
164
+ const subPath = req.path.replace('/__debugger', '') || '/';
165
+
166
+ switch (subPath) {
167
+ case '/':
168
+ case '/api':
169
+ // JSON analytics API
170
+ res.json(analytics.getReport());
171
+ break;
172
+
173
+ case '/dashboard': {
174
+ // Serve the HTML dashboard
175
+ const dashboardPath = path.resolve(__dirname, '../../dashboard/index.html');
176
+ if (fs.existsSync(dashboardPath)) {
177
+ res.sendFile(dashboardPath);
178
+ } else {
179
+ // Fallback: inline minimal dashboard
180
+ res.send(getInlineDashboard());
181
+ }
182
+ break;
183
+ }
184
+
185
+ case '/dashboard/style.css': {
186
+ const cssPath = path.resolve(__dirname, '../../dashboard/style.css');
187
+ if (fs.existsSync(cssPath)) {
188
+ res.type('text/css').sendFile(cssPath);
189
+ } else {
190
+ res.status(404).send('Not found');
191
+ }
192
+ break;
193
+ }
194
+
195
+ case '/dashboard/app.js': {
196
+ const jsPath = path.resolve(__dirname, '../../dashboard/app.js');
197
+ if (fs.existsSync(jsPath)) {
198
+ res.type('application/javascript').sendFile(jsPath);
199
+ } else {
200
+ res.status(404).send('Not found');
201
+ }
202
+ break;
203
+ }
204
+
205
+ case '/health':
206
+ res.json(analytics.getHealthMonitor().getAllHealth());
207
+ break;
208
+
209
+ case '/endpoint': {
210
+ const endpoint = req.query?.path || req.query?.endpoint;
211
+ if (endpoint) {
212
+ const report = analytics.getEndpointReport(String(endpoint));
213
+ res.json(report || { error: 'Endpoint not found' });
214
+ } else {
215
+ res.json({ error: 'Provide ?path=/your/endpoint' });
216
+ }
217
+ break;
218
+ }
219
+
220
+ case '/search': {
221
+ const query = req.query?.q || req.query?.query;
222
+ const env = req.query?.env;
223
+ const limit = req.query?.limit ? parseInt(String(req.query.limit), 10) : 50;
224
+
225
+ if (query) {
226
+ const results = analytics.searchTraces(String(query), { env: env ? String(env) : undefined, limit });
227
+ res.json({ query, results, count: results.length });
228
+ } else {
229
+ res.json({ error: 'Provide ?q=search_term' });
230
+ }
231
+ break;
232
+ }
233
+
234
+ default:
235
+ next();
236
+ }
237
+ } catch (err) {
238
+ res.status(500).json({ error: 'Debugger dashboard error' });
239
+ }
240
+ }
241
+
242
+ /** Inline fallback dashboard if files aren't found */
243
+ function getInlineDashboard(): string {
244
+ return `<!DOCTYPE html>
245
+ <html lang="en">
246
+ <head>
247
+ <meta charset="UTF-8">
248
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
249
+ <title>Flow Debugger Dashboard</title>
250
+ <style>body{font-family:system-ui;background:#0a0a1a;color:#e0e0e0;padding:20px}
251
+ h1{color:#7c3aed}.card{background:#1a1a2e;border-radius:12px;padding:20px;margin:10px 0;border:1px solid #2a2a4a}</style>
252
+ </head>
253
+ <body>
254
+ <h1>🔍 Flow Debugger</h1>
255
+ <p>Dashboard files not found. API available at <a href="/__debugger" style="color:#7c3aed">/__debugger</a></p>
256
+ <div class="card" id="data">Loading...</div>
257
+ <script>
258
+ fetch('/__debugger').then(r=>r.json()).then(d=>{
259
+ document.getElementById('data').innerHTML='<pre>'+JSON.stringify(d,null,2)+'</pre>';
260
+ });
261
+ </script>
262
+ </body>
263
+ </html>`;
264
+ }