@himanshu-panchal/nodescope-sdk 1.0.0 → 1.0.1

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 (3) hide show
  1. package/index.js +54 -26
  2. package/middleware.js +263 -11
  3. package/package.json +20 -8
package/index.js CHANGED
@@ -2,30 +2,29 @@ const { createMiddleware } = require('./middleware');
2
2
  const { getInstance } = require('./tracer');
3
3
  const { Context } = require('./context');
4
4
  const { autoDetectAndPatch } = require('./auto-detect');
5
- const { patchPg } = require('./patches/pg');
6
- const { patchMysql } = require('./patches/mysql');
7
- const { patchMongoose } = require('./patches/mongoose');
8
- const { patchRedis } = require('./patches/redis');
9
- const { patchAxios } = require('./patches/axios');
10
5
  const { v4: uuidv4 } = require('uuid');
11
6
 
12
7
  let autoDetectDone = false;
13
8
 
14
- // Main function
15
- function nodescope(config) {
16
- // Auto detect libraries (ek baar)
9
+ function nodescope(config = {}) {
10
+ // Enhanced default config
11
+ const enhancedConfig = {
12
+ ...config,
13
+ captureHeaders: config.captureHeaders || false,
14
+ captureBody: config.captureBody || false,
15
+ captureResponse: config.captureResponse || false,
16
+ captureQueries: config.captureQueries !== false, // true by default
17
+ };
18
+
17
19
  if (!autoDetectDone) {
18
20
  autoDetectDone = true;
19
- // Tracer pehle init karo
20
- getInstance(config);
21
- // Phir auto detect
21
+ getInstance(enhancedConfig);
22
22
  setTimeout(() => autoDetectAndPatch(), 100);
23
23
  }
24
24
 
25
- return createMiddleware(config);
25
+ return createMiddleware(enhancedConfig);
26
26
  }
27
27
 
28
- // Manual deep trace wrapper
29
28
  function trace(fn) {
30
29
  return function (...args) {
31
30
  const tracer = getInstance();
@@ -40,6 +39,20 @@ function trace(fn) {
40
39
 
41
40
  tracer.startSpan(spanId, traceId, name, 'internal');
42
41
 
42
+ // Capture function arguments (safely)
43
+ if (args.length > 0) {
44
+ const safeArgs = args.map((arg, i) => {
45
+ if (typeof arg === 'object' && arg !== null) {
46
+ // Truncate large objects
47
+ return JSON.stringify(arg).length > 500
48
+ ? `[Object: ${Object.keys(arg).length} keys]`
49
+ : arg;
50
+ }
51
+ return arg;
52
+ });
53
+ tracer.addAttribute(spanId, 'function.arguments', safeArgs);
54
+ }
55
+
43
56
  let result;
44
57
  try {
45
58
  result = fn.apply(this, args);
@@ -50,8 +63,33 @@ function trace(fn) {
50
63
 
51
64
  if (result?.then) {
52
65
  return result
53
- .then((res) => { tracer.endSpan(spanId); return res; })
54
- .catch((err) => { tracer.endSpan(spanId, err); throw err; });
66
+ .then((res) => {
67
+ // Capture return value (safely)
68
+ if (res !== undefined) {
69
+ const safeResult = typeof res === 'object' && res !== null
70
+ ? JSON.stringify(res).length > 500
71
+ ? `[Object: ${Object.keys(res).length} keys]`
72
+ : res
73
+ : res;
74
+ tracer.addAttribute(spanId, 'function.result', safeResult);
75
+ }
76
+ tracer.endSpan(spanId);
77
+ return res;
78
+ })
79
+ .catch((err) => {
80
+ tracer.endSpan(spanId, err);
81
+ throw err;
82
+ });
83
+ }
84
+
85
+ // Capture synchronous return value
86
+ if (result !== undefined) {
87
+ const safeResult = typeof result === 'object' && result !== null
88
+ ? JSON.stringify(result).length > 500
89
+ ? `[Object: ${Object.keys(result).length} keys]`
90
+ : result
91
+ : result;
92
+ tracer.addAttribute(spanId, 'function.result', safeResult);
55
93
  }
56
94
 
57
95
  tracer.endSpan(spanId);
@@ -59,17 +97,7 @@ function trace(fn) {
59
97
  };
60
98
  }
61
99
 
62
- // Exports
63
100
  module.exports = nodescope;
64
-
65
- // Named exports for manual use
66
101
  module.exports.nodescope = nodescope;
67
102
  module.exports.trace = trace;
68
- module.exports.Context = Context;
69
-
70
- // Manual patches (agar auto-detect kaam na kare)
71
- module.exports.patchPg = patchPg;
72
- module.exports.patchMysql = patchMysql;
73
- module.exports.patchMongoose = patchMongoose;
74
- module.exports.patchRedis = patchRedis;
75
- module.exports.patchAxios = patchAxios;
103
+ module.exports.Context = Context;
package/middleware.js CHANGED
@@ -2,21 +2,25 @@ const { v4: uuidv4 } = require('uuid');
2
2
  const { Context } = require('./context');
3
3
  const { getInstance } = require('./tracer');
4
4
 
5
- function createMiddleware(config) {
5
+ function createMiddleware(config = {}) {
6
6
  const tracer = getInstance(config);
7
7
 
8
8
  return function nodeScopeMiddleware(req, res, next) {
9
- // Health check skip karo
10
- if (req.path === '/health' || req.path === '/ping') {
9
+ // Skip health checks and static assets
10
+ if (req.path === '/health' || req.path === '/ping' || req.path.startsWith('/static')) {
11
11
  return next();
12
12
  }
13
13
 
14
14
  const traceId = uuidv4();
15
15
  const requestId = uuidv4();
16
16
 
17
+ // Start the main request trace
17
18
  tracer.startTrace(traceId, req.method, req.path);
18
19
 
20
+ // Run within AsyncLocalStorage context
19
21
  Context.run(traceId, requestId, req.path, req.method, () => {
22
+
23
+ // Attach NodeScope helper to request object
20
24
  req.nodescope = {
21
25
  traceId,
22
26
  startSpan: (name) => {
@@ -25,44 +29,292 @@ function createMiddleware(config) {
25
29
  return {
26
30
  end: (error) => tracer.endSpan(spanId, error),
27
31
  setAttribute: (key, value) => tracer.addAttribute(spanId, key, value),
32
+ addEvent: (name, attributes) => tracer.addAttribute(spanId, `event.${name}`, attributes || {}),
28
33
  };
29
34
  },
30
35
  };
31
36
 
37
+ // Capture request metadata
32
38
  const metadata = {
33
- ip: req.ip || req.connection?.remoteAddress,
39
+ ip: req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress,
34
40
  userAgent: req.get('user-agent'),
35
- query: Object.keys(req.query).length ? req.query : undefined,
36
- route: req.route?.path,
41
+ route: req.route?.path || req.path,
42
+ method: req.method,
43
+ protocol: req.protocol,
44
+ hostname: req.hostname,
45
+ originalUrl: req.originalUrl,
46
+ timestamp: new Date().toISOString(),
37
47
  };
38
48
 
49
+ // Capture query parameters
50
+ if (req.query && Object.keys(req.query).length > 0) {
51
+ metadata.query = req.query;
52
+ }
53
+
54
+ // Capture request headers (with masking)
55
+ if (config.captureHeaders) {
56
+ metadata.headers = maskSensitiveHeaders(req.headers, config.mask || []);
57
+ }
58
+
59
+ // Capture request body (with size limit and masking)
60
+ if (config.captureBody && req.body) {
61
+ metadata.body = maskSensitiveData(req.body, config.mask || []);
62
+ }
63
+
64
+ // Capture request size
65
+ const contentLength = req.get('content-length');
66
+ if (contentLength) {
67
+ metadata.requestSize = parseInt(contentLength);
68
+ }
69
+
70
+ // Response tracking variables
39
71
  let ended = false;
72
+ let responseBody = null;
73
+ let responseStartTime = Date.now();
40
74
 
41
75
  function endTrace() {
42
- if (!ended) {
43
- ended = true;
44
- tracer.endTrace(traceId, res.statusCode, metadata);
76
+ if (ended) return;
77
+ ended = true;
78
+
79
+ // Calculate response time
80
+ const responseTime = Date.now() - responseStartTime;
81
+
82
+ // Add response metadata
83
+ const responseMetadata = {
84
+ ...metadata,
85
+ responseTime,
86
+ statusCode: res.statusCode,
87
+ statusMessage: res.statusMessage,
88
+ };
89
+
90
+ // Capture response headers
91
+ if (config.captureHeaders) {
92
+ responseMetadata.responseHeaders = res.getHeaders();
93
+ }
94
+
95
+ // Capture response body
96
+ if (config.captureResponse && responseBody !== null) {
97
+ responseMetadata.responseBody = maskSensitiveData(responseBody, config.mask || []);
45
98
  }
99
+
100
+ // Capture response size
101
+ const responseLength = res.get('content-length');
102
+ if (responseLength) {
103
+ responseMetadata.responseSize = parseInt(responseLength);
104
+ }
105
+
106
+ // End the trace with all metadata
107
+ tracer.endTrace(traceId, res.statusCode, responseMetadata);
46
108
  }
47
109
 
110
+ // Intercept response methods
48
111
  const originalJson = res.json.bind(res);
49
112
  const originalSend = res.send.bind(res);
113
+ const originalEnd = res.end.bind(res);
50
114
 
115
+ // Override res.json()
51
116
  res.json = function (data) {
117
+ if (config.captureResponse && data !== undefined) {
118
+ responseBody = data;
119
+ }
52
120
  endTrace();
53
121
  return originalJson(data);
54
122
  };
55
123
 
124
+ // Override res.send()
56
125
  res.send = function (data) {
126
+ if (config.captureResponse && data !== undefined) {
127
+ try {
128
+ // Try to parse as JSON if it's a string
129
+ responseBody = typeof data === 'string' ? JSON.parse(data) : data;
130
+ } catch (e) {
131
+ // If not JSON, store as string (truncated if too long)
132
+ responseBody = typeof data === 'string' && data.length > 1000
133
+ ? data.substring(0, 1000) + '...[truncated]'
134
+ : data;
135
+ }
136
+ }
57
137
  endTrace();
58
138
  return originalSend(data);
59
139
  };
60
140
 
61
- res.on('finish', endTrace);
141
+ // Override res.end()
142
+ res.end = function (chunk, encoding) {
143
+ if (config.captureResponse && chunk && !responseBody) {
144
+ try {
145
+ responseBody = chunk.toString();
146
+ if (responseBody.length > 1000) {
147
+ responseBody = responseBody.substring(0, 1000) + '...[truncated]';
148
+ }
149
+ } catch (e) {
150
+ responseBody = '[Binary data]';
151
+ }
152
+ }
153
+ endTrace();
154
+ return originalEnd(chunk, encoding);
155
+ };
156
+
157
+ // Handle response finish event as fallback
158
+ res.on('finish', () => {
159
+ endTrace();
160
+ });
161
+
162
+ // Handle response close event (for aborted requests)
163
+ res.on('close', () => {
164
+ if (!res.headersSent) {
165
+ endTrace();
166
+ }
167
+ });
168
+
169
+ // Error handling
170
+ res.on('error', (error) => {
171
+ metadata.error = {
172
+ message: error.message,
173
+ stack: error.stack,
174
+ type: error.name,
175
+ };
176
+ endTrace();
177
+ });
62
178
 
179
+ // Continue to next middleware
63
180
  next();
64
181
  });
65
182
  };
66
183
  }
67
184
 
68
- module.exports = { createMiddleware };
185
+ // Helper function to mask sensitive headers
186
+ function maskSensitiveHeaders(headers, maskKeys = []) {
187
+ const sensitiveKeys = [
188
+ 'authorization', 'cookie', 'set-cookie', 'x-api-key',
189
+ 'x-auth-token', 'auth-token', 'access-token', 'refresh-token',
190
+ 'x-csrf-token', 'csrf-token', 'session-id', 'sessionid',
191
+ ...maskKeys
192
+ ];
193
+
194
+ const masked = {};
195
+
196
+ for (const [key, value] of Object.entries(headers)) {
197
+ const lowerKey = key.toLowerCase();
198
+ const shouldMask = sensitiveKeys.some(sensitive =>
199
+ lowerKey.includes(sensitive.toLowerCase())
200
+ );
201
+
202
+ if (shouldMask) {
203
+ masked[key] = '***MASKED***';
204
+ } else {
205
+ // Truncate very long header values
206
+ masked[key] = typeof value === 'string' && value.length > 200
207
+ ? value.substring(0, 200) + '...[truncated]'
208
+ : value;
209
+ }
210
+ }
211
+
212
+ return masked;
213
+ }
214
+
215
+ // Helper function to mask sensitive data in objects
216
+ function maskSensitiveData(data, maskKeys = []) {
217
+ if (!data || typeof data !== 'object') {
218
+ return data;
219
+ }
220
+
221
+ const sensitiveKeys = [
222
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'key', 'auth',
223
+ 'authorization', 'cookie', 'session', 'csrf', 'ssn', 'social',
224
+ 'credit', 'card', 'cvv', 'pin', 'otp', 'email', 'phone', 'mobile',
225
+ ...maskKeys
226
+ ];
227
+
228
+ function maskObject(obj) {
229
+ if (Array.isArray(obj)) {
230
+ return obj.map(item => maskObject(item));
231
+ }
232
+
233
+ if (obj === null || typeof obj !== 'object') {
234
+ return obj;
235
+ }
236
+
237
+ const masked = {};
238
+
239
+ for (const [key, value] of Object.entries(obj)) {
240
+ const lowerKey = key.toLowerCase();
241
+ const shouldMask = sensitiveKeys.some(sensitive =>
242
+ lowerKey.includes(sensitive.toLowerCase())
243
+ );
244
+
245
+ if (shouldMask) {
246
+ masked[key] = '***MASKED***';
247
+ } else if (typeof value === 'object' && value !== null) {
248
+ masked[key] = maskObject(value);
249
+ } else if (typeof value === 'string' && value.length > 1000) {
250
+ // Truncate very long strings
251
+ masked[key] = value.substring(0, 1000) + '...[truncated]';
252
+ } else {
253
+ masked[key] = value;
254
+ }
255
+ }
256
+
257
+ return masked;
258
+ }
259
+
260
+ return maskObject(data);
261
+ }
262
+
263
+ // Helper function to safely stringify with size limit
264
+ function safeStringify(obj, maxSize = 10000) {
265
+ try {
266
+ const str = JSON.stringify(obj, null, 2);
267
+ return str.length > maxSize
268
+ ? str.substring(0, maxSize) + '\n...[truncated]'
269
+ : str;
270
+ } catch (error) {
271
+ return '[Circular reference or non-serializable data]';
272
+ }
273
+ }
274
+
275
+ // Helper function to get request size estimate
276
+ function getRequestSizeEstimate(req) {
277
+ let size = 0;
278
+
279
+ // Headers size
280
+ if (req.headers) {
281
+ size += JSON.stringify(req.headers).length;
282
+ }
283
+
284
+ // Body size
285
+ if (req.body) {
286
+ size += JSON.stringify(req.body).length;
287
+ }
288
+
289
+ // Query size
290
+ if (req.query && Object.keys(req.query).length > 0) {
291
+ size += JSON.stringify(req.query).length;
292
+ }
293
+
294
+ return size;
295
+ }
296
+
297
+ // Helper function to detect content type
298
+ function getResponseContentType(res) {
299
+ const contentType = res.get('content-type');
300
+ if (!contentType) return 'unknown';
301
+
302
+ if (contentType.includes('application/json')) return 'json';
303
+ if (contentType.includes('text/html')) return 'html';
304
+ if (contentType.includes('text/plain')) return 'text';
305
+ if (contentType.includes('application/xml')) return 'xml';
306
+ if (contentType.includes('image/')) return 'image';
307
+ if (contentType.includes('video/')) return 'video';
308
+ if (contentType.includes('audio/')) return 'audio';
309
+
310
+ return 'other';
311
+ }
312
+
313
+ module.exports = {
314
+ createMiddleware,
315
+ maskSensitiveHeaders,
316
+ maskSensitiveData,
317
+ safeStringify,
318
+ getRequestSizeEstimate,
319
+ getResponseContentType
320
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@himanshu-panchal/nodescope-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Universal Node.js observability SDK - works with any project",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -32,11 +32,23 @@
32
32
  "express": ">=4.0.0"
33
33
  },
34
34
  "peerDependenciesMeta": {
35
- "express": { "optional": true },
36
- "pg": { "optional": true },
37
- "mysql2": { "optional": true },
38
- "mongoose": { "optional": true },
39
- "redis": { "optional": true },
40
- "axios": { "optional": true }
35
+ "express": {
36
+ "optional": true
37
+ },
38
+ "pg": {
39
+ "optional": true
40
+ },
41
+ "mysql2": {
42
+ "optional": true
43
+ },
44
+ "mongoose": {
45
+ "optional": true
46
+ },
47
+ "redis": {
48
+ "optional": true
49
+ },
50
+ "axios": {
51
+ "optional": true
52
+ }
41
53
  }
42
- }
54
+ }