reactoradar 1.5.2 → 1.5.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/app.js +43 -15
- package/main.js +11 -2
- package/package.json +1 -1
- package/preload.js +1 -0
- package/sdk/RNDebugSDK.js +93 -35
package/app.js
CHANGED
|
@@ -205,15 +205,19 @@ if (window.electronAPI) {
|
|
|
205
205
|
window.electronAPI.on('ports', ports => { state.ports = ports; });
|
|
206
206
|
|
|
207
207
|
window.electronAPI.on('cdp-targets', targets => {
|
|
208
|
-
|
|
209
|
-
$('btnCDP')
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
208
|
+
state.cdpTargets = targets;
|
|
209
|
+
const btn = $('btnCDP');
|
|
210
|
+
if (btn) {
|
|
211
|
+
const hasCDP = targets?.length > 0;
|
|
212
|
+
const port = getStoredMetroPort();
|
|
213
|
+
btn.textContent = hasCDP
|
|
214
|
+
? `JS Debugger (:${port}) [${targets.length}] ↗`
|
|
215
|
+
: `JS Debugger (:${port}) ↗`;
|
|
216
|
+
btn.style.opacity = hasCDP ? '1' : '0.5';
|
|
217
|
+
if (hasCDP) {
|
|
218
|
+
btn.onclick = () => window.electronAPI.openCDPTarget(targets[0].webSocketDebuggerUrl);
|
|
219
|
+
}
|
|
215
220
|
}
|
|
216
|
-
|
|
217
221
|
});
|
|
218
222
|
|
|
219
223
|
window.electronAPI.on('redux-event', handleReduxEvent);
|
|
@@ -2149,9 +2153,9 @@ function initReactPanel() {
|
|
|
2149
2153
|
<div class="react-connect-hint" id="reactHint">
|
|
2150
2154
|
<div class="icon" style="font-size:40px;opacity:.2">⚛️</div>
|
|
2151
2155
|
<div class="label">React DevTools</div>
|
|
2152
|
-
<div class="hint">
|
|
2153
|
-
<div class="hint">React
|
|
2154
|
-
<button class="btn-launch" id="btnReactDT">Open React DevTools ↗</button>
|
|
2156
|
+
<div class="hint">Opens as a separate window connected to your app via port 8097</div>
|
|
2157
|
+
<div class="hint" style="margin-top:8px;color:var(--yellow)">Note: The RN inspector overlay won't work while React DevTools is connected. Close the DevTools window to use the built-in inspector.</div>
|
|
2158
|
+
<button class="btn-launch" id="btnReactDT" style="margin-top:12px">Open React DevTools ↗</button>
|
|
2155
2159
|
</div>
|
|
2156
2160
|
</div>`;
|
|
2157
2161
|
|
|
@@ -2182,6 +2186,12 @@ function getStoredAppName() {
|
|
|
2182
2186
|
function setStoredAppName(n) {
|
|
2183
2187
|
try { localStorage.setItem('rn-debug-appname', n); } catch {}
|
|
2184
2188
|
}
|
|
2189
|
+
function getStoredMetroPort() {
|
|
2190
|
+
try { return parseInt(localStorage.getItem('rn-debug-metro-port')) || 8081; } catch { return 8081; }
|
|
2191
|
+
}
|
|
2192
|
+
function setStoredMetroPort(p) {
|
|
2193
|
+
try { localStorage.setItem('rn-debug-metro-port', String(p)); } catch {}
|
|
2194
|
+
}
|
|
2185
2195
|
function applyAppName(name) {
|
|
2186
2196
|
const logo = document.querySelector('.logo');
|
|
2187
2197
|
if (logo) {
|
|
@@ -2280,10 +2290,11 @@ function initSettingsPanel() {
|
|
|
2280
2290
|
</div>
|
|
2281
2291
|
</div>
|
|
2282
2292
|
<div class="settings-row">
|
|
2283
|
-
<div>
|
|
2284
|
-
<div class="settings-label">Metro Bundler</div>
|
|
2285
|
-
<div class="settings-hint">CDP target discovery
|
|
2293
|
+
<div style="display:flex;flex-direction:column;gap:2px">
|
|
2294
|
+
<div class="settings-label">Metro Bundler Port</div>
|
|
2295
|
+
<div class="settings-hint">Port for CDP target discovery (default: 8081)</div>
|
|
2286
2296
|
</div>
|
|
2297
|
+
<input id="metroPortInput" type="number" class="net-search-input" style="width:70px;text-align:center" value="${getStoredMetroPort()}" />
|
|
2287
2298
|
</div>
|
|
2288
2299
|
</div>
|
|
2289
2300
|
<div class="settings-section">
|
|
@@ -2334,6 +2345,7 @@ function initSettingsPanel() {
|
|
|
2334
2345
|
<div class="about-links" style="display:flex;gap:16px;justify-content:center">
|
|
2335
2346
|
<span class="about-link" id="linkGithub">GitHub</span>
|
|
2336
2347
|
<span class="about-link" id="linkDocs">Documentation</span>
|
|
2348
|
+
<span class="about-link" id="linkLinkedIn">Developer LinkedIn</span>
|
|
2337
2349
|
</div>
|
|
2338
2350
|
</div>
|
|
2339
2351
|
</div>
|
|
@@ -2383,6 +2395,9 @@ function initSettingsPanel() {
|
|
|
2383
2395
|
$('linkDocs')?.addEventListener('click', () => {
|
|
2384
2396
|
window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger#readme');
|
|
2385
2397
|
});
|
|
2398
|
+
$('linkLinkedIn')?.addEventListener('click', () => {
|
|
2399
|
+
window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
|
|
2400
|
+
});
|
|
2386
2401
|
|
|
2387
2402
|
// App name
|
|
2388
2403
|
$('appNameInput').addEventListener('change', (e) => {
|
|
@@ -2396,6 +2411,15 @@ function initSettingsPanel() {
|
|
|
2396
2411
|
applyAppName('ReactoRadar');
|
|
2397
2412
|
});
|
|
2398
2413
|
|
|
2414
|
+
// Metro Port
|
|
2415
|
+
$('metroPortInput')?.addEventListener('change', (e) => {
|
|
2416
|
+
let port = parseInt(e.target.value.trim());
|
|
2417
|
+
if (isNaN(port) || port < 1024 || port > 65535) port = 8081;
|
|
2418
|
+
e.target.value = port;
|
|
2419
|
+
setStoredMetroPort(port);
|
|
2420
|
+
window.electronAPI?.setMetroPort(port);
|
|
2421
|
+
});
|
|
2422
|
+
|
|
2399
2423
|
// Font size controls
|
|
2400
2424
|
$('fontSizeDown').addEventListener('click', () => {
|
|
2401
2425
|
let size = getStoredFontSize();
|
|
@@ -2416,6 +2440,9 @@ applyTheme(getStoredTheme());
|
|
|
2416
2440
|
applyFontSize(getStoredFontSize());
|
|
2417
2441
|
applyAppName(getStoredAppName());
|
|
2418
2442
|
|
|
2443
|
+
// Send stored metro port to backend
|
|
2444
|
+
window.electronAPI?.setMetroPort(getStoredMetroPort());
|
|
2445
|
+
|
|
2419
2446
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2420
2447
|
// SOURCES PANEL — CDP-based file browser + breakpoints
|
|
2421
2448
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2667,7 +2694,8 @@ async function loadSourceFile(filepath) {
|
|
|
2667
2694
|
// Strategy 2: Fetch from Metro
|
|
2668
2695
|
if (!source) {
|
|
2669
2696
|
try {
|
|
2670
|
-
const
|
|
2697
|
+
const port = getStoredMetroPort();
|
|
2698
|
+
const resp = await fetch(`http://localhost:${port}/${filepath}?platform=ios&dev=true`);
|
|
2671
2699
|
if (resp.ok) source = await resp.text();
|
|
2672
2700
|
} catch {}
|
|
2673
2701
|
}
|
package/main.js
CHANGED
|
@@ -55,7 +55,8 @@ app.whenReady().then(async () => {
|
|
|
55
55
|
// Check for updates (non-blocking)
|
|
56
56
|
checkForUpdates();
|
|
57
57
|
startBridgeServers();
|
|
58
|
-
|
|
58
|
+
// React DevTools relay NOT started by default — it blocks RN's built-in inspector.
|
|
59
|
+
// Started on-demand when user clicks React tab or Cmd+R.
|
|
59
60
|
setupMetroCDPProxy();
|
|
60
61
|
setupIPC();
|
|
61
62
|
buildMenu();
|
|
@@ -317,6 +318,8 @@ function setupIPC() {
|
|
|
317
318
|
});
|
|
318
319
|
|
|
319
320
|
ipcMain.on('open-react-devtools', () => {
|
|
321
|
+
// Start the relay server if not already running
|
|
322
|
+
if (!reactDTServer) startReactDevToolsServer();
|
|
320
323
|
// Open standalone react-devtools window
|
|
321
324
|
const rdtWin = new BrowserWindow({
|
|
322
325
|
width: 1100,
|
|
@@ -332,6 +335,12 @@ function setupIPC() {
|
|
|
332
335
|
|
|
333
336
|
// clear-all is handled by renderer via clear-all-ui IPC from menu
|
|
334
337
|
|
|
338
|
+
ipcMain.on('set-metro-port', (_, port) => {
|
|
339
|
+
PORTS.METRO = port;
|
|
340
|
+
fetchCDPTargets();
|
|
341
|
+
mainWindow?.webContents.send('ports', PORTS);
|
|
342
|
+
});
|
|
343
|
+
|
|
335
344
|
ipcMain.on('set-network-capture', (_, enabled) => {
|
|
336
345
|
// Broadcast to connected RN apps so they can stop/start intercepting
|
|
337
346
|
networkClients.forEach(ws => {
|
|
@@ -391,7 +400,7 @@ function setupIPC() {
|
|
|
391
400
|
// Also try to detect from Metro's /json endpoint
|
|
392
401
|
try {
|
|
393
402
|
const result = require('child_process').execSync(
|
|
394
|
-
|
|
403
|
+
`lsof -i :${PORTS.METRO} -t 2>/dev/null | head -1 | xargs -I{} lsof -p {} -Fn 2>/dev/null | grep '^n/' | grep 'node_modules' | head -1 | sed 's|^n||;s|/node_modules.*||'`,
|
|
395
404
|
{ encoding: 'utf8', timeout: 3000 }
|
|
396
405
|
).trim();
|
|
397
406
|
if (result && fs.existsSync(result)) candidates.unshift(result);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactoradar",
|
|
3
3
|
"productName": "ReactoRadar",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.3",
|
|
5
5
|
"description": "macOS debugger for React Native — Console, Sources, Network, Performance, Memory, Redux, AsyncStorage, React tree. Supports RN 0.74+ with Hermes and New Architecture.",
|
|
6
6
|
"main": "main.js",
|
|
7
7
|
"bin": {
|
package/preload.js
CHANGED
|
@@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|
|
26
26
|
setNetworkCapture: (enabled) => ipcRenderer.send('set-network-capture', enabled),
|
|
27
27
|
setStackTraceCapture: (enabled) => ipcRenderer.send('set-stack-trace-capture', enabled),
|
|
28
28
|
setNetworkThrottle: (profile) => ipcRenderer.send('set-network-throttle', profile),
|
|
29
|
+
setMetroPort: (port) => ipcRenderer.send('set-metro-port', port),
|
|
29
30
|
readSourceFile: (filepath) => ipcRenderer.invoke('read-source-file', filepath),
|
|
30
31
|
openExternal: (url) => ipcRenderer.send('open-external', url),
|
|
31
32
|
});
|
package/sdk/RNDebugSDK.js
CHANGED
|
@@ -34,6 +34,49 @@ let _stackTraceEnabled = false; // Disabled by default for performance
|
|
|
34
34
|
let _throttleProfile = 'none'; // 'none', 'fast3g', 'slow3g', 'offline'
|
|
35
35
|
const THROTTLE_DELAYS = { none: 0, fast3g: 500, slow3g: 2000, offline: -1 };
|
|
36
36
|
|
|
37
|
+
// ─── SDK Pause/Resume (allows inspector to work without SDK interference) ────
|
|
38
|
+
// When paused, console/fetch/XHR interception is disabled so the RN inspector
|
|
39
|
+
// and CDP debugger can work without conflicts. Controlled via the debugger app.
|
|
40
|
+
let _sdkPaused = false;
|
|
41
|
+
|
|
42
|
+
function _isSDKActive() {
|
|
43
|
+
return !_sdkPaused;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Debugger Detection ──────────────────────────────────────────────────────
|
|
47
|
+
// Detect if a CDP debugger (Chrome DevTools / Hermes inspector) is attached.
|
|
48
|
+
// When detected, we back off our patches to avoid conflicts with the inspector.
|
|
49
|
+
let _debuggerDetected = false;
|
|
50
|
+
let _debuggerCheckInterval = null;
|
|
51
|
+
|
|
52
|
+
function _checkDebuggerAttached() {
|
|
53
|
+
// Method 1: Check if Hermes debugger globals are set
|
|
54
|
+
const hermesDebugger = !!(global.__DEBUGGER_CONNECTED__ || global.__HERMES_DEBUGGER_CONNECTED__);
|
|
55
|
+
// Method 2: Check React DevTools hook for debugger attachment
|
|
56
|
+
const rdtHook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
57
|
+
const rdtDebugger = !!(rdtHook && rdtHook._debuggerAttached);
|
|
58
|
+
// Method 3: Check if CDP is connected via the inspector agent
|
|
59
|
+
const inspectorConnected = !!(global.__inspectorGlobalObject || global.__inspector);
|
|
60
|
+
|
|
61
|
+
const wasDetected = _debuggerDetected;
|
|
62
|
+
_debuggerDetected = hermesDebugger || rdtDebugger || inspectorConnected;
|
|
63
|
+
|
|
64
|
+
if (_debuggerDetected && !wasDetected) {
|
|
65
|
+
_console.log('[RNDebugSDK] Debugger detected — SDK interception paused to avoid inspector conflicts. Use the ReactoRadar app to resume.');
|
|
66
|
+
} else if (!_debuggerDetected && wasDetected) {
|
|
67
|
+
_console.log('[RNDebugSDK] Debugger disconnected — SDK interception resumed.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check periodically (every 3s) — lightweight, no performance impact
|
|
72
|
+
_debuggerCheckInterval = setInterval(_checkDebuggerAttached, 3000);
|
|
73
|
+
// Also check once immediately after a short delay (debugger may attach during startup)
|
|
74
|
+
setTimeout(_checkDebuggerAttached, 1000);
|
|
75
|
+
|
|
76
|
+
function _shouldIntercept() {
|
|
77
|
+
return _isSDKActive() && !_debuggerDetected;
|
|
78
|
+
}
|
|
79
|
+
|
|
37
80
|
// ─── WebSocket Factory ────────────────────────────────────────────────────────
|
|
38
81
|
function makeChannel(port, name, onMessage) {
|
|
39
82
|
let ws = null, queue = [], connected = false;
|
|
@@ -72,6 +115,21 @@ const mainCh = makeChannel(PORTS.NETWORK_AND_CONSOLE, 'main', (msg) => {
|
|
|
72
115
|
if (msg.action === 'set-network-capture') _networkCaptureEnabled = !!msg.enabled;
|
|
73
116
|
if (msg.action === 'set-throttle') _throttleProfile = msg.profile || 'none';
|
|
74
117
|
if (msg.action === 'set-stack-trace') _stackTraceEnabled = !!msg.enabled;
|
|
118
|
+
// Pause/Resume SDK interception (allows inspector to work)
|
|
119
|
+
if (msg.action === 'pause-sdk') {
|
|
120
|
+
_sdkPaused = true;
|
|
121
|
+
_console.log('[RNDebugSDK] SDK paused — inspector/debugger can now inspect the app freely.');
|
|
122
|
+
mainCh.send({ type: 'control', action: 'sdk-status', paused: true });
|
|
123
|
+
}
|
|
124
|
+
if (msg.action === 'resume-sdk') {
|
|
125
|
+
_sdkPaused = false;
|
|
126
|
+
_console.log('[RNDebugSDK] SDK resumed — interception re-enabled.');
|
|
127
|
+
mainCh.send({ type: 'control', action: 'sdk-status', paused: false });
|
|
128
|
+
}
|
|
129
|
+
// Query current status
|
|
130
|
+
if (msg.action === 'query-sdk-status') {
|
|
131
|
+
mainCh.send({ type: 'control', action: 'sdk-status', paused: _sdkPaused, debuggerDetected: _debuggerDetected });
|
|
132
|
+
}
|
|
75
133
|
}
|
|
76
134
|
});
|
|
77
135
|
const reduxCh = makeChannel(PORTS.REDUX, 'redux');
|
|
@@ -130,6 +188,9 @@ LEVELS.forEach(level => {
|
|
|
130
188
|
_console[level] = console[level].bind(console);
|
|
131
189
|
console[level] = (...args) => {
|
|
132
190
|
_console[level](...args);
|
|
191
|
+
// Skip interception when SDK is paused or debugger is attached
|
|
192
|
+
// This prevents double-logging and message queue deadlocks with CDP
|
|
193
|
+
if (!_shouldIntercept()) return;
|
|
133
194
|
const structuredArgs = args.map(serializeArg);
|
|
134
195
|
const message = args.map(a => {
|
|
135
196
|
if (typeof a === 'string') return a;
|
|
@@ -167,6 +228,10 @@ function _flattenHeaders(h) {
|
|
|
167
228
|
// ─── Fetch Intercept ─────────────────────────────────────────────────────────
|
|
168
229
|
const _fetch = global.fetch;
|
|
169
230
|
global.fetch = async (input, init = {}) => {
|
|
231
|
+
// When SDK is paused or debugger is attached, pass through without interception
|
|
232
|
+
// This prevents racing with CDP's own Fetch.enable domain
|
|
233
|
+
if (!_shouldIntercept()) return _fetch(input, init);
|
|
234
|
+
|
|
170
235
|
// Throttle: simulate slow network or offline
|
|
171
236
|
const delay = THROTTLE_DELAYS[_throttleProfile] || 0;
|
|
172
237
|
if (delay === -1) return Promise.reject(new TypeError('Network request failed (offline throttle)'));
|
|
@@ -236,24 +301,24 @@ global.fetch = async (input, init = {}) => {
|
|
|
236
301
|
return _setHeader.apply(xhr, arguments);
|
|
237
302
|
};
|
|
238
303
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
304
|
+
// Wrap send
|
|
305
|
+
const _send = xhr.send.bind(xhr);
|
|
306
|
+
xhr.send = function(body) {
|
|
307
|
+
if (_shouldIntercept() && _networkCaptureEnabled && !meta.sent) {
|
|
308
|
+
meta.sent = true;
|
|
309
|
+
let reqBody = null;
|
|
310
|
+
if (body != null) {
|
|
311
|
+
try { reqBody = typeof body === 'string' ? body : JSON.parse(JSON.stringify(body)); } catch { reqBody = String(body); }
|
|
312
|
+
}
|
|
313
|
+
mainCh.send({ type: 'network', phase: 'request', id: meta.id, url: meta.url,
|
|
314
|
+
method: meta.method, requestHeaders: meta.headers, requestBody: reqBody });
|
|
315
|
+
}
|
|
316
|
+
return _send.apply(xhr, arguments);
|
|
317
|
+
};
|
|
253
318
|
|
|
254
319
|
// Listen for completion
|
|
255
|
-
|
|
256
|
-
|
|
320
|
+
xhr.addEventListener('readystatechange', function() {
|
|
321
|
+
if (xhr.readyState !== 4 || !meta.sent || !_shouldIntercept() || !_networkCaptureEnabled) return;
|
|
257
322
|
try {
|
|
258
323
|
const duration = Date.now() - meta.t0;
|
|
259
324
|
if (xhr.status > 0) {
|
|
@@ -310,17 +375,8 @@ global.fetch = async (input, init = {}) => {
|
|
|
310
375
|
_console.log('[RNDebugSDK] XHR constructor wrapped for network capture');
|
|
311
376
|
}
|
|
312
377
|
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
});
|
|
378
|
+
// Wrap immediately if available
|
|
379
|
+
if (global.XMLHttpRequest) wrapXHR();
|
|
324
380
|
})();
|
|
325
381
|
|
|
326
382
|
// ─── Axios Interceptor (belt-and-suspenders with XHR patch) ──────────────────
|
|
@@ -334,8 +390,8 @@ setTimeout(() => {
|
|
|
334
390
|
function addDbgInterceptors(instance) {
|
|
335
391
|
if (!instance || !instance.interceptors || instance.__dbgInt) return;
|
|
336
392
|
instance.__dbgInt = true;
|
|
337
|
-
|
|
338
|
-
|
|
393
|
+
instance.interceptors.request.use(config => {
|
|
394
|
+
if (!_shouldIntercept() || !_networkCaptureEnabled) return config;
|
|
339
395
|
const id = `ax-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
|
|
340
396
|
config._dbgId = id;
|
|
341
397
|
config._dbgT0 = Date.now();
|
|
@@ -348,9 +404,10 @@ setTimeout(() => {
|
|
|
348
404
|
mainCh.send({ type:'network', phase:'request', id, url, method:(config.method||'GET').toUpperCase(), requestHeaders:h, requestBody:body });
|
|
349
405
|
return config;
|
|
350
406
|
}, e => Promise.reject(e));
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
407
|
+
instance.interceptors.response.use(resp => {
|
|
408
|
+
if (!_shouldIntercept() || !_networkCaptureEnabled) return resp;
|
|
409
|
+
const c = resp.config || {};
|
|
410
|
+
if (!c._dbgId) return resp;
|
|
354
411
|
const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
|
|
355
412
|
const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
|
|
356
413
|
const rh = {};
|
|
@@ -361,9 +418,10 @@ setTimeout(() => {
|
|
|
361
418
|
mainCh.send({ type:'network', phase:'response', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(),
|
|
362
419
|
status:resp.status, statusText:resp.statusText, duration:dur, responseHeaders:rh, responseBody:body });
|
|
363
420
|
return resp;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
421
|
+
}, err => {
|
|
422
|
+
if (!_shouldIntercept() || !_networkCaptureEnabled) return Promise.reject(err);
|
|
423
|
+
const c = err?.config || {};
|
|
424
|
+
if (c._dbgId) {
|
|
367
425
|
const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
|
|
368
426
|
const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
|
|
369
427
|
const r = err?.response;
|