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 +26 -35
- package/dist/interceptors/metro.js +109 -0
- package/package.json +4 -10
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
|
|
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 ✅', {
|
|
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
|
-
|
|
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
|
|
133
|
-
sanitizedRes =
|
|
134
|
-
if (
|
|
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
|
-
|
|
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.
|
|
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
|
}
|