react-native-qalink 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -7,6 +7,7 @@ const fetch_1 = require("./interceptors/fetch");
7
7
  const errors_1 = require("./interceptors/errors");
8
8
  const console_1 = require("./interceptors/console");
9
9
  const runtime_1 = require("./interceptors/runtime");
10
+ const metro_1 = require("./interceptors/metro");
10
11
  const session_1 = require("./core/session");
11
12
  const device_1 = require("./core/device");
12
13
  const sanitize_1 = require("./core/sanitize");
@@ -27,9 +28,8 @@ class QALinkSDK {
27
28
  return;
28
29
  }
29
30
 
30
- // ── 1. enabled flag ───────────────────────────────────────────────────────
31
31
  if (config.enabled === false) {
32
- this._log('SDK disabled (enabled: false). No data will be sent.');
32
+ this._log('SDK disabled. No data will be sent.');
33
33
  return;
34
34
  }
35
35
 
@@ -41,6 +41,7 @@ class QALinkSDK {
41
41
  sensitiveUrlPatterns: [],
42
42
  sensitiveBodyFields: [],
43
43
  captureRuntimeErrors: true,
44
+ captureMetroErrors: true,
44
45
  console: { captureLogs: true, captureWarnings: true, captureErrors: true, ignorePatterns: [], includePatterns: [] },
45
46
  debug: false,
46
47
  ...config,
@@ -51,14 +52,11 @@ class QALinkSDK {
51
52
  },
52
53
  };
53
54
 
54
- // ── 2. deviceId ───────────────────────────────────────────────────────────
55
55
  this._deviceId = await (0, device_1.getDeviceId)();
56
56
 
57
- // ── 3. WebSocket ──────────────────────────────────────────────────────────
58
57
  this.transport = new websocket_1.WebSocketTransport(this.config.serverUrl, this.config.debug);
59
58
  this.transport.connect();
60
59
 
61
- // ── 4. session_start ──────────────────────────────────────────────────────
62
60
  const deviceInfo = await (0, session_1.getDeviceInfo)(config.appVersion);
63
61
  const sessionEvent = {
64
62
  id: (0, session_1.generateId)(),
@@ -75,16 +73,27 @@ class QALinkSDK {
75
73
  const getScreen = () => this.currentScreen;
76
74
  const getDevId = () => this._deviceId;
77
75
 
78
- // ── 5. Interceptors ───────────────────────────────────────────────────────
79
76
  this.cleanups.push((0, fetch_1.setupFetchInterceptor)(this.transport, this.config, getDevId));
80
77
  this.cleanups.push((0, console_1.setupConsoleInterceptor)(this.transport, this.config, getScreen));
78
+
81
79
  if (this.config.captureRuntimeErrors) {
82
80
  this.cleanups.push((0, runtime_1.setupRuntimeErrorHandler)(this.transport, this.config, getScreen));
83
81
  }
82
+
83
+ // Metro — activo por defecto, desactivar con captureMetroErrors: false
84
+ if (this.config.captureMetroErrors !== false) {
85
+ this.cleanups.push((0, metro_1.setupMetroInterceptor)(this.transport, this.config, getDevId));
86
+ }
87
+
84
88
  this.cleanups.push((0, errors_1.setupErrorHandlers)(this.transport, this.config));
85
89
 
86
90
  this.initialized = true;
87
- this._log('SDK initialized ✅', { sessionId: (0, session_1.getSessionId)(), deviceId: this._deviceId, environment: this.config.environment });
91
+ this._log('SDK initialized ✅', {
92
+ sessionId: (0, session_1.getSessionId)(),
93
+ deviceId: this._deviceId,
94
+ environment: this.config.environment,
95
+ captureMetroErrors: this.config.captureMetroErrors,
96
+ });
88
97
  }
89
98
 
90
99
  interceptAxios(axiosInstance) {
@@ -98,9 +107,7 @@ class QALinkSDK {
98
107
  const event = {
99
108
  id: (0, session_1.generateId)(),
100
109
  type: 'breadcrumb',
101
- action,
102
- screen: this.currentScreen,
103
- data,
110
+ action, screen: this.currentScreen, data,
104
111
  timestamp: Date.now(),
105
112
  sessionId: (0, session_1.getSessionId)(),
106
113
  deviceId: this._deviceId,
@@ -114,42 +121,29 @@ class QALinkSDK {
114
121
  const { method, url, statusCode, requestPayload, responsePayload, durationMs } = options;
115
122
  const isAuth = (0, sanitize_1.isAuthUrl)(url);
116
123
  const extraFields = this.config?.sensitiveBodyFields ?? [];
117
- let sanitizedReq = requestPayload;
118
- let sanitizedRes = responsePayload;
119
- let hadSensitiveData = false;
124
+ let sanitizedReq = requestPayload, sanitizedRes = responsePayload, hadSensitiveData = false;
120
125
 
121
126
  if (requestPayload) {
122
- if (isAuth) {
123
- sanitizedReq = '[REDACTED - auth endpoint]';
124
- hadSensitiveData = true;
125
- } else {
126
- const result = (0, sanitize_1.sanitizePayload)(requestPayload, extraFields);
127
- sanitizedReq = result.sanitized;
128
- hadSensitiveData = result.hadSensitiveData;
129
- }
127
+ if (isAuth) { sanitizedReq = '[REDACTED - auth endpoint]'; hadSensitiveData = true; }
128
+ else { const r = (0, sanitize_1.sanitizePayload)(requestPayload, extraFields); sanitizedReq = r.sanitized; hadSensitiveData = r.hadSensitiveData; }
130
129
  }
131
130
  if (responsePayload) {
132
- const result = (0, sanitize_1.sanitizePayload)(responsePayload, extraFields);
133
- sanitizedRes = result.sanitized;
134
- if (result.hadSensitiveData) hadSensitiveData = true;
131
+ const r = (0, sanitize_1.sanitizePayload)(responsePayload, extraFields);
132
+ sanitizedRes = r.sanitized;
133
+ if (r.hadSensitiveData) hadSensitiveData = true;
135
134
  }
136
135
 
137
136
  const event = {
138
137
  id: (0, session_1.generateId)(),
139
138
  type: 'network_log',
140
- method: method.toUpperCase(),
141
- url,
142
- statusCode,
143
- requestPayload: sanitizedReq,
144
- responsePayload: sanitizedRes,
145
- durationMs,
139
+ method: method.toUpperCase(), url, statusCode,
140
+ requestPayload: sanitizedReq, responsePayload: sanitizedRes, durationMs,
146
141
  timestamp: Date.now(),
147
142
  sessionId: (0, session_1.getSessionId)(),
148
143
  deviceId: this._deviceId,
149
144
  screen: this.currentScreen,
150
145
  sanitized: hadSensitiveData,
151
146
  };
152
-
153
147
  this.transport.send(event);
154
148
  this.config?.onEvent?.(event);
155
149
  this._log(`logRequest: ${method.toUpperCase()} ${url} ${statusCode ?? '?'}${hadSensitiveData ? ' [sanitized]' : ''}`);
@@ -168,9 +162,7 @@ class QALinkSDK {
168
162
  this.cleanups.forEach(fn => fn());
169
163
  this.cleanups = [];
170
164
  this.transport?.disconnect();
171
- this.transport = null;
172
- this.config = null;
173
- this.initialized = false;
165
+ this.transport = null; this.config = null; this.initialized = false;
174
166
  }
175
167
 
176
168
  _log(...args) {
@@ -181,6 +173,5 @@ class QALinkSDK {
181
173
  const QALink = new QALinkSDK();
182
174
  exports.QALink = QALink;
183
175
 
184
- // Re-exports
185
176
  var classifier_1 = require("./core/classifier");
186
177
  Object.defineProperty(exports, "getSourceLabel", { enumerable: true, get: function () { return classifier_1.getSourceLabel; } });
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setupMetroInterceptor = void 0;
4
+ const session_1 = require("../core/session");
5
+
6
+ const METRO_PATTERNS = [
7
+ { pattern: /Unable to resolve module ['"]([^'"]+)['"]\s+from\s+['"]([^'"]+)['"]/i, type: 'module_not_found' },
8
+ { pattern: /SyntaxError:.+\((\d+):(\d+)\)/i, type: 'syntax_error' },
9
+ { pattern: /TransformError/i, type: 'transform_error' },
10
+ { pattern: /bundling failed/i, type: 'bundling_failed' },
11
+ { pattern: /Metro(?:\s+Bundler)?:/i, type: 'metro_generic' },
12
+ { pattern: /error: .+\.(?:js|ts|tsx|jsx):\s*\d+:\d+/i, type: 'build_error' },
13
+ { pattern: /Requiring unknown module/i, type: 'module_not_found'},
14
+ { pattern: /Module not found/i, type: 'module_not_found'},
15
+ { pattern: /cannot find module/i, type: 'module_not_found'},
16
+ { pattern: /ENOENT:.+require/i, type: 'module_not_found'},
17
+ ];
18
+
19
+ function parseFileInfo(message) {
20
+ const m1 = message.match(/([^\s:]+\.(?:js|ts|tsx|jsx)):(\d+):(\d+)/);
21
+ if (m1) return { file: m1[1], lineNumber: parseInt(m1[2], 10), column: parseInt(m1[3], 10) };
22
+ const m2 = message.match(/([^\s(]+\.(?:js|ts|tsx|jsx))\s*\((\d+):(\d+)\)/);
23
+ if (m2) return { file: m2[1], lineNumber: parseInt(m2[2], 10), column: parseInt(m2[3], 10) };
24
+ return {};
25
+ }
26
+
27
+ function isMetroMessage(message) {
28
+ return METRO_PATTERNS.some(({ pattern }) => pattern.test(message));
29
+ }
30
+
31
+ function getMetroSubtype(message) {
32
+ for (const { pattern, type } of METRO_PATTERNS) {
33
+ if (pattern.test(message)) return type;
34
+ }
35
+ return 'metro_generic';
36
+ }
37
+
38
+ function argsToString(args) {
39
+ return args
40
+ .map(a => typeof a === 'string' ? a : a instanceof Error ? `${a.message}\n${a.stack ?? ''}` : String(a))
41
+ .join(' ');
42
+ }
43
+
44
+ function setupMetroInterceptor(transport, config, getDeviceId) {
45
+ const cleanups = [];
46
+
47
+ // ── 1. Interceptar console.error ──────────────────────────────────────────
48
+ const previousConsoleError = console.error;
49
+
50
+ console.error = (...args) => {
51
+ previousConsoleError(...args);
52
+ if (config.captureMetroErrors === false) return;
53
+ const message = argsToString(args);
54
+ if (!isMetroMessage(message)) return;
55
+ if (message.startsWith('[QALink]')) return;
56
+
57
+ const { file, lineNumber, column } = parseFileInfo(message);
58
+ const event = {
59
+ id: (0, session_1.generateId)(),
60
+ type: 'metro_error',
61
+ metroType: getMetroSubtype(message),
62
+ message: message.slice(0, 2000),
63
+ file, lineNumber, column,
64
+ timestamp: Date.now(),
65
+ sessionId: (0, session_1.getSessionId)(),
66
+ deviceId: getDeviceId(),
67
+ };
68
+ transport.send(event);
69
+ config.onEvent?.(event);
70
+ };
71
+
72
+ cleanups.push(() => { console.error = previousConsoleError; });
73
+
74
+ // ── 2. ErrorUtils — crashes fatales de Metro ──────────────────────────────
75
+ try {
76
+ if (typeof ErrorUtils !== 'undefined' && ErrorUtils.getGlobalHandler) {
77
+ const originalHandler = ErrorUtils.getGlobalHandler();
78
+
79
+ ErrorUtils.setGlobalHandler((error, isFatal) => {
80
+ try {
81
+ if (config.captureMetroErrors !== false && error?.message && isMetroMessage(error.message)) {
82
+ const { file, lineNumber, column } = parseFileInfo(error.message);
83
+ const event = {
84
+ id: (0, session_1.generateId)(),
85
+ type: 'metro_error',
86
+ metroType: getMetroSubtype(error.message),
87
+ message: error.message.slice(0, 2000),
88
+ stack: error.stack,
89
+ file, lineNumber, column,
90
+ timestamp: Date.now(),
91
+ sessionId: (0, session_1.getSessionId)(),
92
+ deviceId: getDeviceId(),
93
+ };
94
+ transport.send(event);
95
+ config.onEvent?.(event);
96
+ }
97
+ } catch {}
98
+ originalHandler(error, isFatal);
99
+ });
100
+
101
+ cleanups.push(() => {
102
+ try { ErrorUtils.setGlobalHandler(originalHandler); } catch {}
103
+ });
104
+ }
105
+ } catch {}
106
+
107
+ return () => cleanups.forEach(fn => { try { fn(); } catch {} });
108
+ }
109
+ exports.setupMetroInterceptor = setupMetroInterceptor;
package/package.json CHANGED
@@ -1,20 +1,14 @@
1
1
  {
2
2
  "name": "react-native-qalink",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Real-time error capture SDK for React Native — helps QA teams identify if bugs are frontend or backend",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": ["dist", "README.md"],
8
- "keywords": ["react-native", "debugging", "qa", "error-tracking", "network-interceptor"],
9
- "peerDependencies": {
10
- "react-native": ">=0.70.0",
11
- "axios": ">=1.0.0"
12
- },
8
+ "keywords": ["react-native", "debugging", "qa", "error-tracking", "network-interceptor", "metro"],
9
+ "peerDependencies": { "react-native": ">=0.70.0", "axios": ">=1.0.0" },
13
10
  "peerDependenciesMeta": { "axios": { "optional": true } },
14
- "devDependencies": {
15
- "typescript": "^5.0.0",
16
- "@types/react-native": "^0.72.0"
17
- },
11
+ "devDependencies": { "typescript": "^5.0.0", "@types/react-native": "^0.72.0" },
18
12
  "scripts": { "build": "tsc", "typecheck": "tsc --noEmit" },
19
13
  "license": "MIT"
20
14
  }