reactoradar 1.2.3
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/LICENSE +21 -0
- package/README.md +366 -0
- package/app.js +2450 -0
- package/assets/icon.svg +54 -0
- package/bin/cli.js +79 -0
- package/bin/open-debugger.sh +9 -0
- package/bin/setup.js +473 -0
- package/index.html +82 -0
- package/main.js +528 -0
- package/package.json +76 -0
- package/preload.js +31 -0
- package/sdk/RNDebugSDK.js +540 -0
- package/src/main/main.js +396 -0
- package/src/main/preload.js +28 -0
- package/src/renderer/app.js +221 -0
- package/src/renderer/components/object-tree.js +245 -0
- package/src/renderer/index.html +111 -0
- package/src/renderer/panels/console.js +248 -0
- package/src/renderer/panels/memory.js +60 -0
- package/src/renderer/panels/network.js +559 -0
- package/src/renderer/panels/performance.js +144 -0
- package/src/renderer/panels/react.js +31 -0
- package/src/renderer/panels/redux.js +159 -0
- package/src/renderer/panels/settings.js +93 -0
- package/src/renderer/panels/sources.js +189 -0
- package/src/renderer/panels/storage.js +134 -0
- package/src/renderer/state.js +132 -0
- package/src/renderer/styles/components.css +145 -0
- package/src/renderer/styles/console.css +73 -0
- package/src/renderer/styles/main.css +229 -0
- package/src/renderer/styles/network.css +242 -0
- package/src/renderer/styles/performance.css +45 -0
- package/src/renderer/styles/redux.css +77 -0
- package/src/renderer/styles/settings.css +63 -0
- package/src/renderer/styles/sources.css +48 -0
- package/src/renderer/styles/storage.css +28 -0
- package/src/renderer/styles/theme-light.css +57 -0
- package/styles.css +1308 -0
package/preload.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
4
|
+
|
|
5
|
+
const registeredChannels = new Set();
|
|
6
|
+
|
|
7
|
+
contextBridge.exposeInMainWorld('electronAPI', {
|
|
8
|
+
// Listen from main (idempotent — only one listener per channel)
|
|
9
|
+
on: (channel, cb) => {
|
|
10
|
+
const allowed = [
|
|
11
|
+
'ports', 'cdp-targets', 'redux-event', 'storage-event', 'network-event',
|
|
12
|
+
'console-event', 'perf-event', 'redux-connected', 'storage-connected', 'network-connected',
|
|
13
|
+
'react-dt-status', 'trigger-open-cdp', 'clear-all-ui', 'theme-changed', 'update-available',
|
|
14
|
+
];
|
|
15
|
+
if (allowed.includes(channel)) {
|
|
16
|
+
ipcRenderer.removeAllListeners(channel);
|
|
17
|
+
registeredChannels.add(channel);
|
|
18
|
+
ipcRenderer.on(channel, (_, ...args) => cb(...args));
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
// Send to main
|
|
22
|
+
openCDPTarget: (wsUrl) => ipcRenderer.send('open-cdp-target', wsUrl),
|
|
23
|
+
openReactDevTools: () => ipcRenderer.send('open-react-devtools'),
|
|
24
|
+
clearAll: () => ipcRenderer.send('clear-all'),
|
|
25
|
+
setTheme: (theme) => ipcRenderer.send('set-theme', theme),
|
|
26
|
+
setNetworkCapture: (enabled) => ipcRenderer.send('set-network-capture', enabled),
|
|
27
|
+
setStackTraceCapture: (enabled) => ipcRenderer.send('set-stack-trace-capture', enabled),
|
|
28
|
+
setNetworkThrottle: (profile) => ipcRenderer.send('set-network-throttle', profile),
|
|
29
|
+
readSourceFile: (filepath) => ipcRenderer.invoke('read-source-file', filepath),
|
|
30
|
+
openExternal: (url) => ipcRenderer.send('open-external', url),
|
|
31
|
+
});
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RNDebugSDK.js
|
|
3
|
+
* Place in: src/debug/RNDebugSDK.js
|
|
4
|
+
*
|
|
5
|
+
* Usage in index.js (MUST be the very first import):
|
|
6
|
+
* if (__DEV__) require('./src/debug/RNDebugSDK');
|
|
7
|
+
*
|
|
8
|
+
* For Redux, use the exported enhancer:
|
|
9
|
+
* import { reduxEnhancer } from './src/debug/RNDebugSDK';
|
|
10
|
+
* const store = configureStore({ ..., enhancers: [reduxEnhancer] });
|
|
11
|
+
*
|
|
12
|
+
* For AsyncStorage monitoring, wrap your AsyncStorage calls:
|
|
13
|
+
* import { watchAsyncStorage } from './src/debug/RNDebugSDK';
|
|
14
|
+
* watchAsyncStorage(); // call once early in app
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
if (!__DEV__) {
|
|
18
|
+
module.exports = { reduxEnhancer: x => x, watchAsyncStorage: () => {} };
|
|
19
|
+
} else {
|
|
20
|
+
|
|
21
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
22
|
+
// Android emulator → 10.0.2.2 | iOS sim → 127.0.0.1 | Device → your LAN IP
|
|
23
|
+
const HOST = '10.0.2.2';
|
|
24
|
+
|
|
25
|
+
const PORTS = {
|
|
26
|
+
NETWORK_AND_CONSOLE: 9092, // unified feed for network + console
|
|
27
|
+
REDUX: 9090, // Redux state + actions
|
|
28
|
+
STORAGE: 9091, // AsyncStorage snapshots
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─── Feature Flags (can be toggled by debugger app) ──────────────────────────
|
|
32
|
+
let _networkCaptureEnabled = true;
|
|
33
|
+
let _stackTraceEnabled = false; // Disabled by default for performance
|
|
34
|
+
let _throttleProfile = 'none'; // 'none', 'fast3g', 'slow3g', 'offline'
|
|
35
|
+
const THROTTLE_DELAYS = { none: 0, fast3g: 500, slow3g: 2000, offline: -1 };
|
|
36
|
+
|
|
37
|
+
// ─── WebSocket Factory ────────────────────────────────────────────────────────
|
|
38
|
+
function makeChannel(port, name, onMessage) {
|
|
39
|
+
let ws = null, queue = [], connected = false;
|
|
40
|
+
|
|
41
|
+
function connect() {
|
|
42
|
+
try {
|
|
43
|
+
ws = new WebSocket(`ws://${HOST}:${port}`);
|
|
44
|
+
ws.onopen = () => {
|
|
45
|
+
connected = true;
|
|
46
|
+
queue.forEach(m => ws.send(m));
|
|
47
|
+
queue = [];
|
|
48
|
+
};
|
|
49
|
+
ws.onmessage = (evt) => {
|
|
50
|
+
if (onMessage) {
|
|
51
|
+
try { onMessage(JSON.parse(evt.data)); } catch {}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
ws.onclose = () => { connected = false; setTimeout(connect, 2000); };
|
|
55
|
+
ws.onerror = () => {};
|
|
56
|
+
} catch { setTimeout(connect, 2000); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function send(obj) {
|
|
60
|
+
const msg = JSON.stringify({ ...obj, ts: Date.now() });
|
|
61
|
+
if (connected && ws?.readyState === WebSocket.OPEN) ws.send(msg);
|
|
62
|
+
else { queue.push(msg); if (queue.length > 300) queue.shift(); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
connect();
|
|
66
|
+
return { send };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The main channel (console + network) listens for control messages from the debugger
|
|
70
|
+
const mainCh = makeChannel(PORTS.NETWORK_AND_CONSOLE, 'main', (msg) => {
|
|
71
|
+
if (msg.type === 'control') {
|
|
72
|
+
if (msg.action === 'set-network-capture') _networkCaptureEnabled = !!msg.enabled;
|
|
73
|
+
if (msg.action === 'set-throttle') _throttleProfile = msg.profile || 'none';
|
|
74
|
+
if (msg.action === 'set-stack-trace') _stackTraceEnabled = !!msg.enabled;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const reduxCh = makeChannel(PORTS.REDUX, 'redux');
|
|
78
|
+
const storageCh = makeChannel(PORTS.STORAGE, 'storage');
|
|
79
|
+
|
|
80
|
+
// ─── Console Intercept ────────────────────────────────────────────────────────
|
|
81
|
+
function serializeArg(a) {
|
|
82
|
+
if (a === null) return { t: 'null', v: null };
|
|
83
|
+
if (a === undefined) return { t: 'undefined', v: undefined };
|
|
84
|
+
if (typeof a === 'string') return { t: 'string', v: a };
|
|
85
|
+
if (typeof a === 'number') return { t: 'number', v: a };
|
|
86
|
+
if (typeof a === 'boolean') return { t: 'boolean', v: a };
|
|
87
|
+
if (typeof a === 'symbol') return { t: 'string', v: a.toString() };
|
|
88
|
+
if (typeof a === 'function') return { t: 'string', v: `[Function: ${a.name || 'anonymous'}]` };
|
|
89
|
+
if (a instanceof Error) return { t: 'object', v: { name: a.name, message: a.message, stack: a.stack } };
|
|
90
|
+
if (Array.isArray(a)) {
|
|
91
|
+
try { const j = JSON.parse(JSON.stringify(a)); return { t: 'array', v: j }; }
|
|
92
|
+
catch { return { t: 'string', v: String(a) }; }
|
|
93
|
+
}
|
|
94
|
+
if (typeof a === 'object') {
|
|
95
|
+
try { const j = JSON.parse(JSON.stringify(a)); return { t: 'object', v: j }; }
|
|
96
|
+
catch { return { t: 'string', v: String(a) }; }
|
|
97
|
+
}
|
|
98
|
+
return { t: 'string', v: String(a) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const LEVELS = ['log','info','warn','error','debug'];
|
|
102
|
+
const _console = {};
|
|
103
|
+
|
|
104
|
+
// Pre-compiled regexes for stack parsing (avoid creating per call)
|
|
105
|
+
const _skipRe = /RNDebugSDK|apply \(native\)|call \(native\)|anonymous \(native\)|MessageQueue|__callFunction|__guard|callFunctionReturn|processTicksAndRejections/;
|
|
106
|
+
const _frameRe = /at\s+(.+?)(?:\s+\((.+?):(\d+):\d+\)|(?:\s+)?(.+?):(\d+):\d+)/;
|
|
107
|
+
|
|
108
|
+
function _extractCaller() {
|
|
109
|
+
const stack = (new Error().stack || '').split('\n');
|
|
110
|
+
for (let i = 2; i < Math.min(stack.length, 15); i++) {
|
|
111
|
+
const frame = stack[i]?.trim() || '';
|
|
112
|
+
if (!frame || _skipRe.test(frame)) continue;
|
|
113
|
+
const m = frame.match(_frameRe);
|
|
114
|
+
if (!m) continue;
|
|
115
|
+
const fn = m[1] || '', src = m[2] || m[4] || '', ln = m[3] || m[5] || '';
|
|
116
|
+
// Skip console internals and single-char minified names from Hermes
|
|
117
|
+
if (/^console|^_console|^overrideMethod|^reactConsoleError|^anonymous$/.test(fn)) continue;
|
|
118
|
+
if (fn.length <= 2) continue; // Skip minified single/double-char names like "a", "b", "Oa"
|
|
119
|
+
// Real source file
|
|
120
|
+
if (src && !src.includes('index.bundle') && /\.[jt]sx?$/.test(src)) {
|
|
121
|
+
return `${src.split('/').pop()}:${ln}` + (fn.length > 2 ? ` (${fn})` : '');
|
|
122
|
+
}
|
|
123
|
+
// Named function from bundle — must be meaningful (3+ chars, starts with uppercase = component)
|
|
124
|
+
if (fn.length >= 3 && fn !== 'Object' && fn !== 'Function') return fn;
|
|
125
|
+
}
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
LEVELS.forEach(level => {
|
|
130
|
+
_console[level] = console[level].bind(console);
|
|
131
|
+
console[level] = (...args) => {
|
|
132
|
+
_console[level](...args);
|
|
133
|
+
const structuredArgs = args.map(serializeArg);
|
|
134
|
+
const message = args.map(a => {
|
|
135
|
+
if (typeof a === 'string') return a;
|
|
136
|
+
try { return JSON.stringify(a, null, 2); } catch { return String(a); }
|
|
137
|
+
}).join(' ');
|
|
138
|
+
// Stack trace capture controlled by toggle (disabled by default for performance)
|
|
139
|
+
// When enabled: captures for all levels. When disabled: skips entirely.
|
|
140
|
+
const caller = _stackTraceEnabled ? _extractCaller() : '';
|
|
141
|
+
mainCh.send({ type: 'console', level, message, args: structuredArgs, caller });
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ─── Header Flattener (ensures all values are strings) ───────────────────────
|
|
146
|
+
function _flattenHeaders(h) {
|
|
147
|
+
if (!h) return {};
|
|
148
|
+
const flat = {};
|
|
149
|
+
try {
|
|
150
|
+
// Handle Headers object (has forEach)
|
|
151
|
+
if (typeof h.forEach === 'function') {
|
|
152
|
+
h.forEach((v, k) => { flat[k] = String(v); });
|
|
153
|
+
return flat;
|
|
154
|
+
}
|
|
155
|
+
// Handle plain object — stringify nested objects
|
|
156
|
+
if (typeof h === 'object') {
|
|
157
|
+
Object.entries(h).forEach(([k, v]) => {
|
|
158
|
+
if (v == null) return;
|
|
159
|
+
flat[k] = (typeof v === 'object') ? JSON.stringify(v) : String(v);
|
|
160
|
+
});
|
|
161
|
+
return flat;
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
return flat;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Fetch Intercept ─────────────────────────────────────────────────────────
|
|
168
|
+
const _fetch = global.fetch;
|
|
169
|
+
global.fetch = async (input, init = {}) => {
|
|
170
|
+
// Throttle: simulate slow network or offline
|
|
171
|
+
const delay = THROTTLE_DELAYS[_throttleProfile] || 0;
|
|
172
|
+
if (delay === -1) return Promise.reject(new TypeError('Network request failed (offline throttle)'));
|
|
173
|
+
if (delay > 0) await new Promise(r => setTimeout(r, delay));
|
|
174
|
+
|
|
175
|
+
if (!_networkCaptureEnabled) return _fetch(input, init);
|
|
176
|
+
|
|
177
|
+
const url = typeof input === 'string' ? input : input?.url || '';
|
|
178
|
+
const method = (init.method || 'GET').toUpperCase();
|
|
179
|
+
const id = `f-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
|
180
|
+
|
|
181
|
+
mainCh.send({ type: 'network', phase: 'request', id, url, method,
|
|
182
|
+
requestHeaders: _flattenHeaders(init.headers), requestBody: init.body || null });
|
|
183
|
+
|
|
184
|
+
const t0 = Date.now();
|
|
185
|
+
try {
|
|
186
|
+
const resp = await _fetch(input, init);
|
|
187
|
+
const clone = resp.clone();
|
|
188
|
+
clone.text().then(body => {
|
|
189
|
+
if (!_networkCaptureEnabled) return;
|
|
190
|
+
let parsed = body;
|
|
191
|
+
try { parsed = JSON.parse(body); } catch {}
|
|
192
|
+
const rHeaders = {};
|
|
193
|
+
clone.headers?.forEach?.((v, k) => { rHeaders[k] = v; });
|
|
194
|
+
mainCh.send({ type: 'network', phase: 'response', id, url, method,
|
|
195
|
+
status: resp.status, statusText: resp.statusText,
|
|
196
|
+
duration: Date.now() - t0, responseHeaders: rHeaders, responseBody: parsed });
|
|
197
|
+
}).catch(() => {});
|
|
198
|
+
return resp;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
mainCh.send({ type: 'network', phase: 'error', id, url, method,
|
|
201
|
+
duration: Date.now() - t0, error: err?.message || String(err) });
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ─── Network Intercept via XHR readystatechange (RN 0.81 compatible) ─────────
|
|
207
|
+
// RN 0.81 + Reactotron both fight over XMLHttpRequest.prototype. Instead of
|
|
208
|
+
// patching prototype methods (which get overwritten), we use a non-invasive
|
|
209
|
+
// approach: wrap XMLHttpRequest constructor to add a readystatechange listener
|
|
210
|
+
// on every NEW instance. This works regardless of who patches the prototype.
|
|
211
|
+
(function setupXHRNetworkCapture() {
|
|
212
|
+
const _xhrTracker = new WeakMap();
|
|
213
|
+
|
|
214
|
+
function wrapXHR() {
|
|
215
|
+
const OrigXHR = global.XMLHttpRequest;
|
|
216
|
+
if (!OrigXHR || OrigXHR.__dbgWrapped) return;
|
|
217
|
+
|
|
218
|
+
function WrappedXHR() {
|
|
219
|
+
const xhr = new OrigXHR();
|
|
220
|
+
const meta = { id: `x-${Date.now()}-${Math.random().toString(36).slice(2,6)}`, method: 'GET', url: '', t0: 0, headers: {}, sent: false };
|
|
221
|
+
_xhrTracker.set(xhr, meta);
|
|
222
|
+
|
|
223
|
+
// Wrap open
|
|
224
|
+
const _open = xhr.open.bind(xhr);
|
|
225
|
+
xhr.open = function(method, url) {
|
|
226
|
+
meta.method = (method || 'GET').toUpperCase();
|
|
227
|
+
meta.url = String(url);
|
|
228
|
+
meta.t0 = Date.now();
|
|
229
|
+
return _open.apply(xhr, arguments);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Wrap setRequestHeader
|
|
233
|
+
const _setHeader = xhr.setRequestHeader.bind(xhr);
|
|
234
|
+
xhr.setRequestHeader = function(key, value) {
|
|
235
|
+
meta.headers[key] = value;
|
|
236
|
+
return _setHeader.apply(xhr, arguments);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Wrap send
|
|
240
|
+
const _send = xhr.send.bind(xhr);
|
|
241
|
+
xhr.send = function(body) {
|
|
242
|
+
if (_networkCaptureEnabled && !meta.sent) {
|
|
243
|
+
meta.sent = true;
|
|
244
|
+
let reqBody = null;
|
|
245
|
+
if (body != null) {
|
|
246
|
+
try { reqBody = typeof body === 'string' ? body : JSON.parse(JSON.stringify(body)); } catch { reqBody = String(body); }
|
|
247
|
+
}
|
|
248
|
+
mainCh.send({ type: 'network', phase: 'request', id: meta.id, url: meta.url,
|
|
249
|
+
method: meta.method, requestHeaders: meta.headers, requestBody: reqBody });
|
|
250
|
+
}
|
|
251
|
+
return _send.apply(xhr, arguments);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Listen for completion
|
|
255
|
+
xhr.addEventListener('readystatechange', function() {
|
|
256
|
+
if (xhr.readyState !== 4 || !meta.sent || !_networkCaptureEnabled) return;
|
|
257
|
+
try {
|
|
258
|
+
const duration = Date.now() - meta.t0;
|
|
259
|
+
if (xhr.status > 0) {
|
|
260
|
+
// Safely read response body — responseText throws if responseType is blob/arraybuffer
|
|
261
|
+
let respBody = null;
|
|
262
|
+
const rType = xhr.responseType || '';
|
|
263
|
+
if (rType === '' || rType === 'text') {
|
|
264
|
+
try { respBody = xhr.responseText || ''; } catch { respBody = ''; }
|
|
265
|
+
try { respBody = JSON.parse(respBody); } catch {}
|
|
266
|
+
} else if (rType === 'json') {
|
|
267
|
+
respBody = xhr.response;
|
|
268
|
+
} else {
|
|
269
|
+
// blob, arraybuffer, document — can't serialize, show type info
|
|
270
|
+
respBody = `[${rType} response — ${xhr.response?.size || xhr.response?.byteLength || '?'} bytes]`;
|
|
271
|
+
}
|
|
272
|
+
const respHeaders = {};
|
|
273
|
+
try {
|
|
274
|
+
const raw = xhr.getAllResponseHeaders() || '';
|
|
275
|
+
raw.split('\r\n').forEach(line => {
|
|
276
|
+
const idx = line.indexOf(':');
|
|
277
|
+
if (idx > 0) respHeaders[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
278
|
+
});
|
|
279
|
+
} catch {}
|
|
280
|
+
mainCh.send({ type: 'network', phase: 'response', id: meta.id, url: meta.url,
|
|
281
|
+
method: meta.method, status: xhr.status, statusText: xhr.statusText,
|
|
282
|
+
duration, responseHeaders: respHeaders, responseBody: respBody });
|
|
283
|
+
} else {
|
|
284
|
+
mainCh.send({ type: 'network', phase: 'error', id: meta.id, url: meta.url,
|
|
285
|
+
method: meta.method, duration: Date.now() - meta.t0, error: 'Request failed (status 0)' });
|
|
286
|
+
}
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Safety net — never let our interceptor crash the app
|
|
289
|
+
mainCh.send({ type: 'network', phase: 'response', id: meta.id, url: meta.url,
|
|
290
|
+
method: meta.method, status: xhr.status || 0, duration: Date.now() - meta.t0,
|
|
291
|
+
responseBody: `[Error reading response: ${e.message}]` });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return xhr;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Copy static properties and prototype
|
|
299
|
+
WrappedXHR.prototype = OrigXHR.prototype;
|
|
300
|
+
WrappedXHR.UNSENT = 0;
|
|
301
|
+
WrappedXHR.OPENED = 1;
|
|
302
|
+
WrappedXHR.HEADERS_RECEIVED = 2;
|
|
303
|
+
WrappedXHR.LOADING = 3;
|
|
304
|
+
WrappedXHR.DONE = 4;
|
|
305
|
+
WrappedXHR.__dbgWrapped = true;
|
|
306
|
+
// Keep reference to original for Reactotron etc
|
|
307
|
+
WrappedXHR.__original = OrigXHR;
|
|
308
|
+
|
|
309
|
+
global.XMLHttpRequest = WrappedXHR;
|
|
310
|
+
_console.log('[RNDebugSDK] XHR constructor wrapped for network capture');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Wrap immediately if available
|
|
314
|
+
if (global.XMLHttpRequest) wrapXHR();
|
|
315
|
+
|
|
316
|
+
// Also wrap after RN polyfills set up (they replace global.XMLHttpRequest)
|
|
317
|
+
[0, 50, 200, 500].forEach(delay => {
|
|
318
|
+
setTimeout(() => {
|
|
319
|
+
if (global.XMLHttpRequest && !global.XMLHttpRequest.__dbgWrapped) {
|
|
320
|
+
wrapXHR();
|
|
321
|
+
}
|
|
322
|
+
}, delay);
|
|
323
|
+
});
|
|
324
|
+
})();
|
|
325
|
+
|
|
326
|
+
// ─── Axios Interceptor (belt-and-suspenders with XHR patch) ──────────────────
|
|
327
|
+
// Patches axios.create after a tick so import hoisting has resolved.
|
|
328
|
+
setTimeout(() => {
|
|
329
|
+
try {
|
|
330
|
+
const axios = require('axios');
|
|
331
|
+
if (!axios || axios.__dbgPatched) return;
|
|
332
|
+
axios.__dbgPatched = true;
|
|
333
|
+
|
|
334
|
+
function addDbgInterceptors(instance) {
|
|
335
|
+
if (!instance || !instance.interceptors || instance.__dbgInt) return;
|
|
336
|
+
instance.__dbgInt = true;
|
|
337
|
+
instance.interceptors.request.use(config => {
|
|
338
|
+
if (!_networkCaptureEnabled) return config;
|
|
339
|
+
const id = `ax-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
|
340
|
+
config._dbgId = id;
|
|
341
|
+
config._dbgT0 = Date.now();
|
|
342
|
+
const url = config.baseURL
|
|
343
|
+
? config.baseURL.replace(/\/+$/, '') + '/' + (config.url || '').replace(/^\/+/, '')
|
|
344
|
+
: (config.url || '');
|
|
345
|
+
const h = _flattenHeaders(typeof config.headers?.toJSON === 'function' ? config.headers.toJSON() : config.headers);
|
|
346
|
+
let body = null;
|
|
347
|
+
if (config.data != null) { try { body = typeof config.data === 'string' ? config.data : JSON.parse(JSON.stringify(config.data)); } catch { body = String(config.data); } }
|
|
348
|
+
mainCh.send({ type:'network', phase:'request', id, url, method:(config.method||'GET').toUpperCase(), requestHeaders:h, requestBody:body });
|
|
349
|
+
return config;
|
|
350
|
+
}, e => Promise.reject(e));
|
|
351
|
+
instance.interceptors.response.use(resp => {
|
|
352
|
+
const c = resp.config || {};
|
|
353
|
+
if (!c._dbgId) return resp;
|
|
354
|
+
const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
|
|
355
|
+
const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
|
|
356
|
+
const rh = {};
|
|
357
|
+
try { const h = typeof resp.headers?.toJSON === 'function' ? resp.headers.toJSON() : resp.headers;
|
|
358
|
+
if (h) Object.entries(h).forEach(([k,v]) => { if (typeof v === 'string') rh[k] = v; }); } catch {}
|
|
359
|
+
let body = resp.data;
|
|
360
|
+
if (body && typeof body === 'object') { try { body = JSON.parse(JSON.stringify(body)); } catch {} }
|
|
361
|
+
mainCh.send({ type:'network', phase:'response', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(),
|
|
362
|
+
status:resp.status, statusText:resp.statusText, duration:dur, responseHeaders:rh, responseBody:body });
|
|
363
|
+
return resp;
|
|
364
|
+
}, err => {
|
|
365
|
+
const c = err?.config || {};
|
|
366
|
+
if (c._dbgId) {
|
|
367
|
+
const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
|
|
368
|
+
const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
|
|
369
|
+
const r = err?.response;
|
|
370
|
+
if (r) { let b = r.data; if (b && typeof b === 'object') { try { b = JSON.parse(JSON.stringify(b)); } catch {} }
|
|
371
|
+
mainCh.send({ type:'network', phase:'response', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(), status:r.status, statusText:r.statusText, duration:dur, responseBody:b });
|
|
372
|
+
} else { mainCh.send({ type:'network', phase:'error', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(), duration:dur, error:err?.message||String(err) }); }
|
|
373
|
+
}
|
|
374
|
+
return Promise.reject(err);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
addDbgInterceptors(axios);
|
|
379
|
+
const _create = axios.create.bind(axios);
|
|
380
|
+
axios.create = function(...args) {
|
|
381
|
+
const inst = _create(...args);
|
|
382
|
+
addDbgInterceptors(inst);
|
|
383
|
+
return inst;
|
|
384
|
+
};
|
|
385
|
+
_console.log('[RNDebugSDK] Axios interceptor active (global + create)');
|
|
386
|
+
} catch {}
|
|
387
|
+
}, 0);
|
|
388
|
+
|
|
389
|
+
// ─── Redux Enhancer ──────────────────────────────────────────────────────────
|
|
390
|
+
function reduxEnhancer(createStore) {
|
|
391
|
+
return (reducer, preloadedState, enhancer) => {
|
|
392
|
+
const store = createStore(reducer, preloadedState, enhancer);
|
|
393
|
+
let actionCount = 0;
|
|
394
|
+
|
|
395
|
+
// Send initial state
|
|
396
|
+
reduxCh.send({ type: 'redux', action: { type: '@@INIT' }, nextState: store.getState(), index: actionCount++ });
|
|
397
|
+
|
|
398
|
+
const origDispatch = store.dispatch;
|
|
399
|
+
store.dispatch = (action) => {
|
|
400
|
+
const result = origDispatch(action);
|
|
401
|
+
const nextState = store.getState();
|
|
402
|
+
reduxCh.send({ type: 'redux', action, nextState, index: actionCount++ });
|
|
403
|
+
return result;
|
|
404
|
+
};
|
|
405
|
+
return store;
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ─── Redux Toolkit middleware (alternative) ───────────────────────────────────
|
|
410
|
+
// If you use RTK configureStore, add this to middleware array instead:
|
|
411
|
+
const reduxMiddleware = store => next => action => {
|
|
412
|
+
const result = next(action);
|
|
413
|
+
reduxCh.send({ type: 'redux', action, nextState: store.getState() });
|
|
414
|
+
return result;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// ─── AsyncStorage Monitor ─────────────────────────────────────────────────────
|
|
418
|
+
let _asyncStoragePatched = false;
|
|
419
|
+
function watchAsyncStorage() {
|
|
420
|
+
if (_asyncStoragePatched) return; // Only patch once
|
|
421
|
+
_asyncStoragePatched = true;
|
|
422
|
+
try {
|
|
423
|
+
const RNAsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
424
|
+
if (!RNAsyncStorage) return;
|
|
425
|
+
|
|
426
|
+
// Send full snapshot once on first connect
|
|
427
|
+
RNAsyncStorage.getAllKeys().then(keys => {
|
|
428
|
+
if (!keys?.length) return;
|
|
429
|
+
RNAsyncStorage.multiGet(keys).then(pairs => {
|
|
430
|
+
const snapshot = Object.fromEntries(pairs);
|
|
431
|
+
storageCh.send({ type: 'storage', action: 'snapshot', key: snapshot });
|
|
432
|
+
}).catch(() => {});
|
|
433
|
+
}).catch(() => {});
|
|
434
|
+
|
|
435
|
+
// Patch individual methods
|
|
436
|
+
const _setItem = RNAsyncStorage.setItem.bind(RNAsyncStorage);
|
|
437
|
+
RNAsyncStorage.setItem = async (key, value, ...rest) => {
|
|
438
|
+
const result = await _setItem(key, value, ...rest);
|
|
439
|
+
storageCh.send({ type: 'storage', action: 'set', key, value });
|
|
440
|
+
return result;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const _removeItem = RNAsyncStorage.removeItem.bind(RNAsyncStorage);
|
|
444
|
+
RNAsyncStorage.removeItem = async (key, ...rest) => {
|
|
445
|
+
const result = await _removeItem(key, ...rest);
|
|
446
|
+
storageCh.send({ type: 'storage', action: 'remove', key });
|
|
447
|
+
return result;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const _mergeItem = RNAsyncStorage.mergeItem.bind(RNAsyncStorage);
|
|
451
|
+
RNAsyncStorage.mergeItem = async (key, value, ...rest) => {
|
|
452
|
+
const result = await _mergeItem(key, value, ...rest);
|
|
453
|
+
// Read back merged value
|
|
454
|
+
RNAsyncStorage.getItem(key).then(v => storageCh.send({ type: 'storage', action: 'set', key, value: v }));
|
|
455
|
+
return result;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const _clear = RNAsyncStorage.clear.bind(RNAsyncStorage);
|
|
459
|
+
RNAsyncStorage.clear = async (...rest) => {
|
|
460
|
+
const result = await _clear(...rest);
|
|
461
|
+
storageCh.send({ type: 'storage', action: 'snapshot', key: {} });
|
|
462
|
+
return result;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
console.log('[RNDebugSDK] AsyncStorage monitoring active');
|
|
466
|
+
} catch (e) {
|
|
467
|
+
console.warn('[RNDebugSDK] AsyncStorage not available:', e.message);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── Fix: Guard against "Debug JS Remotely" crash on Hermes/New Arch ─────────
|
|
472
|
+
// RN 0.74+ with Hermes removed DevSettings.setIsDebuggingRemotely.
|
|
473
|
+
// Some packages (react-native-devsettings, etc.) still call it and crash.
|
|
474
|
+
// We patch it as a no-op to prevent the crash.
|
|
475
|
+
try {
|
|
476
|
+
const { NativeModules } = require('react-native');
|
|
477
|
+
const DevSettings = NativeModules?.DevSettings;
|
|
478
|
+
if (DevSettings && typeof DevSettings.setIsDebuggingRemotely !== 'function') {
|
|
479
|
+
DevSettings.setIsDebuggingRemotely = () => {
|
|
480
|
+
_console.warn('[RNDebugSDK] "Debug JS Remotely" is not available on Hermes. Use "Open DevTools" instead — it will open in the ReactoRadar app.');
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
} catch {}
|
|
484
|
+
|
|
485
|
+
// ─── Performance + Memory Metrics ────────────────────────────────────────────
|
|
486
|
+
// Sends FPS, JS thread time, and memory stats every 2 seconds
|
|
487
|
+
(function startPerfMetrics() {
|
|
488
|
+
let frameCount = 0;
|
|
489
|
+
let lastTime = Date.now();
|
|
490
|
+
|
|
491
|
+
// FPS counter using requestAnimationFrame
|
|
492
|
+
function countFrame() {
|
|
493
|
+
frameCount++;
|
|
494
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
495
|
+
requestAnimationFrame(countFrame);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
499
|
+
requestAnimationFrame(countFrame);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
setInterval(() => {
|
|
503
|
+
const now = Date.now();
|
|
504
|
+
const elapsed = (now - lastTime) / 1000;
|
|
505
|
+
const fps = elapsed > 0 ? Math.round(frameCount / elapsed) : 0;
|
|
506
|
+
frameCount = 0;
|
|
507
|
+
lastTime = now;
|
|
508
|
+
|
|
509
|
+
const perfData = { type: 'perf', fps };
|
|
510
|
+
|
|
511
|
+
// Hermes memory stats
|
|
512
|
+
try {
|
|
513
|
+
if (global.HermesInternal && typeof global.HermesInternal.getRuntimeProperties === 'function') {
|
|
514
|
+
const props = global.HermesInternal.getRuntimeProperties();
|
|
515
|
+
perfData.heapUsed = props['js_heapSize'] || 0;
|
|
516
|
+
perfData.heapTotal = props['js_totalHeapSize'] || 0;
|
|
517
|
+
perfData.native = props['js_nativeHeapSize'] || 0;
|
|
518
|
+
}
|
|
519
|
+
} catch {}
|
|
520
|
+
|
|
521
|
+
// Try Performance API for thread timing
|
|
522
|
+
try {
|
|
523
|
+
if (global.performance && typeof global.performance.now === 'function') {
|
|
524
|
+
perfData.jsThread = global.performance.now() % 16.67; // approximate frame time
|
|
525
|
+
}
|
|
526
|
+
} catch {}
|
|
527
|
+
|
|
528
|
+
mainCh.send(perfData);
|
|
529
|
+
}, 2000);
|
|
530
|
+
})();
|
|
531
|
+
|
|
532
|
+
// Note: "Open DevTools" in the simulator dev menu opens Chrome/Metro's built-in debugger.
|
|
533
|
+
// To debug JS in the ReactoRadar app instead, press Cmd+D in the debugger app
|
|
534
|
+
// or click the "JS Debugger" button. Both can coexist — they connect to the
|
|
535
|
+
// same Hermes CDP target independently.
|
|
536
|
+
|
|
537
|
+
console.log(`[RNDebugSDK] Connected to ${HOST} | Console+Network:${PORTS.NETWORK_AND_CONSOLE} Redux:${PORTS.REDUX} Storage:${PORTS.STORAGE}`);
|
|
538
|
+
|
|
539
|
+
module.exports = { reduxEnhancer, reduxMiddleware, watchAsyncStorage };
|
|
540
|
+
}
|