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,172 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // flow-debugger — Fetch API Auto-Instrument
3
+ // Wraps global fetch to trace all HTTP 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
+ interface FetchTracerOptions {
11
+ config?: Partial<DebuggerConfig>;
12
+ getTracer?: () => RequestTracer | null;
13
+ /** Map URL patterns to service tags (e.g. 'stripe.com' -> 'stripe') */
14
+ serviceMap?: Record<string, ServiceTag>;
15
+ }
16
+
17
+ const DEFAULT_SERVICE_MAP: Record<string, ServiceTag> = {
18
+ 'stripe.com': 'stripe',
19
+ 'api.stripe.com': 'stripe',
20
+ 'razorpay.com': 'razorpay',
21
+ 'api.razorpay.com': 'razorpay',
22
+ 'sendgrid.com': 'sendgrid',
23
+ 'api.sendgrid.com': 'sendgrid',
24
+ 'twilio.com': 'twilio',
25
+ 'api.twilio.com': 'twilio',
26
+ };
27
+
28
+ let originalFetch: typeof fetch;
29
+
30
+ /**
31
+ * Auto-instrument global fetch to trace all HTTP requests.
32
+ *
33
+ * Usage:
34
+ * fetchTracer({ getTracer: () => currentTracer, serviceMap: { 'myapi.com': 'external' } })
35
+ *
36
+ * Output:
37
+ * Fetch POST https://api.stripe.com/v1/charges → 234ms ✔ [stripe]
38
+ * Fetch GET https://api.razorpay.com/orders → timeout ❌ [razorpay]
39
+ */
40
+ export function fetchTracer(options?: FetchTracerOptions): void {
41
+ try {
42
+ if (typeof globalThis.fetch === 'undefined') return;
43
+ if ((globalThis.fetch as any).__flowDebuggerPatched) return;
44
+
45
+ const config = { ...DEFAULT_CONFIG, ...options?.config };
46
+ const serviceMap = { ...DEFAULT_SERVICE_MAP, ...options?.serviceMap };
47
+
48
+ originalFetch = globalThis.fetch;
49
+
50
+ globalThis.fetch = async function tracedFetch(input: string | Request | URL, init?: RequestInit): Promise<Response> {
51
+ const tracer = options?.getTracer?.();
52
+ if (!tracer) {
53
+ return originalFetch(input, init);
54
+ }
55
+
56
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
57
+ const method = init?.method || 'GET';
58
+ const service = getServiceFromUrl(url, serviceMap);
59
+ const stepName = `Fetch ${method} ${getDomain(url)}`;
60
+ const startTime = performance.now();
61
+
62
+ let status: StepStatus = 'success';
63
+ let error: string | undefined;
64
+ let stackTrace: string | undefined;
65
+ let response: Response;
66
+
67
+ try {
68
+ response = await originalFetch(input, init);
69
+
70
+ // HTTP errors (4xx, 5xx) are not thrown by fetch, but we should mark them
71
+ if (!response.ok) {
72
+ status = 'error';
73
+ error = `HTTP ${response.status} ${response.statusText}`;
74
+ }
75
+ } catch (err: unknown) {
76
+ status = 'error';
77
+ const e = err instanceof Error ? err : new Error(String(err));
78
+ error = e.message;
79
+ stackTrace = e.stack;
80
+
81
+ // Detect network errors as timeouts
82
+ if (error.includes('fetch failed') || error.includes('network') || error.includes('ECONNREFUSED')) {
83
+ status = 'timeout';
84
+ }
85
+
86
+ const endTime = performance.now();
87
+ const duration = endTime - startTime;
88
+ const classification = classifyQuery(duration, status, config);
89
+
90
+ tracer.addStep({
91
+ name: stepName,
92
+ service,
93
+ status,
94
+ classification,
95
+ startTime,
96
+ endTime,
97
+ duration,
98
+ error,
99
+ stackTrace,
100
+ metadata: { url, method, service },
101
+ });
102
+
103
+ throw err;
104
+ }
105
+
106
+ const endTime = performance.now();
107
+ const duration = endTime - startTime;
108
+ const classification = classifyQuery(duration, status, config);
109
+
110
+ tracer.addStep({
111
+ name: stepName,
112
+ service,
113
+ status,
114
+ classification,
115
+ startTime,
116
+ endTime,
117
+ duration,
118
+ error,
119
+ metadata: { url, method, statusCode: response.status },
120
+ });
121
+
122
+ return response;
123
+ };
124
+
125
+ (globalThis.fetch as any).__flowDebuggerPatched = true;
126
+ } catch (_) {
127
+ // Production-safe: never crash
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Remove fetch tracer (for testing/cleanup)
133
+ */
134
+ export function removeFetchTracer(): void {
135
+ try {
136
+ if (originalFetch) {
137
+ globalThis.fetch = originalFetch;
138
+ delete (globalThis.fetch as any).__flowDebuggerPatched;
139
+ }
140
+ } catch (_) { }
141
+ }
142
+
143
+ /** Extract service tag from URL */
144
+ function getServiceFromUrl(url: string, serviceMap: Record<string, ServiceTag>): ServiceTag {
145
+ try {
146
+ const urlObj = new URL(url);
147
+ const hostname = urlObj.hostname;
148
+
149
+ // Check exact match
150
+ if (serviceMap[hostname]) return serviceMap[hostname];
151
+
152
+ // Check partial match
153
+ for (const [pattern, service] of Object.entries(serviceMap)) {
154
+ if (hostname.includes(pattern)) return service;
155
+ }
156
+
157
+ // Default to fetch
158
+ return 'fetch';
159
+ } catch {
160
+ return 'fetch';
161
+ }
162
+ }
163
+
164
+ /** Extract domain from URL for display */
165
+ function getDomain(url: string): string {
166
+ try {
167
+ const urlObj = new URL(url);
168
+ return urlObj.hostname + urlObj.pathname.substring(0, 30);
169
+ } catch {
170
+ return url.substring(0, 50);
171
+ }
172
+ }
@@ -0,0 +1,130 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // flow-debugger — MongoDB/Mongoose Auto-Instrument
3
+ // Patches Mongoose to automatically trace all DB 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 AnyMongoose = any;
11
+
12
+ interface MongoTracerOptions {
13
+ /** Config overrides for slow query thresholds */
14
+ config?: Partial<DebuggerConfig>;
15
+ /** Function to get the current active tracer (for auto-mode) */
16
+ getTracer?: () => RequestTracer | null;
17
+ }
18
+
19
+ /**
20
+ * Auto-instrument Mongoose to trace all queries.
21
+ *
22
+ * Usage:
23
+ * mongoTracer(mongoose, { getTracer: () => currentTracer })
24
+ *
25
+ * Output:
26
+ * Mongo users.findOne → 22ms ✔
27
+ * Mongo payments.insertMany → error ❌
28
+ */
29
+ export function mongoTracer(mongoose: AnyMongoose, options?: MongoTracerOptions): void {
30
+ try {
31
+ const config = { ...DEFAULT_CONFIG, ...options?.config };
32
+ const queryProto = mongoose.Query?.prototype;
33
+
34
+ if (!queryProto || queryProto.__flowDebuggerPatched) return;
35
+ queryProto.__flowDebuggerPatched = true;
36
+
37
+ const originalExec = queryProto.exec;
38
+
39
+ queryProto.exec = async function (this: any, ...args: any[]) {
40
+ const tracer = options?.getTracer?.();
41
+ if (!tracer) {
42
+ return originalExec.apply(this, args);
43
+ }
44
+
45
+ const op = this.op || 'query';
46
+ const collection = this.mongooseCollection?.name || this.model?.collection?.name || 'unknown';
47
+ const stepName = `Mongo ${collection}.${op}`;
48
+ const startTime = performance.now();
49
+
50
+ let status: StepStatus = 'success';
51
+ let error: string | undefined;
52
+ let stackTrace: string | undefined;
53
+ let result: any;
54
+
55
+ try {
56
+ result = await originalExec.apply(this, args);
57
+ } catch (err: unknown) {
58
+ status = 'error';
59
+ const e = err instanceof Error ? err : new Error(String(err));
60
+ error = e.message;
61
+ stackTrace = e.stack;
62
+
63
+ // Record step before re-throwing
64
+ const endTime = performance.now();
65
+ const duration = endTime - startTime;
66
+ const classification = classifyQuery(duration, status, config);
67
+
68
+ const step: TraceStep = {
69
+ name: stepName,
70
+ service: 'mongo',
71
+ status,
72
+ classification,
73
+ startTime: startTime,
74
+ endTime,
75
+ duration,
76
+ error,
77
+ stackTrace,
78
+ metadata: { operation: op, collection },
79
+ };
80
+
81
+ tracer.addStep(step);
82
+ throw err;
83
+ }
84
+
85
+ const endTime = performance.now();
86
+ const duration = endTime - startTime;
87
+ const classification = classifyQuery(duration, status, config);
88
+
89
+ const step: TraceStep = {
90
+ name: stepName,
91
+ service: 'mongo',
92
+ status,
93
+ classification,
94
+ startTime: startTime,
95
+ endTime,
96
+ duration,
97
+ error,
98
+ stackTrace,
99
+ metadata: { operation: op, collection },
100
+ };
101
+
102
+ tracer.addStep(step);
103
+
104
+ // Log slow query warning
105
+ if (duration > (config.slowQueryThreshold ?? 300)) {
106
+ try {
107
+ (config.logger || console.log)(
108
+ `\x1b[33m⚠ Slow MongoDB query: ${stepName} (${Math.round(duration)}ms)\x1b[0m`
109
+ );
110
+ } catch (_) { }
111
+ }
112
+
113
+ return result;
114
+ };
115
+ } catch (_) {
116
+ // Production-safe: never crash the app
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Remove the monkey-patch from Mongoose (for testing/cleanup).
122
+ */
123
+ export function removeMongoTracer(mongoose: AnyMongoose): void {
124
+ try {
125
+ const queryProto = mongoose.Query?.prototype;
126
+ if (queryProto) {
127
+ delete queryProto.__flowDebuggerPatched;
128
+ }
129
+ } catch (_) { }
130
+ }
@@ -0,0 +1,239 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // flow-debugger — MySQL Auto-Instrument (mysql2)
3
+ // Patches mysql2 pool 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 MysqlTracerOptions {
13
+ config?: Partial<DebuggerConfig>;
14
+ getTracer?: () => RequestTracer | null;
15
+ }
16
+
17
+ /**
18
+ * Auto-instrument mysql2 pool to trace all queries.
19
+ *
20
+ * Usage:
21
+ * mysqlTracer(pool, { getTracer: () => currentTracer })
22
+ *
23
+ * Output:
24
+ * MySQL SELECT users → 12ms ✔
25
+ * MySQL INSERT payments → error ❌
26
+ */
27
+ export function mysqlTracer(pool: AnyPool, options?: MysqlTracerOptions): void {
28
+ try {
29
+ if (pool.__flowDebuggerPatched) return;
30
+ pool.__flowDebuggerPatched = true;
31
+
32
+ const config = { ...DEFAULT_CONFIG, ...options?.config };
33
+
34
+ // Patch pool.query
35
+ const originalQuery = pool.query.bind(pool);
36
+ pool.query = function tracedQuery(...args: any[]) {
37
+ const tracer = options?.getTracer?.();
38
+ if (!tracer) {
39
+ return originalQuery(...args);
40
+ }
41
+
42
+ const sql = typeof args[0] === 'string' ? args[0] : args[0]?.sql || 'unknown';
43
+ const operation = extractSqlOperation(sql);
44
+ const stepName = `MySQL ${operation}`;
45
+ const startTime = performance.now();
46
+
47
+ // Check if using callback or promise style
48
+ const lastArg = args[args.length - 1];
49
+ if (typeof lastArg === 'function') {
50
+ // Callback style
51
+ const callback = lastArg;
52
+ args[args.length - 1] = function (err: any, results: any, fields: any) {
53
+ const endTime = performance.now();
54
+ const duration = endTime - startTime;
55
+ const status: StepStatus = err ? 'error' : 'success';
56
+ const classification = classifyQuery(duration, status, config);
57
+
58
+ const step: TraceStep = {
59
+ name: stepName,
60
+ service: 'mysql',
61
+ status,
62
+ classification,
63
+ startTime,
64
+ endTime,
65
+ duration,
66
+ error: err?.message,
67
+ stackTrace: err?.stack,
68
+ metadata: { sql: sql.substring(0, 200), operation },
69
+ };
70
+ tracer.addStep(step);
71
+
72
+ if (duration > (config.slowQueryThreshold ?? 300)) {
73
+ try {
74
+ (config.logger || console.log)(
75
+ `\x1b[33m⚠ Slow MySQL query: ${stepName} (${Math.round(duration)}ms)\n ${sql.substring(0, 100)}\x1b[0m`
76
+ );
77
+ } catch (_) { }
78
+ }
79
+
80
+ callback(err, results, fields);
81
+ };
82
+ return originalQuery(...args);
83
+ } else {
84
+ // Promise style
85
+ const promise = originalQuery(...args);
86
+ if (promise && typeof promise.then === 'function') {
87
+ return promise.then(
88
+ (result: any) => {
89
+ const endTime = performance.now();
90
+ const duration = endTime - startTime;
91
+ const classification = classifyQuery(duration, 'success', config);
92
+
93
+ const step: TraceStep = {
94
+ name: stepName,
95
+ service: 'mysql',
96
+ status: 'success',
97
+ classification,
98
+ startTime,
99
+ endTime,
100
+ duration,
101
+ metadata: { sql: sql.substring(0, 200), operation },
102
+ };
103
+ tracer.addStep(step);
104
+
105
+ if (duration > (config.slowQueryThreshold ?? 300)) {
106
+ try {
107
+ (config.logger || console.log)(
108
+ `\x1b[33m⚠ Slow MySQL query: ${stepName} (${Math.round(duration)}ms)\n ${sql.substring(0, 100)}\x1b[0m`
109
+ );
110
+ } catch (_) { }
111
+ }
112
+
113
+ return result;
114
+ },
115
+ (err: any) => {
116
+ const endTime = performance.now();
117
+ const duration = endTime - startTime;
118
+ const classification = classifyQuery(duration, 'error', config);
119
+
120
+ const step: TraceStep = {
121
+ name: stepName,
122
+ service: 'mysql',
123
+ status: 'error',
124
+ classification,
125
+ startTime,
126
+ endTime,
127
+ duration,
128
+ error: err?.message,
129
+ stackTrace: err?.stack,
130
+ metadata: { sql: sql.substring(0, 200), operation },
131
+ };
132
+ tracer.addStep(step);
133
+ throw err;
134
+ },
135
+ );
136
+ }
137
+ return promise;
138
+ }
139
+ };
140
+
141
+ // Patch pool.execute (mysql2 specific)
142
+ if (pool.execute) {
143
+ const originalExecute = pool.execute.bind(pool);
144
+ pool.execute = function tracedExecute(...args: any[]) {
145
+ const tracer = options?.getTracer?.();
146
+ if (!tracer) {
147
+ return originalExecute(...args);
148
+ }
149
+
150
+ const sql = typeof args[0] === 'string' ? args[0] : args[0]?.sql || 'unknown';
151
+ const operation = extractSqlOperation(sql);
152
+ const stepName = `MySQL ${operation}`;
153
+ const startTime = performance.now();
154
+
155
+ const lastArg = args[args.length - 1];
156
+ if (typeof lastArg === 'function') {
157
+ const callback = lastArg;
158
+ args[args.length - 1] = function (err: any, results: any, fields: any) {
159
+ const endTime = performance.now();
160
+ const duration = endTime - startTime;
161
+ const status: StepStatus = err ? 'error' : 'success';
162
+ const classification = classifyQuery(duration, status, config);
163
+
164
+ tracer.addStep({
165
+ name: stepName,
166
+ service: 'mysql',
167
+ status,
168
+ classification,
169
+ startTime,
170
+ endTime,
171
+ duration,
172
+ error: err?.message,
173
+ stackTrace: err?.stack,
174
+ metadata: { sql: sql.substring(0, 200), operation },
175
+ });
176
+
177
+ callback(err, results, fields);
178
+ };
179
+ return originalExecute(...args);
180
+ } else {
181
+ const promise = originalExecute(...args);
182
+ if (promise && typeof promise.then === 'function') {
183
+ return promise.then(
184
+ (result: any) => {
185
+ const endTime = performance.now();
186
+ const duration = endTime - startTime;
187
+ tracer.addStep({
188
+ name: stepName,
189
+ service: 'mysql',
190
+ status: 'success',
191
+ classification: classifyQuery(duration, 'success', config),
192
+ startTime,
193
+ endTime,
194
+ duration,
195
+ metadata: { sql: sql.substring(0, 200), operation },
196
+ });
197
+ return result;
198
+ },
199
+ (err: any) => {
200
+ const endTime = performance.now();
201
+ const duration = endTime - startTime;
202
+ tracer.addStep({
203
+ name: stepName,
204
+ service: 'mysql',
205
+ status: 'error',
206
+ classification: classifyQuery(duration, 'error', config),
207
+ startTime,
208
+ endTime,
209
+ duration,
210
+ error: err?.message,
211
+ stackTrace: err?.stack,
212
+ metadata: { sql: sql.substring(0, 200), operation },
213
+ });
214
+ throw err;
215
+ },
216
+ );
217
+ }
218
+ return promise;
219
+ }
220
+ };
221
+ }
222
+ } catch (_) {
223
+ // Production-safe: never crash
224
+ }
225
+ }
226
+
227
+ /** Extract the SQL operation type from a query string */
228
+ function extractSqlOperation(sql: string): string {
229
+ const trimmed = sql.trim().toUpperCase();
230
+ const match = trimmed.match(/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE|REPLACE|CALL)\b/);
231
+ if (match) {
232
+ const op = match[1];
233
+ // Try to extract table name
234
+ const tableMatch = sql.match(/(?:FROM|INTO|UPDATE|TABLE|JOIN)\s+[`"]?(\w+)[`"]?/i);
235
+ const table = tableMatch ? tableMatch[1] : '';
236
+ return table ? `${op} ${table}` : op;
237
+ }
238
+ return sql.substring(0, 30);
239
+ }