react-native-qalink 0.1.2 → 0.2.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.
@@ -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,186 @@
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.getSourceLabel = exports.QALink = void 0;
3
+ exports.QALink = void 0;
18
4
  const websocket_1 = require("./transport/websocket");
19
- const axios_1 = require("./interceptors/axios");
20
- const fetch_1 = require("./interceptors/fetch");
21
- const errors_1 = require("./interceptors/errors");
22
- const console_1 = require("./interceptors/console");
23
- const runtime_1 = require("./interceptors/runtime");
24
- const session_1 = require("./core/session");
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 session_1 = require("./core/session");
11
+ const device_1 = require("./core/device");
12
+ const sanitize_1 = require("./core/sanitize");
13
+
25
14
  class QALinkSDK {
26
- constructor() {
27
- this.transport = null;
28
- this.config = null;
29
- this.cleanups = [];
30
- this.initialized = false;
31
- this.currentScreen = 'unknown';
32
- }
33
- /**
34
- * Inicializa el SDK. Llamar al inicio de la app, antes de cualquier request.
35
- *
36
- * @example
37
- * QALink.init({
38
- * serverUrl: 'ws://192.168.1.100:3000',
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 ✅');
112
- }
113
- /**
114
- * Registra una acción del usuario (breadcrumb).
115
- *
116
- * @example
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);
15
+ constructor() {
16
+ this.transport = null;
17
+ this.config = null;
18
+ this.cleanups = [];
19
+ this.initialized = false;
20
+ this.currentScreen= 'unknown';
21
+ this._deviceId = 'unknown';
22
+ }
23
+
24
+ async init(config) {
25
+ if (this.initialized) {
26
+ console.warn('[QALink] Already initialized. Call destroy() first.');
27
+ return;
134
28
  }
135
- /**
136
- * Registra la pantalla actual. Llamar en cada cambio de navegación.
137
- * Genera automáticamente un breadcrumb de navegación.
138
- */
139
- setScreen(screenName) {
140
- if (!this.initialized)
141
- return;
142
- this.currentScreen = screenName;
143
- this.addBreadcrumb(`NAVIGATE → ${screenName}`);
29
+
30
+ // ── 1. enabled flag ───────────────────────────────────────────────────────
31
+ if (config.enabled === false) {
32
+ this._log('SDK disabled (enabled: false). No data will be sent.');
33
+ return;
144
34
  }
145
- /** Estado actual de la conexión WebSocket */
146
- getStatus() {
147
- var _a, _b;
148
- return (_b = (_a = this.transport) === null || _a === void 0 ? void 0 : _a.getStatus()) !== null && _b !== void 0 ? _b : 'not initialized';
35
+
36
+ this.config = {
37
+ enabled: true,
38
+ environment: 'dev',
39
+ logNetworkBodies: false,
40
+ sensitiveHeaders: [],
41
+ sensitiveUrlPatterns: [],
42
+ sensitiveBodyFields: [],
43
+ captureRuntimeErrors: true,
44
+ console: { captureLogs: true, captureWarnings: true, captureErrors: true, ignorePatterns: [], includePatterns: [] },
45
+ debug: false,
46
+ ...config,
47
+ console: {
48
+ captureLogs: true, captureWarnings: true, captureErrors: true,
49
+ ignorePatterns: [], includePatterns: [],
50
+ ...(config.console ?? {}),
51
+ },
52
+ };
53
+
54
+ // ── 2. deviceId ───────────────────────────────────────────────────────────
55
+ this._deviceId = await (0, device_1.getDeviceId)();
56
+
57
+ // ── 3. WebSocket ──────────────────────────────────────────────────────────
58
+ this.transport = new websocket_1.WebSocketTransport(this.config.serverUrl, this.config.debug);
59
+ this.transport.connect();
60
+
61
+ // ── 4. session_start ──────────────────────────────────────────────────────
62
+ const deviceInfo = await (0, session_1.getDeviceInfo)(config.appVersion);
63
+ const sessionEvent = {
64
+ id: (0, session_1.generateId)(),
65
+ type: 'session_start',
66
+ sessionId: (0, session_1.getSessionId)(),
67
+ deviceId: this._deviceId,
68
+ timestamp: Date.now(),
69
+ deviceInfo,
70
+ appVersion: config.appVersion,
71
+ environment: this.config.environment ?? 'dev',
72
+ };
73
+ this.transport.send(sessionEvent);
74
+
75
+ const getScreen = () => this.currentScreen;
76
+ const getDevId = () => this._deviceId;
77
+
78
+ // ── 5. Interceptors ───────────────────────────────────────────────────────
79
+ this.cleanups.push((0, fetch_1.setupFetchInterceptor)(this.transport, this.config, getDevId));
80
+ this.cleanups.push((0, console_1.setupConsoleInterceptor)(this.transport, this.config, getScreen));
81
+ if (this.config.captureRuntimeErrors) {
82
+ this.cleanups.push((0, runtime_1.setupRuntimeErrorHandler)(this.transport, this.config, getScreen));
149
83
  }
150
- /** Limpia todos los interceptores y desconecta. Útil en tests. */
151
- destroy() {
152
- var _a;
153
- this.cleanups.forEach(fn => fn());
154
- this.cleanups = [];
155
- (_a = this.transport) === null || _a === void 0 ? void 0 : _a.disconnect();
156
- this.transport = null;
157
- this.config = null;
158
- this.initialized = false;
84
+ this.cleanups.push((0, errors_1.setupErrorHandlers)(this.transport, this.config));
85
+
86
+ this.initialized = true;
87
+ this._log('SDK initialized ✅', { sessionId: (0, session_1.getSessionId)(), deviceId: this._deviceId, environment: this.config.environment });
88
+ }
89
+
90
+ interceptAxios(axiosInstance) {
91
+ if (!this.transport || !this.config) { console.warn('[QALink] Call init() before interceptAxios()'); return; }
92
+ this.cleanups.push((0, axios_1.setupAxiosInterceptor)(axiosInstance, this.transport, this.config));
93
+ this._log('Axios interceptor installed ✅');
94
+ }
95
+
96
+ addBreadcrumb(action, data) {
97
+ if (!this.transport || !this.initialized) return;
98
+ const event = {
99
+ id: (0, session_1.generateId)(),
100
+ type: 'breadcrumb',
101
+ action,
102
+ screen: this.currentScreen,
103
+ data,
104
+ timestamp: Date.now(),
105
+ sessionId: (0, session_1.getSessionId)(),
106
+ deviceId: this._deviceId,
107
+ };
108
+ this.transport.send(event);
109
+ this.config?.onEvent?.(event);
110
+ }
111
+
112
+ logRequest(options) {
113
+ if (!this.transport || !this.initialized) return;
114
+ const { method, url, statusCode, requestPayload, responsePayload, durationMs } = options;
115
+ const isAuth = (0, sanitize_1.isAuthUrl)(url);
116
+ const extraFields = this.config?.sensitiveBodyFields ?? [];
117
+ let sanitizedReq = requestPayload;
118
+ let sanitizedRes = responsePayload;
119
+ let hadSensitiveData = false;
120
+
121
+ 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
+ }
159
130
  }
160
- log(...args) {
161
- var _a;
162
- if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.debug) {
163
- console.log('[QALink]', ...args);
164
- }
131
+ if (responsePayload) {
132
+ const result = (0, sanitize_1.sanitizePayload)(responsePayload, extraFields);
133
+ sanitizedRes = result.sanitized;
134
+ if (result.hadSensitiveData) hadSensitiveData = true;
165
135
  }
136
+
137
+ const event = {
138
+ id: (0, session_1.generateId)(),
139
+ type: 'network_log',
140
+ method: method.toUpperCase(),
141
+ url,
142
+ statusCode,
143
+ requestPayload: sanitizedReq,
144
+ responsePayload: sanitizedRes,
145
+ durationMs,
146
+ timestamp: Date.now(),
147
+ sessionId: (0, session_1.getSessionId)(),
148
+ deviceId: this._deviceId,
149
+ screen: this.currentScreen,
150
+ sanitized: hadSensitiveData,
151
+ };
152
+
153
+ this.transport.send(event);
154
+ this.config?.onEvent?.(event);
155
+ this._log(`logRequest: ${method.toUpperCase()} ${url} ${statusCode ?? '?'}${hadSensitiveData ? ' [sanitized]' : ''}`);
156
+ }
157
+
158
+ setScreen(screenName) {
159
+ if (!this.initialized) return;
160
+ this.currentScreen = screenName;
161
+ this.addBreadcrumb(`NAVIGATE → ${screenName}`);
162
+ }
163
+
164
+ getDeviceId() { return this._deviceId; }
165
+ getStatus() { return this.transport?.getStatus() ?? 'not initialized'; }
166
+
167
+ destroy() {
168
+ this.cleanups.forEach(fn => fn());
169
+ this.cleanups = [];
170
+ this.transport?.disconnect();
171
+ this.transport = null;
172
+ this.config = null;
173
+ this.initialized = false;
174
+ }
175
+
176
+ _log(...args) {
177
+ if (this.config?.debug) console.log('[QALink]', ...args);
178
+ }
166
179
  }
167
- exports.QALink = new QALinkSDK();
168
- __exportStar(require("./types"), exports);
180
+
181
+ const QALink = new QALinkSDK();
182
+ exports.QALink = QALink;
183
+
184
+ // Re-exports
169
185
  var classifier_1 = require("./core/classifier");
170
186
  Object.defineProperty(exports, "getSourceLabel", { enumerable: true, get: function () { return classifier_1.getSourceLabel; } });
171
- //# sourceMappingURL=index.js.map
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupConsoleInterceptor = setupConsoleInterceptor;
3
+ exports.setupConsoleInterceptor = void 0;
4
4
  const session_1 = require("../core/session");
5
- // Guardamos referencia a los métodos originales ANTES de sobrescribir
5
+
6
6
  const originalConsole = {
7
7
  log: console.log.bind(console),
8
8
  warn: console.warn.bind(console),
@@ -10,132 +10,88 @@ const originalConsole = {
10
10
  info: console.info.bind(console),
11
11
  debug: console.debug.bind(console),
12
12
  };
13
- /**
14
- * Patrones que identifican logs internos de React Native
15
- * para clasificarlos correctamente en el dashboard.
16
- */
17
- const RN_WARNING_PATTERNS = [
18
- 'Warning:',
19
- 'VirtualizedList',
20
- 'Each child in a list',
21
- 'componentWillMount',
22
- 'componentWillReceiveProps',
23
- 'componentWillUpdate',
24
- 'findDOMNode',
25
- 'ReactDOM.render',
26
- 'key prop',
27
- 'unique "key"',
28
- 'Maximum update depth',
29
- ];
30
- const RN_ERROR_PATTERNS = [
31
- 'Invariant Violation',
32
- 'Text strings must be rendered',
33
- 'Cannot update a component',
34
- 'Element type is invalid',
35
- 'Objects are not valid as a React child',
36
- 'TypeError',
37
- 'ReferenceError',
38
- 'Cannot read property',
39
- 'Cannot read properties',
40
- 'is not a function',
41
- 'undefined is not an object',
42
- 'null is not an object',
43
- ];
44
- const METRO_ERROR_PATTERNS = [
45
- 'Unable to resolve module',
46
- 'Module not found',
47
- 'SyntaxError',
48
- 'TransformError',
49
- 'bundling failed',
50
- 'Metro Bundler',
51
- 'Error: Metro',
52
- ];
53
- const THIRD_PARTY_PATTERNS = [
54
- '[react-navigation]',
55
- '[react-query]',
56
- '[redux]',
57
- '[mobx]',
58
- '[zustand]',
59
- 'Reanimated',
60
- 'Gesture Handler',
61
- 'SafeAreaProvider',
62
- ];
13
+
14
+ const RN_WARNING_PATTERNS = ['Warning:', 'VirtualizedList', 'Each child in a list', 'componentWillMount', 'componentWillReceiveProps', 'componentWillUpdate', 'findDOMNode', 'ReactDOM.render', 'key prop', 'unique "key"', 'Maximum update depth'];
15
+ const RN_ERROR_PATTERNS = ['Invariant Violation', 'Text strings must be rendered', 'Cannot update a component', 'Element type is invalid', 'Objects are not valid as a React child', 'TypeError', 'ReferenceError', 'Cannot read property', 'Cannot read properties', 'is not a function', 'undefined is not an object', 'null is not an object'];
16
+ const METRO_ERROR_PATTERNS = ['Unable to resolve module', 'Module not found', 'SyntaxError', 'TransformError', 'bundling failed', 'Metro Bundler', 'Error: Metro'];
17
+ const THIRD_PARTY_PATTERNS = ['[react-navigation]', '[react-query]', '[redux]', '[mobx]', '[zustand]', 'Reanimated', 'Gesture Handler', 'SafeAreaProvider'];
18
+
63
19
  function classifyConsoleMessage(message) {
64
- if (METRO_ERROR_PATTERNS.some(p => message.includes(p)))
65
- return 'rn_error';
66
- if (RN_ERROR_PATTERNS.some(p => message.includes(p)))
67
- return 'rn_error';
68
- if (RN_WARNING_PATTERNS.some(p => message.includes(p)))
69
- return 'rn_warning';
70
- if (THIRD_PARTY_PATTERNS.some(p => message.includes(p)))
71
- return 'third_party';
20
+ if (METRO_ERROR_PATTERNS.some(p => message.includes(p))) return 'rn_error';
21
+ if (RN_ERROR_PATTERNS.some(p => message.includes(p))) return 'rn_error';
22
+ if (RN_WARNING_PATTERNS.some(p => message.includes(p))) return 'rn_warning';
23
+ if (THIRD_PARTY_PATTERNS.some(p => message.includes(p))) return 'third_party';
72
24
  return 'user_log';
73
25
  }
26
+
74
27
  function serializeArg(arg) {
75
- var _a;
76
- if (arg === null)
77
- return 'null';
78
- if (arg === undefined)
79
- return 'undefined';
80
- if (typeof arg === 'string')
81
- return arg;
82
- if (typeof arg === 'number' || typeof arg === 'boolean')
83
- return String(arg);
84
- if (arg instanceof Error)
85
- return `${arg.name}: ${arg.message}\n${(_a = arg.stack) !== null && _a !== void 0 ? _a : ''}`;
86
- try {
87
- return JSON.stringify(arg, null, 2);
88
- }
89
- catch (_b) {
90
- return String(arg);
91
- }
28
+ if (arg === null) return 'null';
29
+ if (arg === undefined) return 'undefined';
30
+ if (typeof arg === 'string') return arg;
31
+ if (typeof arg === 'number' || typeof arg === 'boolean') return String(arg);
32
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack ?? ''}`;
33
+ try { return JSON.stringify(arg, null, 2); } catch { return String(arg); }
92
34
  }
35
+
93
36
  function shouldCapture(message, config) {
94
- var _a;
95
- if (!config)
96
- return true;
97
- // Ignorar si matchea algún patrón de exclusión
98
- if ((_a = config.ignorePatterns) === null || _a === void 0 ? void 0 : _a.some(p => message.includes(p)))
99
- return false;
100
- // Si hay patrones de inclusión, solo capturar si matchea alguno
37
+ if (!config) return true;
38
+ if (config.ignorePatterns?.some(p => message.includes(p))) return false;
101
39
  if (config.includePatterns && config.includePatterns.length > 0) {
102
40
  return config.includePatterns.some(p => message.includes(p));
103
41
  }
104
42
  return true;
105
43
  }
44
+
45
+ // FLAG para prevenir loops de recursión
46
+ let _isSending = false;
47
+
106
48
  function setupConsoleInterceptor(transport, config, getCurrentScreen) {
107
- var _a;
108
- const consoleConfig = (_a = config.console) !== null && _a !== void 0 ? _a : {};
109
- const { captureLogs = true, captureWarnings = true, captureErrors = true, } = consoleConfig;
49
+ const consoleConfig = config.console ?? {};
50
+ const { captureLogs = true, captureWarnings = true, captureErrors = true } = consoleConfig;
51
+
110
52
  function intercept(level, eventType, originalFn) {
111
53
  return (...args) => {
112
- var _a;
113
- // Siempre llamar al original primero — Metro sigue mostrando todo
54
+ // SIEMPRE llamar el original primero
114
55
  originalFn(...args);
56
+
57
+ // Evitar loop recursivo — si ya estamos enviando, no volver a entrar
58
+ if (_isSending) return;
59
+
115
60
  const serializedArgs = args.map(serializeArg);
116
61
  const message = serializedArgs.join(' ');
117
- if (!shouldCapture(message, consoleConfig))
118
- return;
119
- // Evitar loop infinito si QALink mismo hace console.log en debug mode
120
- if (message.startsWith('[QALink]'))
121
- return;
122
- const category = classifyConsoleMessage(message);
123
- const event = {
124
- id: (0, session_1.generateId)(),
125
- type: eventType,
126
- level,
127
- message,
128
- args: serializedArgs,
129
- category,
130
- timestamp: Date.now(),
131
- screen: getCurrentScreen(),
132
- sessionId: (0, session_1.getSessionId)(),
133
- };
134
- transport.send(event);
135
- (_a = config.onEvent) === null || _a === void 0 ? void 0 : _a.call(config, event);
62
+
63
+ // Ignorar logs internos de QALink
64
+ if (message.startsWith('[QALink]')) return;
65
+
66
+ // Ignorar errores de call stack para evitar loop infinito
67
+ if (message.includes('Maximum call stack') || message.includes('call stack size exceeded')) return;
68
+
69
+ if (!shouldCapture(message, consoleConfig)) return;
70
+
71
+ try {
72
+ _isSending = true;
73
+ const category = classifyConsoleMessage(message);
74
+ const event = {
75
+ id: (0, session_1.generateId)(),
76
+ type: eventType,
77
+ level,
78
+ message,
79
+ args: serializedArgs,
80
+ category,
81
+ timestamp: Date.now(),
82
+ screen: getCurrentScreen(),
83
+ sessionId: (0, session_1.getSessionId)(),
84
+ };
85
+ transport.send(event);
86
+ config.onEvent?.(event);
87
+ } catch {
88
+ // Nunca romper la app por un error del interceptor
89
+ } finally {
90
+ _isSending = false;
91
+ }
136
92
  };
137
93
  }
138
- // Sobrescribir métodos de consola
94
+
139
95
  if (captureLogs) {
140
96
  console.log = intercept('log', 'console_log', originalConsole.log);
141
97
  console.info = intercept('info', 'console_log', originalConsole.info);
@@ -147,7 +103,7 @@ function setupConsoleInterceptor(transport, config, getCurrentScreen) {
147
103
  if (captureErrors) {
148
104
  console.error = intercept('error', 'console_error', originalConsole.error);
149
105
  }
150
- // Cleanup: restaurar consola original
106
+
151
107
  return () => {
152
108
  console.log = originalConsole.log;
153
109
  console.warn = originalConsole.warn;
@@ -156,4 +112,4 @@ function setupConsoleInterceptor(transport, config, getCurrentScreen) {
156
112
  console.debug = originalConsole.debug;
157
113
  };
158
114
  }
159
- //# sourceMappingURL=console.js.map
115
+ exports.setupConsoleInterceptor = setupConsoleInterceptor;
@@ -1,83 +1,108 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupFetchInterceptor = setupFetchInterceptor;
3
+ exports.setupFetchInterceptor = void 0;
4
4
  const classifier_1 = require("../core/classifier");
5
- const session_1 = require("../core/session");
5
+ const session_1 = require("../core/session");
6
+ const sanitize_1 = require("../core/sanitize");
7
+
6
8
  const originalFetch = global.fetch;
7
- function setupFetchInterceptor(transport, config) {
8
- global.fetch = async (input, init) => {
9
- var _a, _b, _c, _d, _e, _f, _g, _h;
10
- const requestId = (0, session_1.generateId)();
11
- const startTime = Date.now();
12
- const method = (_b = (_a = init === null || init === void 0 ? void 0 : init.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : 'GET';
13
- const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
14
- if (shouldIgnoreUrl(url, (_c = config.sensitiveUrlPatterns) !== null && _c !== void 0 ? _c : [])) {
15
- return originalFetch(input, init);
16
- }
17
- let response;
18
- let responseBody;
19
- let statusCode;
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
- response = await originalFetch(input, init);
22
- statusCode = response.status;
23
- if (config.logNetworkBodies) {
24
- try {
25
- const cloned = response.clone();
26
- responseBody = await cloned.json().catch(() => cloned.text());
27
- }
28
- catch (_j) {
29
- responseBody = '[could not read body]';
30
- }
31
- }
32
- const event = {
33
- id: requestId,
34
- type: statusCode >= 400 ? 'network_error' : 'network_request',
35
- method,
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
- catch (error) {
51
- const event = {
52
- id: requestId,
53
- type: 'network_error',
54
- method,
55
- url,
56
- statusCode: undefined,
57
- requestHeaders: (0, session_1.sanitizeHeaders)(headersToObject(new Headers(init === null || init === void 0 ? void 0 : init.headers)), config.sensitiveHeaders),
58
- requestBody: (0, session_1.sanitizeBody)(init === null || init === void 0 ? void 0 : init.body, (_g = config.logNetworkBodies) !== null && _g !== void 0 ? _g : false),
59
- durationMs: Date.now() - startTime,
60
- timestamp: Date.now(),
61
- source: (0, classifier_1.classifyErrorSource)(undefined, error),
62
- sessionId: (0, session_1.getSessionId)(),
63
- };
64
- transport.send(event);
65
- (_h = config.onEvent) === null || _h === void 0 ? void 0 : _h.call(config, event);
66
- throw error;
67
- }
68
- };
69
- return () => {
70
- global.fetch = originalFetch;
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
- const result = {};
75
- headers.forEach((value, key) => {
76
- result[key] = value;
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
- return patterns.some(pattern => url.includes(pattern));
107
+ return patterns.some(p => url.includes(p));
82
108
  }
83
- //# sourceMappingURL=fetch.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-qalink",
3
- "version": "0.1.2",
3
+ "version": "0.2.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",
@@ -15,9 +15,6 @@
15
15
  "typescript": "^5.0.0",
16
16
  "@types/react-native": "^0.72.0"
17
17
  },
18
- "scripts": {
19
- "build": "tsc",
20
- "typecheck": "tsc --noEmit"
21
- },
18
+ "scripts": { "build": "tsc", "typecheck": "tsc --noEmit" },
22
19
  "license": "MIT"
23
20
  }