react-native-qalink 0.1.3 → 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/core/device.js +35 -0
- package/dist/core/sanitize.js +62 -0
- package/dist/index.js +164 -158
- package/dist/interceptors/fetch.js +97 -72
- package/dist/interceptors/metro.js +109 -0
- package/package.json +5 -14
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resetDeviceId = exports.getDeviceId = void 0;
|
|
4
|
+
|
|
5
|
+
let cachedDeviceId = null;
|
|
6
|
+
|
|
7
|
+
function generateDeviceId() {
|
|
8
|
+
return 'dev_xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
9
|
+
const r = Math.random() * 16 | 0;
|
|
10
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
11
|
+
return v.toString(16);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = '@qalink_device_id';
|
|
16
|
+
|
|
17
|
+
async function getDeviceId() {
|
|
18
|
+
if (cachedDeviceId) return cachedDeviceId;
|
|
19
|
+
try {
|
|
20
|
+
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
21
|
+
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
22
|
+
if (stored) { cachedDeviceId = stored; return cachedDeviceId; }
|
|
23
|
+
const newId = generateDeviceId();
|
|
24
|
+
await AsyncStorage.setItem(STORAGE_KEY, newId);
|
|
25
|
+
cachedDeviceId = newId;
|
|
26
|
+
return cachedDeviceId;
|
|
27
|
+
} catch {
|
|
28
|
+
if (!cachedDeviceId) cachedDeviceId = generateDeviceId();
|
|
29
|
+
return cachedDeviceId;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.getDeviceId = getDeviceId;
|
|
33
|
+
|
|
34
|
+
function resetDeviceId() { cachedDeviceId = null; }
|
|
35
|
+
exports.resetDeviceId = resetDeviceId;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeHeaders = exports.sanitizePayload = exports.isAuthUrl = void 0;
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SENSITIVE_FIELDS = [
|
|
6
|
+
'password','passwd','pass','secret','token','access_token','refresh_token',
|
|
7
|
+
'id_token','authorization','auth','api_key','apikey','api_secret',
|
|
8
|
+
'credit_card','card_number','cvv','cvc','ssn','social_security',
|
|
9
|
+
'pin','otp','private_key','client_secret',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const AUTH_URL_PATTERNS = [
|
|
13
|
+
'/auth/','/login','/signin','/sign-in','/signup','/sign-up',
|
|
14
|
+
'/register','/password','/token','/oauth','/credentials','/session',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function isAuthUrl(url) {
|
|
18
|
+
const lower = url.toLowerCase();
|
|
19
|
+
return AUTH_URL_PATTERNS.some(p => lower.includes(p));
|
|
20
|
+
}
|
|
21
|
+
exports.isAuthUrl = isAuthUrl;
|
|
22
|
+
|
|
23
|
+
function sanitizePayload(payload, extraSensitiveFields = []) {
|
|
24
|
+
if (!payload) return { sanitized: payload, hadSensitiveData: false };
|
|
25
|
+
const allSensitive = [...DEFAULT_SENSITIVE_FIELDS, ...extraSensitiveFields.map(f => f.toLowerCase())];
|
|
26
|
+
let hadSensitiveData = false;
|
|
27
|
+
|
|
28
|
+
const sanitize = (obj) => {
|
|
29
|
+
if (typeof obj === 'string') {
|
|
30
|
+
try { const parsed = JSON.parse(obj); return JSON.stringify(sanitize(parsed)); } catch { return obj; }
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(obj)) return obj.map(sanitize);
|
|
33
|
+
if (obj !== null && typeof obj === 'object') {
|
|
34
|
+
const result = {};
|
|
35
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
36
|
+
if (allSensitive.includes(key.toLowerCase())) {
|
|
37
|
+
result[key] = '[REDACTED]';
|
|
38
|
+
hadSensitiveData = true;
|
|
39
|
+
} else {
|
|
40
|
+
result[key] = sanitize(value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
return obj;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return { sanitized: sanitize(payload), hadSensitiveData };
|
|
49
|
+
}
|
|
50
|
+
exports.sanitizePayload = sanitizePayload;
|
|
51
|
+
|
|
52
|
+
function sanitizeHeaders(headers, sensitiveHeaders = []) {
|
|
53
|
+
const defaultSensitive = ['authorization','x-api-key','cookie','set-cookie','x-auth-token'];
|
|
54
|
+
const allSensitive = [...defaultSensitive, ...sensitiveHeaders.map(h => h.toLowerCase())];
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
Object.entries(headers).map(([key, value]) => [
|
|
57
|
+
key,
|
|
58
|
+
allSensitive.includes(key.toLowerCase()) ? '[REDACTED]' : value,
|
|
59
|
+
])
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
exports.sanitizeHeaders = sanitizeHeaders;
|
package/dist/index.js
CHANGED
|
@@ -1,171 +1,177 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.
|
|
3
|
+
exports.QALink = void 0;
|
|
18
4
|
const websocket_1 = require("./transport/websocket");
|
|
19
|
-
const axios_1
|
|
20
|
-
const fetch_1
|
|
21
|
-
const errors_1
|
|
22
|
-
const console_1
|
|
23
|
-
const runtime_1
|
|
24
|
-
const
|
|
5
|
+
const axios_1 = require("./interceptors/axios");
|
|
6
|
+
const fetch_1 = require("./interceptors/fetch");
|
|
7
|
+
const errors_1 = require("./interceptors/errors");
|
|
8
|
+
const console_1 = require("./interceptors/console");
|
|
9
|
+
const runtime_1 = require("./interceptors/runtime");
|
|
10
|
+
const metro_1 = require("./interceptors/metro");
|
|
11
|
+
const session_1 = require("./core/session");
|
|
12
|
+
const device_1 = require("./core/device");
|
|
13
|
+
const sanitize_1 = require("./core/sanitize");
|
|
14
|
+
|
|
25
15
|
class QALinkSDK {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* appVersion: '1.2.3',
|
|
40
|
-
* captureRuntimeErrors: true,
|
|
41
|
-
* console: {
|
|
42
|
-
* captureLogs: true,
|
|
43
|
-
* captureWarnings: true,
|
|
44
|
-
* captureErrors: true,
|
|
45
|
-
* ignorePatterns: ['[react-query]'],
|
|
46
|
-
* },
|
|
47
|
-
* });
|
|
48
|
-
*/
|
|
49
|
-
async init(config) {
|
|
50
|
-
var _a;
|
|
51
|
-
if (this.initialized) {
|
|
52
|
-
console.warn('[QALink] Already initialized. Call destroy() first.');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (config.enabled === false)
|
|
56
|
-
return;
|
|
57
|
-
this.config = Object.assign(Object.assign({ enabled: true, logNetworkBodies: false, sensitiveHeaders: [], sensitiveUrlPatterns: [], captureRuntimeErrors: true, console: {
|
|
58
|
-
captureLogs: true,
|
|
59
|
-
captureWarnings: true,
|
|
60
|
-
captureErrors: true,
|
|
61
|
-
ignorePatterns: [],
|
|
62
|
-
includePatterns: [],
|
|
63
|
-
}, debug: false }, config), {
|
|
64
|
-
// Merge profundo de console config
|
|
65
|
-
console: Object.assign({ captureLogs: true, captureWarnings: true, captureErrors: true, ignorePatterns: [], includePatterns: [] }, ((_a = config.console) !== null && _a !== void 0 ? _a : {})) });
|
|
66
|
-
// 1. WebSocket
|
|
67
|
-
this.transport = new websocket_1.WebSocketTransport(this.config.serverUrl, this.config.debug);
|
|
68
|
-
this.transport.connect();
|
|
69
|
-
// 2. Evento de inicio de sesión
|
|
70
|
-
const deviceInfo = await (0, session_1.getDeviceInfo)(config.appVersion);
|
|
71
|
-
const sessionEvent = {
|
|
72
|
-
id: (0, session_1.generateId)(),
|
|
73
|
-
type: 'session_start',
|
|
74
|
-
sessionId: (0, session_1.getSessionId)(),
|
|
75
|
-
timestamp: Date.now(),
|
|
76
|
-
deviceInfo,
|
|
77
|
-
appVersion: config.appVersion,
|
|
78
|
-
};
|
|
79
|
-
this.transport.send(sessionEvent);
|
|
80
|
-
const getScreen = () => this.currentScreen;
|
|
81
|
-
// 3. Interceptor de fetch nativo
|
|
82
|
-
this.cleanups.push((0, fetch_1.setupFetchInterceptor)(this.transport, this.config));
|
|
83
|
-
// 4. Interceptor de consola (console.log/warn/error + Metro logs)
|
|
84
|
-
this.cleanups.push((0, console_1.setupConsoleInterceptor)(this.transport, this.config, getScreen));
|
|
85
|
-
// 5. Handler de errores del runtime de RN (pantalla roja, yellow box, promises)
|
|
86
|
-
if (this.config.captureRuntimeErrors) {
|
|
87
|
-
this.cleanups.push((0, runtime_1.setupRuntimeErrorHandler)(this.transport, this.config, getScreen));
|
|
88
|
-
}
|
|
89
|
-
// 6. Handler legacy de errores JS (fallback para versiones viejas de RN)
|
|
90
|
-
this.cleanups.push((0, errors_1.setupErrorHandlers)(this.transport, this.config));
|
|
91
|
-
this.initialized = true;
|
|
92
|
-
this.log('SDK initialized ✅', { sessionId: (0, session_1.getSessionId)() });
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Registra interceptor para una instancia específica de Axios.
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* import axios from 'axios';
|
|
99
|
-
* QALink.interceptAxios(axios);
|
|
100
|
-
*
|
|
101
|
-
* // También con instancias custom:
|
|
102
|
-
* const api = axios.create({ baseURL: 'https://api.miapp.com' });
|
|
103
|
-
* QALink.interceptAxios(api);
|
|
104
|
-
*/
|
|
105
|
-
interceptAxios(axiosInstance) {
|
|
106
|
-
if (!this.transport || !this.config) {
|
|
107
|
-
console.warn('[QALink] Call init() before interceptAxios()');
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
this.cleanups.push((0, axios_1.setupAxiosInterceptor)(axiosInstance, this.transport, this.config));
|
|
111
|
-
this.log('Axios interceptor installed ✅');
|
|
16
|
+
constructor() {
|
|
17
|
+
this.transport = null;
|
|
18
|
+
this.config = null;
|
|
19
|
+
this.cleanups = [];
|
|
20
|
+
this.initialized = false;
|
|
21
|
+
this.currentScreen= 'unknown';
|
|
22
|
+
this._deviceId = 'unknown';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async init(config) {
|
|
26
|
+
if (this.initialized) {
|
|
27
|
+
console.warn('[QALink] Already initialized. Call destroy() first.');
|
|
28
|
+
return;
|
|
112
29
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
* QALink.addBreadcrumb('TAP → Confirmar Compra', { total: 150 });
|
|
118
|
-
*/
|
|
119
|
-
addBreadcrumb(action, data) {
|
|
120
|
-
var _a, _b;
|
|
121
|
-
if (!this.transport || !this.initialized)
|
|
122
|
-
return;
|
|
123
|
-
const event = {
|
|
124
|
-
id: (0, session_1.generateId)(),
|
|
125
|
-
type: 'breadcrumb',
|
|
126
|
-
action,
|
|
127
|
-
screen: this.currentScreen,
|
|
128
|
-
data,
|
|
129
|
-
timestamp: Date.now(),
|
|
130
|
-
sessionId: (0, session_1.getSessionId)(),
|
|
131
|
-
};
|
|
132
|
-
this.transport.send(event);
|
|
133
|
-
(_b = (_a = this.config) === null || _a === void 0 ? void 0 : _a.onEvent) === null || _b === void 0 ? void 0 : _b.call(_a, event);
|
|
30
|
+
|
|
31
|
+
if (config.enabled === false) {
|
|
32
|
+
this._log('SDK disabled. No data will be sent.');
|
|
33
|
+
return;
|
|
134
34
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
35
|
+
|
|
36
|
+
this.config = {
|
|
37
|
+
enabled: true,
|
|
38
|
+
environment: 'dev',
|
|
39
|
+
logNetworkBodies: false,
|
|
40
|
+
sensitiveHeaders: [],
|
|
41
|
+
sensitiveUrlPatterns: [],
|
|
42
|
+
sensitiveBodyFields: [],
|
|
43
|
+
captureRuntimeErrors: true,
|
|
44
|
+
captureMetroErrors: true,
|
|
45
|
+
console: { captureLogs: true, captureWarnings: true, captureErrors: true, ignorePatterns: [], includePatterns: [] },
|
|
46
|
+
debug: false,
|
|
47
|
+
...config,
|
|
48
|
+
console: {
|
|
49
|
+
captureLogs: true, captureWarnings: true, captureErrors: true,
|
|
50
|
+
ignorePatterns: [], includePatterns: [],
|
|
51
|
+
...(config.console ?? {}),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this._deviceId = await (0, device_1.getDeviceId)();
|
|
56
|
+
|
|
57
|
+
this.transport = new websocket_1.WebSocketTransport(this.config.serverUrl, this.config.debug);
|
|
58
|
+
this.transport.connect();
|
|
59
|
+
|
|
60
|
+
const deviceInfo = await (0, session_1.getDeviceInfo)(config.appVersion);
|
|
61
|
+
const sessionEvent = {
|
|
62
|
+
id: (0, session_1.generateId)(),
|
|
63
|
+
type: 'session_start',
|
|
64
|
+
sessionId: (0, session_1.getSessionId)(),
|
|
65
|
+
deviceId: this._deviceId,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
deviceInfo,
|
|
68
|
+
appVersion: config.appVersion,
|
|
69
|
+
environment: this.config.environment ?? 'dev',
|
|
70
|
+
};
|
|
71
|
+
this.transport.send(sessionEvent);
|
|
72
|
+
|
|
73
|
+
const getScreen = () => this.currentScreen;
|
|
74
|
+
const getDevId = () => this._deviceId;
|
|
75
|
+
|
|
76
|
+
this.cleanups.push((0, fetch_1.setupFetchInterceptor)(this.transport, this.config, getDevId));
|
|
77
|
+
this.cleanups.push((0, console_1.setupConsoleInterceptor)(this.transport, this.config, getScreen));
|
|
78
|
+
|
|
79
|
+
if (this.config.captureRuntimeErrors) {
|
|
80
|
+
this.cleanups.push((0, runtime_1.setupRuntimeErrorHandler)(this.transport, this.config, getScreen));
|
|
144
81
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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));
|
|
149
86
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
87
|
+
|
|
88
|
+
this.cleanups.push((0, errors_1.setupErrorHandlers)(this.transport, this.config));
|
|
89
|
+
|
|
90
|
+
this.initialized = true;
|
|
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
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interceptAxios(axiosInstance) {
|
|
100
|
+
if (!this.transport || !this.config) { console.warn('[QALink] Call init() before interceptAxios()'); return; }
|
|
101
|
+
this.cleanups.push((0, axios_1.setupAxiosInterceptor)(axiosInstance, this.transport, this.config));
|
|
102
|
+
this._log('Axios interceptor installed ✅');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
addBreadcrumb(action, data) {
|
|
106
|
+
if (!this.transport || !this.initialized) return;
|
|
107
|
+
const event = {
|
|
108
|
+
id: (0, session_1.generateId)(),
|
|
109
|
+
type: 'breadcrumb',
|
|
110
|
+
action, screen: this.currentScreen, data,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
sessionId: (0, session_1.getSessionId)(),
|
|
113
|
+
deviceId: this._deviceId,
|
|
114
|
+
};
|
|
115
|
+
this.transport.send(event);
|
|
116
|
+
this.config?.onEvent?.(event);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
logRequest(options) {
|
|
120
|
+
if (!this.transport || !this.initialized) return;
|
|
121
|
+
const { method, url, statusCode, requestPayload, responsePayload, durationMs } = options;
|
|
122
|
+
const isAuth = (0, sanitize_1.isAuthUrl)(url);
|
|
123
|
+
const extraFields = this.config?.sensitiveBodyFields ?? [];
|
|
124
|
+
let sanitizedReq = requestPayload, sanitizedRes = responsePayload, hadSensitiveData = false;
|
|
125
|
+
|
|
126
|
+
if (requestPayload) {
|
|
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; }
|
|
159
129
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
130
|
+
if (responsePayload) {
|
|
131
|
+
const r = (0, sanitize_1.sanitizePayload)(responsePayload, extraFields);
|
|
132
|
+
sanitizedRes = r.sanitized;
|
|
133
|
+
if (r.hadSensitiveData) hadSensitiveData = true;
|
|
165
134
|
}
|
|
135
|
+
|
|
136
|
+
const event = {
|
|
137
|
+
id: (0, session_1.generateId)(),
|
|
138
|
+
type: 'network_log',
|
|
139
|
+
method: method.toUpperCase(), url, statusCode,
|
|
140
|
+
requestPayload: sanitizedReq, responsePayload: sanitizedRes, durationMs,
|
|
141
|
+
timestamp: Date.now(),
|
|
142
|
+
sessionId: (0, session_1.getSessionId)(),
|
|
143
|
+
deviceId: this._deviceId,
|
|
144
|
+
screen: this.currentScreen,
|
|
145
|
+
sanitized: hadSensitiveData,
|
|
146
|
+
};
|
|
147
|
+
this.transport.send(event);
|
|
148
|
+
this.config?.onEvent?.(event);
|
|
149
|
+
this._log(`logRequest: ${method.toUpperCase()} ${url} ${statusCode ?? '?'}${hadSensitiveData ? ' [sanitized]' : ''}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setScreen(screenName) {
|
|
153
|
+
if (!this.initialized) return;
|
|
154
|
+
this.currentScreen = screenName;
|
|
155
|
+
this.addBreadcrumb(`NAVIGATE → ${screenName}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getDeviceId() { return this._deviceId; }
|
|
159
|
+
getStatus() { return this.transport?.getStatus() ?? 'not initialized'; }
|
|
160
|
+
|
|
161
|
+
destroy() {
|
|
162
|
+
this.cleanups.forEach(fn => fn());
|
|
163
|
+
this.cleanups = [];
|
|
164
|
+
this.transport?.disconnect();
|
|
165
|
+
this.transport = null; this.config = null; this.initialized = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_log(...args) {
|
|
169
|
+
if (this.config?.debug) console.log('[QALink]', ...args);
|
|
170
|
+
}
|
|
166
171
|
}
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
|
|
173
|
+
const QALink = new QALinkSDK();
|
|
174
|
+
exports.QALink = QALink;
|
|
175
|
+
|
|
169
176
|
var classifier_1 = require("./core/classifier");
|
|
170
177
|
Object.defineProperty(exports, "getSourceLabel", { enumerable: true, get: function () { return classifier_1.getSourceLabel; } });
|
|
171
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,83 +1,108 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setupFetchInterceptor =
|
|
3
|
+
exports.setupFetchInterceptor = void 0;
|
|
4
4
|
const classifier_1 = require("../core/classifier");
|
|
5
|
-
const session_1
|
|
5
|
+
const session_1 = require("../core/session");
|
|
6
|
+
const sanitize_1 = require("../core/sanitize");
|
|
7
|
+
|
|
6
8
|
const originalFetch = global.fetch;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
|
|
10
|
+
function setupFetchInterceptor(transport, config, getDeviceId) {
|
|
11
|
+
global.fetch = async (input, init) => {
|
|
12
|
+
const requestId = (0, session_1.generateId)();
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
const method = init?.method?.toUpperCase() ?? 'GET';
|
|
15
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
16
|
+
|
|
17
|
+
if (shouldIgnoreUrl(url, config.sensitiveUrlPatterns ?? [])) {
|
|
18
|
+
return originalFetch(input, init);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let response, responseBody, statusCode;
|
|
22
|
+
const isAuth = (0, sanitize_1.isAuthUrl)(url);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
response = await originalFetch(input, init);
|
|
26
|
+
statusCode = response.status;
|
|
27
|
+
|
|
28
|
+
if (config.logNetworkBodies) {
|
|
20
29
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
url,
|
|
37
|
-
statusCode,
|
|
38
|
-
requestHeaders: (0, session_1.sanitizeHeaders)(headersToObject(new Headers(init === null || init === void 0 ? void 0 : init.headers)), config.sensitiveHeaders),
|
|
39
|
-
requestBody: (0, session_1.sanitizeBody)(init === null || init === void 0 ? void 0 : init.body, (_d = config.logNetworkBodies) !== null && _d !== void 0 ? _d : false),
|
|
40
|
-
responseBody: (0, session_1.sanitizeBody)(responseBody, (_e = config.logNetworkBodies) !== null && _e !== void 0 ? _e : false),
|
|
41
|
-
durationMs: Date.now() - startTime,
|
|
42
|
-
timestamp: Date.now(),
|
|
43
|
-
source: (0, classifier_1.classifyErrorSource)(statusCode),
|
|
44
|
-
sessionId: (0, session_1.getSessionId)(),
|
|
45
|
-
};
|
|
46
|
-
transport.send(event);
|
|
47
|
-
(_f = config.onEvent) === null || _f === void 0 ? void 0 : _f.call(config, event);
|
|
48
|
-
return response;
|
|
30
|
+
const cloned = response.clone();
|
|
31
|
+
responseBody = await cloned.json().catch(() => cloned.text());
|
|
32
|
+
} catch { responseBody = '[could not read body]'; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let reqBody = init?.body;
|
|
36
|
+
let sanitizedFlag = false;
|
|
37
|
+
if (reqBody && config.logNetworkBodies) {
|
|
38
|
+
if (isAuth) {
|
|
39
|
+
reqBody = '[REDACTED - auth endpoint]';
|
|
40
|
+
sanitizedFlag = true;
|
|
41
|
+
} else {
|
|
42
|
+
const { sanitized, hadSensitiveData } = (0, sanitize_1.sanitizePayload)(reqBody, config.sensitiveBodyFields);
|
|
43
|
+
reqBody = sanitized;
|
|
44
|
+
sanitizedFlag = hadSensitiveData;
|
|
49
45
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
46
|
+
} else {
|
|
47
|
+
reqBody = '[body omitted - enable logNetworkBodies]';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let resBody = responseBody;
|
|
51
|
+
if (resBody && config.logNetworkBodies) {
|
|
52
|
+
const { sanitized } = (0, sanitize_1.sanitizePayload)(resBody, config.sensitiveBodyFields);
|
|
53
|
+
resBody = sanitized;
|
|
54
|
+
} else if (!config.logNetworkBodies) {
|
|
55
|
+
resBody = '[body omitted - enable logNetworkBodies]';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const event = {
|
|
59
|
+
id: requestId,
|
|
60
|
+
type: statusCode >= 400 ? 'network_error' : 'network_request',
|
|
61
|
+
method, url, statusCode,
|
|
62
|
+
requestHeaders: (0, sanitize_1.sanitizeHeaders)(headersToObject(new Headers(init?.headers)), config.sensitiveHeaders),
|
|
63
|
+
requestBody: reqBody,
|
|
64
|
+
responseBody: resBody,
|
|
65
|
+
durationMs: Date.now() - startTime,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
source: (0, classifier_1.classifyErrorSource)(statusCode),
|
|
68
|
+
sessionId: (0, session_1.getSessionId)(),
|
|
69
|
+
deviceId: getDeviceId(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
transport.send(event);
|
|
73
|
+
config.onEvent?.(event);
|
|
74
|
+
return response;
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const event = {
|
|
78
|
+
id: requestId,
|
|
79
|
+
type: 'network_error',
|
|
80
|
+
method, url,
|
|
81
|
+
statusCode: undefined,
|
|
82
|
+
requestHeaders: (0, sanitize_1.sanitizeHeaders)(headersToObject(new Headers(init?.headers)), config.sensitiveHeaders),
|
|
83
|
+
requestBody: '[body omitted on error]',
|
|
84
|
+
durationMs: Date.now() - startTime,
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
source: (0, classifier_1.classifyErrorSource)(undefined, error),
|
|
87
|
+
sessionId: (0, session_1.getSessionId)(),
|
|
88
|
+
deviceId: getDeviceId(),
|
|
89
|
+
};
|
|
90
|
+
transport.send(event);
|
|
91
|
+
config.onEvent?.(event);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return () => { global.fetch = originalFetch; };
|
|
72
97
|
}
|
|
98
|
+
exports.setupFetchInterceptor = setupFetchInterceptor;
|
|
99
|
+
|
|
73
100
|
function headersToObject(headers) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
return result;
|
|
101
|
+
const result = {};
|
|
102
|
+
headers.forEach((value, key) => { result[key] = value; });
|
|
103
|
+
return result;
|
|
79
104
|
}
|
|
105
|
+
|
|
80
106
|
function shouldIgnoreUrl(url, patterns) {
|
|
81
|
-
|
|
107
|
+
return patterns.some(p => url.includes(p));
|
|
82
108
|
}
|
|
83
|
-
//# sourceMappingURL=fetch.js.map
|
|
@@ -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,23 +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
|
-
|
|
16
|
-
"@types/react-native": "^0.72.0"
|
|
17
|
-
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "tsc",
|
|
20
|
-
"typecheck": "tsc --noEmit"
|
|
21
|
-
},
|
|
11
|
+
"devDependencies": { "typescript": "^5.0.0", "@types/react-native": "^0.72.0" },
|
|
12
|
+
"scripts": { "build": "tsc", "typecheck": "tsc --noEmit" },
|
|
22
13
|
"license": "MIT"
|
|
23
14
|
}
|