reactoradar 1.6.10 → 1.6.12
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/README.md +30 -5
- package/app.js +27 -2
- package/bin/setup.js +26 -26
- package/init.js +18 -7
- package/main.js +60 -7
- package/package.json +1 -1
- package/panels/console.js +49 -8
- package/panels/native.js +93 -36
- package/panels/network.js +29 -5
- package/panels/redux.js +3 -3
- package/styles.css +72 -0
package/README.md
CHANGED
|
@@ -53,6 +53,17 @@
|
|
|
53
53
|
| **React** | Component tree and props inspector via `react-devtools-core` relay |
|
|
54
54
|
| **Settings** | 9 color themes, font family/size, configurable panel visibility with drag-to-reorder, Metro port config, keyboard shortcuts, auto-update, support link |
|
|
55
55
|
|
|
56
|
+
### What's New in v1.6.11
|
|
57
|
+
|
|
58
|
+
- **Auto-clear on reconnect** — All tabs reset automatically when the RN app relaunches (fresh session, no stale data)
|
|
59
|
+
- **No more `[object Object]`** — Safe string serialization everywhere in Console, Network, and Redux panels
|
|
60
|
+
- **SDK auto-detects platform** — Android emulator (`10.0.2.2`) and iOS simulator (`127.0.0.1`) detected at runtime. No manual HOST editing.
|
|
61
|
+
- **Setup auto-patches legacy `createStore`** — Detects `const middleware = []` pattern and wires `reduxMiddleware` automatically
|
|
62
|
+
- **SDK hardened** — BigInt/circular-safe JSON, Redux state >1MB truncated, binary response guard, reconnect backoff, console try/catch
|
|
63
|
+
- **Version rollback** — Settings > Version History shows all releases with download/install buttons
|
|
64
|
+
- **Panel-per-file architecture** — Each panel in its own file under `panels/`. Safer, easier to maintain.
|
|
65
|
+
- **Null guards everywhere** — All panel init functions, badge updates, and DOM access null-safe
|
|
66
|
+
|
|
56
67
|
### What's New in v1.6.0
|
|
57
68
|
|
|
58
69
|
- **Auto-Update** — `.dmg` builds auto-download updates from GitHub Releases. Settings shows "Restart & Update" when ready.
|
|
@@ -134,7 +145,7 @@ Console, Network, Redux, GA4, AsyncStorage data flows automatically. No config n
|
|
|
134
145
|
| Android real device (USB) | `10.0.2.2` | `adb reverse` tunnels over USB (auto-configured) |
|
|
135
146
|
| iOS real device (USB/WiFi) | Mac's LAN IP | Auto-detected. Device must be on same WiFi as Mac. |
|
|
136
147
|
|
|
137
|
-
|
|
148
|
+
The SDK auto-detects the platform at runtime. For iOS real devices, set `HOST_OVERRIDE` in `src/debug/RNDebugSDK.js` to your Mac's LAN IP. `npx reactoradar setup` handles this automatically.
|
|
138
149
|
|
|
139
150
|
### Uninstall
|
|
140
151
|
|
|
@@ -146,7 +157,7 @@ npx reactoradar remove
|
|
|
146
157
|
|
|
147
158
|
| ReactoRadar | React Native | Engine | Architecture |
|
|
148
159
|
|---|---|---|---|
|
|
149
|
-
| v1.6+ | 0.74 — 0.81+ | Hermes | Old & New Architecture |
|
|
160
|
+
| v1.6.11+ | 0.74 — 0.81+ | Hermes | Old & New Architecture |
|
|
150
161
|
|
|
151
162
|
## Network Inspector
|
|
152
163
|
|
|
@@ -203,13 +214,26 @@ export const store = configureStore({
|
|
|
203
214
|
});
|
|
204
215
|
```
|
|
205
216
|
|
|
206
|
-
**Legacy Redux (createStore):**
|
|
217
|
+
**Legacy Redux (createStore with middleware array):**
|
|
218
|
+
```js
|
|
219
|
+
const middleware = [];
|
|
220
|
+
// ... your existing middleware (saga, thunk, etc.)
|
|
221
|
+
if (__DEV__) {
|
|
222
|
+
try {
|
|
223
|
+
const { reduxMiddleware } = require('./debug/RNDebugSDK');
|
|
224
|
+
if (reduxMiddleware) middleware.push(reduxMiddleware);
|
|
225
|
+
} catch {}
|
|
226
|
+
}
|
|
227
|
+
const store = createStore(rootReducer, applyMiddleware(...middleware));
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Legacy Redux (createStore without middleware):**
|
|
207
231
|
```js
|
|
208
232
|
import { reduxEnhancer } from '../debug/RNDebugSDK';
|
|
209
233
|
const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);
|
|
210
234
|
```
|
|
211
235
|
|
|
212
|
-
> **Note:** The import path is relative from your store file to `src/debug/RNDebugSDK`.
|
|
236
|
+
> **Note:** `npx reactoradar setup` auto-detects your store file and patches it. If it can't, follow the examples above. The import path is relative from your store file to `src/debug/RNDebugSDK`.
|
|
213
237
|
|
|
214
238
|
## Settings
|
|
215
239
|
|
|
@@ -293,7 +317,8 @@ const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);
|
|
|
293
317
|
| Network tab empty | Run Metro with `--reset-cache` |
|
|
294
318
|
| Blank screen after long use | Click "Clear All Data" on the memory warning banner, or restart the app |
|
|
295
319
|
| Redux shows "No actions dispatched" | Verify `reduxMiddleware` is wired in your store. Run `npx reactoradar setup` to auto-detect. |
|
|
296
|
-
|
|
|
320
|
+
| Android emulator not connecting | Run `adb reverse tcp:9090 tcp:9090 && adb reverse tcp:9091 tcp:9091 && adb reverse tcp:9092 tcp:9092`. Re-run after emulator restart. |
|
|
321
|
+
| Real device not connecting | Set `HOST_OVERRIDE` in `src/debug/RNDebugSDK.js` to your Mac's LAN IP. Re-run `npx reactoradar setup`. |
|
|
297
322
|
| `XHRInterceptor.js` warning | Set `networking: false` in ReactotronConfig.js |
|
|
298
323
|
| GA4 events not showing | Restart Metro with `--reset-cache` after setup |
|
|
299
324
|
| Port conflict | Run `kill $(lsof -ti :9092)` to free the port, then restart |
|
package/app.js
CHANGED
|
@@ -46,6 +46,14 @@ const esc = s => s == null ? '' : String(s)
|
|
|
46
46
|
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
47
47
|
const ts = ms => new Date(ms).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
48
48
|
|
|
49
|
+
// Safe string conversion — never returns [object Object]
|
|
50
|
+
function safeStr(val) {
|
|
51
|
+
if (val == null) return '';
|
|
52
|
+
if (typeof val === 'string') return val;
|
|
53
|
+
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
54
|
+
try { return JSON.stringify(val); } catch { return '[Complex object]'; }
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
|
|
50
58
|
function pretty(val) {
|
|
51
59
|
if (val == null) return '';
|
|
@@ -63,11 +71,28 @@ function syntaxHighlight(json) {
|
|
|
63
71
|
});
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
// Sort object keys alphabetically for display (recursive)
|
|
75
|
+
function _sortKeys(obj) {
|
|
76
|
+
if (Array.isArray(obj)) return obj.map(_sortKeys);
|
|
77
|
+
if (obj !== null && typeof obj === 'object') {
|
|
78
|
+
const sorted = {};
|
|
79
|
+
Object.keys(obj).sort().forEach(k => { sorted[k] = _sortKeys(obj[k]); });
|
|
80
|
+
return sorted;
|
|
81
|
+
}
|
|
82
|
+
return obj;
|
|
83
|
+
}
|
|
84
|
+
|
|
66
85
|
function renderJSON(val) {
|
|
86
|
+
if (val == null) return '<span style="color:var(--text-dim)">Empty response</span>';
|
|
67
87
|
try {
|
|
68
|
-
|
|
88
|
+
let data = val;
|
|
89
|
+
// Parse string JSON so we can sort keys
|
|
90
|
+
if (typeof data === 'string') { try { data = JSON.parse(data); } catch { return syntaxHighlight(esc(data)); } }
|
|
91
|
+
const sorted = _sortKeys(data);
|
|
92
|
+
const str = JSON.stringify(sorted, null, 2);
|
|
93
|
+
if (!str || str === '{}' || str === '""') return '<span style="color:var(--text-dim)">Empty response body</span>';
|
|
69
94
|
return syntaxHighlight(esc(str));
|
|
70
|
-
} catch { return esc(
|
|
95
|
+
} catch { try { return esc(JSON.stringify(val)); } catch { return esc('[Unserializable data]'); } }
|
|
71
96
|
}
|
|
72
97
|
|
|
73
98
|
function tryURL(url) { try { return new URL(url); } catch { return null; } }
|
package/bin/setup.js
CHANGED
|
@@ -383,30 +383,9 @@ ${SDK_MARKER_END}
|
|
|
383
383
|
} else {
|
|
384
384
|
log('Redux store already has RNDebugSDK wired correctly — skipping');
|
|
385
385
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
|
|
390
|
-
warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
|
|
391
|
-
console.log(C.dim + ' Add manually to your middleware:' + C.reset);
|
|
392
|
-
console.log(C.dim + ` import { reduxMiddleware } from '${relSDK}';` + C.reset);
|
|
393
|
-
console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
|
|
394
|
-
console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
|
|
395
|
-
console.log(C.dim + ' : getDefault(),' + C.reset);
|
|
396
|
-
} else {
|
|
397
|
-
// Add middleware field to configureStore
|
|
398
|
-
const patched = storeContent.replace(
|
|
399
|
-
/(configureStore\s*\(\s*\{)/,
|
|
400
|
-
`$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('${relSDK}').reduxMiddleware)\n : getDefaultMiddleware(),`
|
|
401
|
-
);
|
|
402
|
-
if (patched !== storeContent) {
|
|
403
|
-
fs.writeFileSync(storePath, patched);
|
|
404
|
-
log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired');
|
|
405
|
-
} else {
|
|
406
|
-
warn('Could not auto-patch', storeFile, '— wire Redux manually');
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
} else if (storeContent.includes('createStore')) {
|
|
386
|
+
} else if (/createStore\s*\(/.test(storeContent)) {
|
|
387
|
+
// Legacy createStore (check this BEFORE configureStore — a file may have both
|
|
388
|
+
// if the user named their wrapper function "configureStore" but uses Redux's createStore inside)
|
|
410
389
|
// Legacy createStore — try to auto-patch by adding reduxMiddleware to middleware array
|
|
411
390
|
let patched = storeContent;
|
|
412
391
|
let didPatch = false;
|
|
@@ -449,9 +428,30 @@ ${SDK_MARKER_END}
|
|
|
449
428
|
console.log(C.dim + ' Could not auto-patch. Add manually:' + C.reset);
|
|
450
429
|
console.log(C.dim + ` if (__DEV__) { try { const { reduxMiddleware } = require('${relSDK}'); middleware.push(reduxMiddleware); } catch {} }` + C.reset);
|
|
451
430
|
console.log(C.dim + ' Add this BEFORE the createStore() call in your middleware setup.' + C.reset);
|
|
431
|
+
}
|
|
432
|
+
} else if (/configureStore\s*\(\s*\{/.test(storeContent)) {
|
|
433
|
+
// RTK configureStore({ ... }) — actual Redux Toolkit usage
|
|
434
|
+
if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
|
|
435
|
+
warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
|
|
436
|
+
console.log(C.dim + ' Add manually to your middleware:' + C.reset);
|
|
437
|
+
console.log(C.dim + ` import { reduxMiddleware } from '${relSDK}';` + C.reset);
|
|
438
|
+
console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
|
|
439
|
+
console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
|
|
440
|
+
console.log(C.dim + ' : getDefault(),' + C.reset);
|
|
441
|
+
} else {
|
|
442
|
+
const patched = storeContent.replace(
|
|
443
|
+
/(configureStore\s*\(\s*\{)/,
|
|
444
|
+
`$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('${relSDK}').reduxMiddleware)\n : getDefaultMiddleware(),`
|
|
445
|
+
);
|
|
446
|
+
if (patched !== storeContent) {
|
|
447
|
+
fs.writeFileSync(storePath, patched);
|
|
448
|
+
log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired (RTK)');
|
|
449
|
+
} else {
|
|
450
|
+
warn('Could not auto-patch', storeFile, '— wire Redux manually');
|
|
451
|
+
}
|
|
452
452
|
}
|
|
453
|
-
|
|
454
|
-
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
455
|
warn('Redux detected but store file not found automatically');
|
|
456
456
|
console.log(C.dim + ' Add to your store setup:' + C.reset);
|
|
457
457
|
console.log(C.dim + ' import { reduxMiddleware } from \'./src/debug/RNDebugSDK\';' + C.reset);
|
package/init.js
CHANGED
|
@@ -47,21 +47,32 @@ if (window.electronAPI) {
|
|
|
47
47
|
|
|
48
48
|
window.electronAPI.on('clear-all-ui', clearAll);
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
// Debounced to avoid data loss during hot reloads or flaky connections.
|
|
50
|
+
// Device disconnect → debounced freeMemory. Reconnect → clearAll (fresh session).
|
|
52
51
|
let _disconnectTimer = null;
|
|
52
|
+
let _wasDisconnected = false;
|
|
53
|
+
|
|
53
54
|
window.electronAPI.on('device-all-disconnected', () => {
|
|
55
|
+
_wasDisconnected = true;
|
|
54
56
|
clearTimeout(_disconnectTimer);
|
|
55
57
|
_disconnectTimer = setTimeout(() => {
|
|
56
58
|
console.log('[App] All devices disconnected — freeing memory');
|
|
57
59
|
freeMemory();
|
|
58
60
|
}, 3000);
|
|
59
61
|
});
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
|
|
63
|
+
const _handleReconnect = () => {
|
|
64
|
+
clearTimeout(_disconnectTimer);
|
|
65
|
+
_disconnectTimer = null;
|
|
66
|
+
if (_wasDisconnected) {
|
|
67
|
+
_wasDisconnected = false;
|
|
68
|
+
console.log('[App] Device reconnected — clearing old session data');
|
|
69
|
+
clearAll();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
window.electronAPI.on('redux-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('redux', on); });
|
|
74
|
+
window.electronAPI.on('network-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('network', on); });
|
|
75
|
+
window.electronAPI.on('storage-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('storage', on); });
|
|
65
76
|
window.electronAPI.on('react-dt-status', on => { updateDeviceBanner('reactDT', on); });
|
|
66
77
|
|
|
67
78
|
// Cmd+F — focus the search input for the active panel
|
package/main.js
CHANGED
|
@@ -739,11 +739,15 @@ function setupIPC() {
|
|
|
739
739
|
if (platform === 'android') {
|
|
740
740
|
// adb logcat — show only new logs from now (not historical buffer)
|
|
741
741
|
cmd = 'adb';
|
|
742
|
-
|
|
742
|
+
// -T 1 = last 1 line then real-time. Include Firebase tags at Verbose level for GA4 event capture.
|
|
743
|
+
args = ['logcat', '-v', 'threadtime', '-T', '1', 'FA:V', 'FA-SVC:V', 'FirebaseAnalytics:V', '*:W'];
|
|
743
744
|
} else if (platform === 'ios-sim') {
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
745
|
+
// Use macOS unified log to capture iOS simulator logs
|
|
746
|
+
// processImagePath CONTAINS "CoreSimulator" filters to simulator processes only
|
|
747
|
+
// Captures errors + Firebase/FIRAnalytics events (requires -FIRDebugEnabled in Xcode scheme)
|
|
748
|
+
cmd = '/usr/bin/log';
|
|
749
|
+
args = ['stream', '--style', 'syslog', '--predicate',
|
|
750
|
+
'processImagePath CONTAINS "CoreSimulator" AND (messageType >= error OR composedMessage CONTAINS "firebase" OR composedMessage CONTAINS "FIRAnalytics" OR composedMessage CONTAINS "Logging event" OR composedMessage CONTAINS "GoogleAnalytics" OR composedMessage CONTAINS "[GA4]")'];
|
|
747
751
|
} else if (platform === 'ios-device') {
|
|
748
752
|
// idevicesyslog for real iOS device
|
|
749
753
|
cmd = 'idevicesyslog';
|
|
@@ -808,13 +812,50 @@ function setupIPC() {
|
|
|
808
812
|
if (_nativeLogProcess) { try { _nativeLogProcess.kill(); } catch {} }
|
|
809
813
|
});
|
|
810
814
|
|
|
815
|
+
// Firebase/GA tag detection for both Android and iOS
|
|
816
|
+
const _firebaseTags = new Set(['FA', 'FA-SVC', 'FA:Application', 'FA:Service', 'FirebaseAnalytics', 'FIRAnalytics', 'firebase', 'google.analytics', 'AnalyticsService']);
|
|
817
|
+
const _firebaseTagRe = /^(FA|FA-SVC|FA:Application|FA:Service|FirebaseAnalytics|FIRAnalytics|firebase|google\.analytics|AnalyticsService)/i;
|
|
818
|
+
|
|
819
|
+
function _parseFirebaseEvent(message, tag) {
|
|
820
|
+
if (!message) return null;
|
|
821
|
+
// Android FA tag patterns:
|
|
822
|
+
// "Logging event (FE): session_start(_s), Bundle[{...}]"
|
|
823
|
+
// "Setting event parameter: engagement_time_msec = 1234"
|
|
824
|
+
// "Screen exposed: main_screen"
|
|
825
|
+
let m;
|
|
826
|
+
if ((m = message.match(/Logging event\s*\(?(\w+)?\)?\s*:\s*(\S+?)(?:\((\w+)\))?\s*,?\s*Bundle\[\{(.*)\}\]/i))) {
|
|
827
|
+
const params = {};
|
|
828
|
+
// Parse "key=value, key=value" from Bundle
|
|
829
|
+
if (m[4]) m[4].split(/,\s*/).forEach(p => { const [k, v] = p.split('='); if (k) params[k.trim()] = v ? v.trim() : ''; });
|
|
830
|
+
return { eventName: m[2] || m[3] || 'unknown', source: m[1] || 'native', params };
|
|
831
|
+
}
|
|
832
|
+
if ((m = message.match(/Logging event\s*\(?(\w+)?\)?\s*:\s*(\S+)/i))) {
|
|
833
|
+
return { eventName: m[2], source: m[1] || 'native', params: {} };
|
|
834
|
+
}
|
|
835
|
+
// iOS FIRAnalytics pattern: "Logging event: origin, name, params: { ... }"
|
|
836
|
+
if ((m = message.match(/Logging event:\s*(\w+),\s*(\w+),\s*params:\s*(\{.*\})/i))) {
|
|
837
|
+
let params = {};
|
|
838
|
+
try { params = JSON.parse(m[3]); } catch {}
|
|
839
|
+
return { eventName: m[2], source: m[1] || 'native', params };
|
|
840
|
+
}
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
|
|
811
844
|
function _parseNativeLog(line, platform) {
|
|
812
845
|
if (platform === 'android') {
|
|
813
846
|
// Android logcat format: "06-05 10:30:45.123 1234 5678 E TAG: message"
|
|
814
847
|
const m = line.match(/^\d{2}-\d{2}\s+(\d{2}:\d{2}:\d{2})\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s+([^:]+):\s*(.*)/);
|
|
815
848
|
if (m) {
|
|
816
849
|
const levelMap = { V: 'verbose', D: 'debug', I: 'info', W: 'warn', E: 'error', F: 'fatal' };
|
|
817
|
-
|
|
850
|
+
const tag = m[3].trim();
|
|
851
|
+
const parsed = { ts: Date.now(), time: m[1], level: levelMap[m[2]] || 'info', tag, message: m[4], raw: line };
|
|
852
|
+
// Detect Firebase/GA events
|
|
853
|
+
if (_firebaseTagRe.test(tag)) {
|
|
854
|
+
parsed.firebase = true;
|
|
855
|
+
const fe = _parseFirebaseEvent(m[4], tag);
|
|
856
|
+
if (fe) parsed.firebaseEvent = fe;
|
|
857
|
+
}
|
|
858
|
+
return parsed;
|
|
818
859
|
}
|
|
819
860
|
return { ts: Date.now(), level: 'info', message: line, raw: line };
|
|
820
861
|
}
|
|
@@ -823,13 +864,25 @@ function setupIPC() {
|
|
|
823
864
|
const m1 = line.match(/(\d{2}:\d{2}:\d{2})\.\d+[^\s]*\s+\S+\s+(\S+)\[\d+\].*?<(\w+)>:\s*(.*)/);
|
|
824
865
|
if (m1) {
|
|
825
866
|
const levelMap = { Notice: 'info', Info: 'info', Default: 'info', Debug: 'debug', Error: 'error', Fault: 'fatal' };
|
|
826
|
-
|
|
867
|
+
const parsed = { ts: Date.now(), time: m1[1], level: levelMap[m1[3]] || 'info', tag: m1[2], message: m1[4], raw: line };
|
|
868
|
+
if (_firebaseTagRe.test(m1[2]) || /FIRAnalytics|GoogleAnalytics|firebase/i.test(m1[4])) {
|
|
869
|
+
parsed.firebase = true;
|
|
870
|
+
const fe = _parseFirebaseEvent(m1[4], m1[2]);
|
|
871
|
+
if (fe) parsed.firebaseEvent = fe;
|
|
872
|
+
}
|
|
873
|
+
return parsed;
|
|
827
874
|
}
|
|
828
875
|
// idevicesyslog format: "Jun 5 10:30:45 iPhone MyApp(libsystem)[123] <Error>: message"
|
|
829
876
|
const m2 = line.match(/\w+\s+\d+\s+(\d{2}:\d{2}:\d{2})\s+\S+\s+(\S+?)[\[(].*?<(\w+)>:\s*(.*)/);
|
|
830
877
|
if (m2) {
|
|
831
878
|
const levelMap = { Notice: 'info', Info: 'info', Debug: 'debug', Warning: 'warn', Error: 'error', Critical: 'fatal' };
|
|
832
|
-
|
|
879
|
+
const parsed = { ts: Date.now(), time: m2[1], level: levelMap[m2[3]] || 'info', tag: m2[2], message: m2[4], raw: line };
|
|
880
|
+
if (_firebaseTagRe.test(m2[2]) || /FIRAnalytics|GoogleAnalytics|firebase/i.test(m2[4])) {
|
|
881
|
+
parsed.firebase = true;
|
|
882
|
+
const fe = _parseFirebaseEvent(m2[4], m2[2]);
|
|
883
|
+
if (fe) parsed.firebaseEvent = fe;
|
|
884
|
+
}
|
|
885
|
+
return parsed;
|
|
833
886
|
}
|
|
834
887
|
// Fallback
|
|
835
888
|
const timeMatch = line.match(/(\d{2}:\d{2}:\d{2})/);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactoradar",
|
|
3
3
|
"productName": "ReactoRadar",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.12",
|
|
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/panels/console.js
CHANGED
|
@@ -359,7 +359,8 @@ function collectEntries(val) {
|
|
|
359
359
|
if (!(k in result)) result[k] = val[k];
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
// Sort keys alphabetically for consistent, readable display
|
|
363
|
+
return Object.entries(result).sort((a, b) => String(a[0]).localeCompare(String(b[0])));
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
function objPreview(val, maxLen) {
|
|
@@ -400,13 +401,23 @@ function primitivePreview(val) {
|
|
|
400
401
|
if (typeof val === 'string') return val.length > 50 ? `"${val.slice(0,50)}..."` : `"${val}"`;
|
|
401
402
|
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
402
403
|
if (Array.isArray(val)) return `Array(${val.length})`;
|
|
403
|
-
if (typeof val === 'object')
|
|
404
|
-
|
|
404
|
+
if (typeof val === 'object') {
|
|
405
|
+
const keys = Object.keys(val);
|
|
406
|
+
if (keys.length === 0) return '{}';
|
|
407
|
+
if (keys.length <= 3) return `{${keys.join(', ')}}`;
|
|
408
|
+
return `{${keys.slice(0, 3).join(', ')}, +${keys.length - 3}}`;
|
|
409
|
+
}
|
|
410
|
+
if (typeof val === 'function') return `[Function: ${val.name || 'anonymous'}]`;
|
|
411
|
+
return safeStr(val);
|
|
405
412
|
}
|
|
406
413
|
|
|
407
|
-
function createTreeNode(key, val, startCollapsed) {
|
|
414
|
+
function createTreeNode(key, val, startCollapsed, parentPath) {
|
|
408
415
|
const isArray = Array.isArray(val);
|
|
409
416
|
const isObj = val !== null && typeof val === 'object';
|
|
417
|
+
// Build full dot-notation path for "Copy path"
|
|
418
|
+
const fullPath = key !== null
|
|
419
|
+
? (parentPath ? (typeof key === 'number' ? `${parentPath}[${key}]` : `${parentPath}.${key}`) : String(key))
|
|
420
|
+
: (parentPath || '');
|
|
410
421
|
|
|
411
422
|
if (!isObj) {
|
|
412
423
|
// Primitive leaf
|
|
@@ -419,6 +430,16 @@ function createTreeNode(key, val, startCollapsed) {
|
|
|
419
430
|
row.appendChild(k);
|
|
420
431
|
}
|
|
421
432
|
row.appendChild(createPrimitiveSpan(val));
|
|
433
|
+
// Right-click to copy value
|
|
434
|
+
row.addEventListener('contextmenu', (e) => {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
e.stopPropagation();
|
|
437
|
+
const items = [];
|
|
438
|
+
if (key !== null) items.push({ label: `Copy value of "${key}"`, action: () => navigator.clipboard.writeText(safeStr(val)) });
|
|
439
|
+
items.push({ label: 'Copy as JSON', action: () => { try { navigator.clipboard.writeText(JSON.stringify(val)); } catch { navigator.clipboard.writeText(safeStr(val)); } } });
|
|
440
|
+
if (fullPath) items.push({ label: 'Copy path', action: () => navigator.clipboard.writeText(fullPath) });
|
|
441
|
+
showContextMenu(e, items);
|
|
442
|
+
});
|
|
422
443
|
return row;
|
|
423
444
|
}
|
|
424
445
|
|
|
@@ -459,7 +480,7 @@ function createTreeNode(key, val, startCollapsed) {
|
|
|
459
480
|
populated = true;
|
|
460
481
|
const entries = collectEntries(val);
|
|
461
482
|
entries.forEach(([k, v]) => {
|
|
462
|
-
children.appendChild(createTreeNode(k, v, true));
|
|
483
|
+
children.appendChild(createTreeNode(k, v, true, fullPath));
|
|
463
484
|
});
|
|
464
485
|
// For arrays show length, for objects show prototype hint
|
|
465
486
|
if (isArray) {
|
|
@@ -496,6 +517,26 @@ function createTreeNode(key, val, startCollapsed) {
|
|
|
496
517
|
}
|
|
497
518
|
});
|
|
498
519
|
|
|
520
|
+
// Right-click on any node to copy its subtree
|
|
521
|
+
header.addEventListener('contextmenu', (e) => {
|
|
522
|
+
e.preventDefault();
|
|
523
|
+
e.stopPropagation();
|
|
524
|
+
const items = [];
|
|
525
|
+
if (key !== null) {
|
|
526
|
+
items.push({ label: `Copy "${key}" value`, action: () => {
|
|
527
|
+
try { navigator.clipboard.writeText(JSON.stringify(val, null, 2)); } catch { navigator.clipboard.writeText(safeStr(val)); }
|
|
528
|
+
}});
|
|
529
|
+
items.push({ label: `Copy "${key}" key-value`, action: () => {
|
|
530
|
+
try { navigator.clipboard.writeText(JSON.stringify({ [key]: val }, null, 2)); } catch { navigator.clipboard.writeText(`${key}: ${safeStr(val)}`); }
|
|
531
|
+
}});
|
|
532
|
+
}
|
|
533
|
+
items.push({ label: 'Copy entire object', action: () => {
|
|
534
|
+
try { navigator.clipboard.writeText(JSON.stringify(val, null, 2)); } catch { navigator.clipboard.writeText(safeStr(val)); }
|
|
535
|
+
}});
|
|
536
|
+
if (fullPath) items.push({ label: 'Copy path', action: () => navigator.clipboard.writeText(fullPath) });
|
|
537
|
+
showContextMenu(e, items);
|
|
538
|
+
});
|
|
539
|
+
|
|
499
540
|
container.appendChild(children);
|
|
500
541
|
return container;
|
|
501
542
|
}
|
|
@@ -505,7 +546,7 @@ function _safeStr(val) {
|
|
|
505
546
|
if (val === undefined) return 'undefined';
|
|
506
547
|
if (typeof val === 'string') return val;
|
|
507
548
|
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
508
|
-
try { return JSON.stringify(val, null, 2); } catch { return
|
|
549
|
+
try { return JSON.stringify(val, null, 2); } catch { return '[Complex object]'; }
|
|
509
550
|
}
|
|
510
551
|
|
|
511
552
|
function createPrimitiveSpan(val) {
|
|
@@ -561,7 +602,7 @@ function buildLogBody(logEntry) {
|
|
|
561
602
|
});
|
|
562
603
|
} else if (logEntry.message != null) {
|
|
563
604
|
// Legacy / flat message — try to parse JSON objects out of it
|
|
564
|
-
const msg =
|
|
605
|
+
const msg = safeStr(logEntry.message);
|
|
565
606
|
// Try parsing the whole message as JSON
|
|
566
607
|
try {
|
|
567
608
|
const parsed = JSON.parse(msg);
|
|
@@ -691,7 +732,7 @@ function buildLogRow(l) {
|
|
|
691
732
|
items.push({ label: 'Copy as JSON', action: () => {
|
|
692
733
|
const json = l.args.map(a => {
|
|
693
734
|
if (a.t === 'object' || a.t === 'array') return JSON.stringify(a.v, null, 2);
|
|
694
|
-
return
|
|
735
|
+
return safeStr(a.v);
|
|
695
736
|
}).join(' ');
|
|
696
737
|
navigator.clipboard.writeText(json);
|
|
697
738
|
}});
|
package/panels/native.js
CHANGED
|
@@ -36,6 +36,8 @@ function initNativeLogsPanel() {
|
|
|
36
36
|
<div class="native-prereq-step">2. Enable <b>USB Debugging</b><br/><span style="color:var(--text-dim);font-size:9px">Settings → Developer Options → USB Debugging → ON</span></div>
|
|
37
37
|
<div class="native-prereq-step">3. Connect device via USB and accept the prompt</div>
|
|
38
38
|
<div class="native-prereq-step">4. Verify: run <code>adb devices</code> in terminal</div>
|
|
39
|
+
<div class="native-prereq-step" style="margin-top:6px"><b>Firebase events:</b></div>
|
|
40
|
+
<div class="native-prereq-step"><code>adb shell setprop debug.firebase.analytics.app <pkg></code></div>
|
|
39
41
|
</div>
|
|
40
42
|
<div id="nativeAndroidStatus" class="native-detect-status"></div>
|
|
41
43
|
<button class="native-connect-btn" id="nativeConnectAndroid">Connect Android</button>
|
|
@@ -51,6 +53,8 @@ function initNativeLogsPanel() {
|
|
|
51
53
|
<div class="native-prereq-step">1. Install: <code>brew install libimobiledevice</code></div>
|
|
52
54
|
<div class="native-prereq-step">2. Connect device, tap <b>Trust</b> on the prompt</div>
|
|
53
55
|
<div class="native-prereq-step">3. Verify: <code>idevice_id -l</code> shows device UDID</div>
|
|
56
|
+
<div class="native-prereq-step" style="margin-top:6px"><b>Firebase events:</b></div>
|
|
57
|
+
<div class="native-prereq-step">Add <code>-FIRDebugEnabled</code> to Xcode scheme launch arguments</div>
|
|
54
58
|
</div>
|
|
55
59
|
<div id="nativeIOSStatus" class="native-detect-status"></div>
|
|
56
60
|
<div style="display:flex;gap:6px;margin-top:8px">
|
|
@@ -66,6 +70,7 @@ function initNativeLogsPanel() {
|
|
|
66
70
|
<input id="nativeSearch" class="net-search-input" placeholder="Filter logs..." />
|
|
67
71
|
<div class="native-level-filters" id="nativeLevelFilters">
|
|
68
72
|
<button class="net-status-btn active" data-level="all">All</button>
|
|
73
|
+
<button class="net-status-btn" data-level="firebase" style="color:var(--yellow)">🔥 Firebase</button>
|
|
69
74
|
<button class="net-status-btn" data-level="fatal">Fatal</button>
|
|
70
75
|
<button class="net-status-btn" data-level="error">Error</button>
|
|
71
76
|
<button class="net-status-btn" data-level="warn">Warn</button>
|
|
@@ -197,51 +202,103 @@ function _appendNativeLog(log) {
|
|
|
197
202
|
if (!list) return;
|
|
198
203
|
|
|
199
204
|
// Check filters
|
|
200
|
-
if (_nativeState.levelFilter
|
|
205
|
+
if (_nativeState.levelFilter === 'firebase') {
|
|
206
|
+
if (!log.firebase) return; // Only show Firebase-tagged logs
|
|
207
|
+
} else if (_nativeState.levelFilter !== 'all' && log.level !== _nativeState.levelFilter) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
201
210
|
if (_nativeState.searchFilter && !log.message?.toLowerCase().includes(_nativeState.searchFilter) && !log.tag?.toLowerCase().includes(_nativeState.searchFilter)) return;
|
|
202
211
|
|
|
203
|
-
const isExpandable = log.level === 'error' || log.level === 'fatal' || (log.message || '').length > 200;
|
|
204
|
-
const row = document.createElement('div');
|
|
205
|
-
row.className = `native-log-row native-${log.level || 'info'}`;
|
|
206
|
-
|
|
207
212
|
const time = log.time || new Date(log.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
208
213
|
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
// Firebase events — render GA4-style cards
|
|
215
|
+
if (log.firebase && log.firebaseEvent && _nativeState.levelFilter === 'firebase') {
|
|
216
|
+
const fe = log.firebaseEvent;
|
|
217
|
+
const row = document.createElement('div');
|
|
218
|
+
row.className = 'native-firebase-card';
|
|
219
|
+
|
|
220
|
+
const paramKeys = Object.keys(fe.params || {});
|
|
221
|
+
const paramHtml = paramKeys.length > 0
|
|
222
|
+
? `<div class="native-firebase-params">${paramKeys.map(k =>
|
|
223
|
+
`<div class="native-firebase-param"><span class="native-firebase-param-key">${esc(k)}</span><span class="native-firebase-param-val">${esc(safeStr(fe.params[k]))}</span></div>`
|
|
224
|
+
).join('')}</div>`
|
|
225
|
+
: '';
|
|
226
|
+
|
|
227
|
+
row.innerHTML = `
|
|
228
|
+
<div class="native-firebase-header">
|
|
229
|
+
<span class="native-firebase-event">${esc(fe.eventName)}</span>
|
|
230
|
+
<span class="native-firebase-source">${esc(fe.source)}</span>
|
|
231
|
+
<span class="native-firebase-time">${esc(time)}</span>
|
|
232
|
+
</div>
|
|
233
|
+
${paramHtml}`;
|
|
217
234
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
235
|
+
// Expand/collapse params
|
|
236
|
+
const header = row.querySelector('.native-firebase-header');
|
|
237
|
+
const params = row.querySelector('.native-firebase-params');
|
|
238
|
+
if (header && params) {
|
|
239
|
+
params.style.display = 'none';
|
|
240
|
+
header.style.cursor = 'pointer';
|
|
241
|
+
header.addEventListener('click', () => {
|
|
242
|
+
const open = params.style.display !== 'none';
|
|
243
|
+
params.style.display = open ? 'none' : 'block';
|
|
244
|
+
row.classList.toggle('expanded', !open);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
225
247
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
// Right-click
|
|
249
|
+
row.addEventListener('contextmenu', (e) => {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
showContextMenu(e, [
|
|
252
|
+
{ label: `Copy Event: ${fe.eventName}`, action: () => navigator.clipboard.writeText(fe.eventName) },
|
|
253
|
+
{ label: 'Copy Params (JSON)', action: () => navigator.clipboard.writeText(JSON.stringify(fe.params, null, 2)) },
|
|
254
|
+
{ label: 'Copy Raw Log', action: () => navigator.clipboard.writeText(log.raw || log.message || '') },
|
|
255
|
+
]);
|
|
231
256
|
});
|
|
232
|
-
}
|
|
233
257
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
...(log.tag ? [{ label: `Copy Tag (${log.tag})`, action: () => navigator.clipboard.writeText(log.tag) }] : []),
|
|
241
|
-
]);
|
|
242
|
-
});
|
|
258
|
+
list.appendChild(row);
|
|
259
|
+
} else {
|
|
260
|
+
// Standard log row — with Firebase badge if applicable
|
|
261
|
+
const isExpandable = log.level === 'error' || log.level === 'fatal' || (log.message || '').length > 200;
|
|
262
|
+
const row = document.createElement('div');
|
|
263
|
+
row.className = `native-log-row native-${log.level || 'info'}${log.firebase ? ' native-firebase' : ''}`;
|
|
243
264
|
|
|
244
|
-
|
|
265
|
+
const header = document.createElement('div');
|
|
266
|
+
header.className = 'native-log-header';
|
|
267
|
+
header.innerHTML = `<span class="native-log-time">${esc(time)}</span>`
|
|
268
|
+
+ `<span class="native-log-level">${esc((log.level || 'info').toUpperCase())}</span>`
|
|
269
|
+
+ (log.firebase ? '<span class="native-firebase-badge">Firebase</span>' : '')
|
|
270
|
+
+ (log.tag ? `<span class="native-log-tag">${esc(log.tag)}</span>` : '')
|
|
271
|
+
+ `<span class="native-log-preview">${esc((log.message || '').split('\\n')[0].slice(0, 200))}</span>`;
|
|
272
|
+
row.appendChild(header);
|
|
273
|
+
|
|
274
|
+
if (isExpandable) {
|
|
275
|
+
const fullMsg = document.createElement('div');
|
|
276
|
+
fullMsg.className = 'native-log-full';
|
|
277
|
+
fullMsg.style.display = 'none';
|
|
278
|
+
fullMsg.textContent = log.message || '';
|
|
279
|
+
row.appendChild(fullMsg);
|
|
280
|
+
|
|
281
|
+
header.style.cursor = 'pointer';
|
|
282
|
+
header.addEventListener('click', () => {
|
|
283
|
+
const open = fullMsg.style.display !== 'none';
|
|
284
|
+
fullMsg.style.display = open ? 'none' : 'block';
|
|
285
|
+
row.classList.toggle('expanded', !open);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
row.addEventListener('contextmenu', (e) => {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
const items = [
|
|
292
|
+
{ label: 'Copy Message', action: () => navigator.clipboard.writeText(log.message || '') },
|
|
293
|
+
{ label: 'Copy Raw Line', action: () => navigator.clipboard.writeText(log.raw || log.message || '') },
|
|
294
|
+
];
|
|
295
|
+
if (log.tag) items.push({ label: `Copy Tag (${log.tag})`, action: () => navigator.clipboard.writeText(log.tag) });
|
|
296
|
+
if (log.firebaseEvent) items.push({ label: `Copy Event: ${log.firebaseEvent.eventName}`, action: () => navigator.clipboard.writeText(log.firebaseEvent.eventName) });
|
|
297
|
+
showContextMenu(e, items);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
list.appendChild(row);
|
|
301
|
+
}
|
|
245
302
|
|
|
246
303
|
// Cap DOM rows
|
|
247
304
|
while (list.children.length > 1000) list.firstChild.remove();
|
package/panels/network.js
CHANGED
|
@@ -203,13 +203,13 @@ function initNetworkPanel() {
|
|
|
203
203
|
method: r.method || 'GET',
|
|
204
204
|
url: r.url || '',
|
|
205
205
|
headers: Object.entries(r.requestHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
|
|
206
|
-
postData: r.requestBody ? { mimeType: 'application/json', text: typeof r.requestBody === 'object' ? JSON.stringify(r.requestBody) :
|
|
206
|
+
postData: r.requestBody ? { mimeType: 'application/json', text: typeof r.requestBody === 'object' ? JSON.stringify(r.requestBody) : safeStr(r.requestBody) } : undefined,
|
|
207
207
|
},
|
|
208
208
|
response: {
|
|
209
209
|
status: r.status || 0,
|
|
210
210
|
statusText: r.statusText || '',
|
|
211
211
|
headers: Object.entries(r.responseHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
|
|
212
|
-
content: { size: -1, mimeType: 'application/json', text: r.responseBody ? (typeof r.responseBody === 'object' ? JSON.stringify(r.responseBody) :
|
|
212
|
+
content: { size: -1, mimeType: 'application/json', text: r.responseBody ? (typeof r.responseBody === 'object' ? JSON.stringify(r.responseBody) : safeStr(r.responseBody)) : '' },
|
|
213
213
|
},
|
|
214
214
|
timings: { send: 0, wait: r.duration || 0, receive: 0 },
|
|
215
215
|
};
|
|
@@ -835,7 +835,7 @@ function renderNetDetailContent(r) {
|
|
|
835
835
|
if (!keys.length) return `<div class="section-label">${title}</div><span style="color:var(--text-dim)">none</span>`;
|
|
836
836
|
return `<div class="section-label">${title}</div><div class="kv-grid">${keys.map(k => {
|
|
837
837
|
let val = h[k];
|
|
838
|
-
if (val && typeof val === 'object') { try { val = JSON.stringify(val); } catch { val =
|
|
838
|
+
if (val && typeof val === 'object') { try { val = JSON.stringify(val); } catch { val = '[Complex object]'; } }
|
|
839
839
|
return `<span class="kv-key">${esc(k)}</span><span class="kv-val">${esc(val)}</span>`;
|
|
840
840
|
}).join('')}</div>`;
|
|
841
841
|
};
|
|
@@ -870,6 +870,7 @@ function renderNetDetailContent(r) {
|
|
|
870
870
|
const isErrStatus = _isHttpError(r);
|
|
871
871
|
if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
|
|
872
872
|
if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
|
|
873
|
+
if (r.responseBody == null || r.responseBody === '') { body.innerHTML = '<span style="color:var(--text-dim)">Empty response body</span>'; return; }
|
|
873
874
|
// Render as collapsible JSON tree with right-click copy
|
|
874
875
|
const val = r.responseBody;
|
|
875
876
|
let treeData = val;
|
|
@@ -896,13 +897,14 @@ function renderNetDetailContent(r) {
|
|
|
896
897
|
});
|
|
897
898
|
} else {
|
|
898
899
|
body.innerHTML = isErrStatus
|
|
899
|
-
? `<span style="color:var(--red)">${esc(
|
|
900
|
+
? `<span style="color:var(--red)">${esc(safeStr(r.responseBody))}</span>`
|
|
900
901
|
: '<span style="color:var(--text-dim)">No preview available</span>';
|
|
901
902
|
}
|
|
902
903
|
} else if (tab === 'response') {
|
|
903
904
|
const isErrStatus = _isHttpError(r);
|
|
904
905
|
if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
|
|
905
906
|
if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
|
|
907
|
+
if (r.responseBody == null || r.responseBody === '') { body.innerHTML = '<span style="color:var(--text-dim)">Empty response body</span>'; return; }
|
|
906
908
|
if (isErrStatus) {
|
|
907
909
|
const errBanner = document.createElement('div');
|
|
908
910
|
errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
|
|
@@ -916,6 +918,28 @@ function renderNetDetailContent(r) {
|
|
|
916
918
|
} else {
|
|
917
919
|
body.innerHTML = renderJSON(r.responseBody);
|
|
918
920
|
}
|
|
921
|
+
// Right-click to copy response object
|
|
922
|
+
body.addEventListener('contextmenu', (e) => {
|
|
923
|
+
e.preventDefault();
|
|
924
|
+
const sel = window.getSelection();
|
|
925
|
+
const items = [];
|
|
926
|
+
if (sel && sel.toString().length > 0) {
|
|
927
|
+
items.push({ label: 'Copy Selection', action: () => navigator.clipboard.writeText(sel.toString()) });
|
|
928
|
+
}
|
|
929
|
+
items.push({ label: 'Copy Response Object', action: () => {
|
|
930
|
+
try {
|
|
931
|
+
const data = typeof r.responseBody === 'string' ? JSON.parse(r.responseBody) : r.responseBody;
|
|
932
|
+
navigator.clipboard.writeText(JSON.stringify(_sortKeys(data), null, 2));
|
|
933
|
+
} catch { navigator.clipboard.writeText(safeStr(r.responseBody)); }
|
|
934
|
+
}});
|
|
935
|
+
items.push({ label: 'Copy Response (minified)', action: () => {
|
|
936
|
+
try {
|
|
937
|
+
const data = typeof r.responseBody === 'string' ? JSON.parse(r.responseBody) : r.responseBody;
|
|
938
|
+
navigator.clipboard.writeText(JSON.stringify(_sortKeys(data)));
|
|
939
|
+
} catch { navigator.clipboard.writeText(safeStr(r.responseBody)); }
|
|
940
|
+
}});
|
|
941
|
+
showContextMenu(e, items);
|
|
942
|
+
});
|
|
919
943
|
}
|
|
920
944
|
}
|
|
921
945
|
|
|
@@ -942,7 +966,7 @@ function showNetContextMenu(e, r) {
|
|
|
942
966
|
|
|
943
967
|
function showPreviewCopyMenu(e, fullData) {
|
|
944
968
|
const items = [
|
|
945
|
-
{ label: 'Copy Object', action: () => navigator.clipboard.writeText(JSON.stringify(fullData, null, 2)) },
|
|
969
|
+
{ label: 'Copy Object', action: () => navigator.clipboard.writeText(JSON.stringify(_sortKeys(fullData), null, 2)) },
|
|
946
970
|
];
|
|
947
971
|
const sel = window.getSelection();
|
|
948
972
|
if (sel && sel.toString().length > 0) {
|
package/panels/redux.js
CHANGED
|
@@ -82,7 +82,7 @@ function _findLeafChanges(oldVal, newVal, basePath, maxDepth) {
|
|
|
82
82
|
changes.push({ path, oldVal: a, newVal: b });
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
-
const allKeys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
85
|
+
const allKeys = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
|
|
86
86
|
allKeys.forEach(k => {
|
|
87
87
|
if (!_deepEqual(a[k], b[k])) {
|
|
88
88
|
const childPath = path ? `${path}.${k}` : k;
|
|
@@ -169,7 +169,7 @@ function _createHighlightedTree(key, val, changedPaths, currentPath, isOld) {
|
|
|
169
169
|
function populate() {
|
|
170
170
|
if (populated) return;
|
|
171
171
|
populated = true;
|
|
172
|
-
const entries = isArray ? val.map((v, i) => [i, v]) : Object.entries(val);
|
|
172
|
+
const entries = isArray ? val.map((v, i) => [i, v]) : Object.entries(val).sort((a, b) => String(a[0]).localeCompare(String(b[0])));
|
|
173
173
|
entries.forEach(([k, v]) => {
|
|
174
174
|
children.appendChild(_createHighlightedTree(k, v, changedPaths, myPath, isOld));
|
|
175
175
|
});
|
|
@@ -197,7 +197,7 @@ function handleReduxEvent(event) {
|
|
|
197
197
|
const prevState = state.redux.states.length > 0 ? state.redux.states[state.redux.states.length - 1] : null;
|
|
198
198
|
const changedKeys = [];
|
|
199
199
|
if (prevState && nextState && typeof prevState === 'object' && typeof nextState === 'object') {
|
|
200
|
-
const allKeys = new Set([...Object.keys(prevState), ...Object.keys(nextState)]);
|
|
200
|
+
const allKeys = [...new Set([...Object.keys(prevState), ...Object.keys(nextState)])].sort();
|
|
201
201
|
allKeys.forEach(k => { if (!_deepEqual(prevState[k], nextState[k])) changedKeys.push(k); });
|
|
202
202
|
}
|
|
203
203
|
|
package/styles.css
CHANGED
|
@@ -1799,6 +1799,78 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
|
|
|
1799
1799
|
.native-fatal { background: rgba(255,94,114,.08); }
|
|
1800
1800
|
.native-fatal .native-log-msg { color: var(--red); font-weight: 700; }
|
|
1801
1801
|
|
|
1802
|
+
/* Firebase event cards (GA4-style in Native Logs) */
|
|
1803
|
+
.native-firebase-badge {
|
|
1804
|
+
display: inline-block;
|
|
1805
|
+
background: rgba(255,196,0,.15);
|
|
1806
|
+
color: #ffa500;
|
|
1807
|
+
font-size: 9px;
|
|
1808
|
+
font-weight: 700;
|
|
1809
|
+
padding: 1px 5px;
|
|
1810
|
+
border-radius: 3px;
|
|
1811
|
+
margin-right: 4px;
|
|
1812
|
+
text-transform: uppercase;
|
|
1813
|
+
}
|
|
1814
|
+
.native-firebase { border-left: 2px solid #ffa500; }
|
|
1815
|
+
.native-firebase-card {
|
|
1816
|
+
padding: 8px 12px;
|
|
1817
|
+
border-bottom: 1px solid var(--border);
|
|
1818
|
+
border-left: 3px solid #ffa500;
|
|
1819
|
+
transition: background 0.12s;
|
|
1820
|
+
}
|
|
1821
|
+
.native-firebase-card:hover { background: var(--bg3); }
|
|
1822
|
+
.native-firebase-card.expanded { background: var(--bg3); }
|
|
1823
|
+
.native-firebase-header {
|
|
1824
|
+
display: flex;
|
|
1825
|
+
align-items: center;
|
|
1826
|
+
gap: 8px;
|
|
1827
|
+
font-size: 11px;
|
|
1828
|
+
}
|
|
1829
|
+
.native-firebase-event {
|
|
1830
|
+
font-weight: 700;
|
|
1831
|
+
color: var(--accent);
|
|
1832
|
+
font-size: 12px;
|
|
1833
|
+
}
|
|
1834
|
+
.native-firebase-source {
|
|
1835
|
+
font-size: 9px;
|
|
1836
|
+
color: var(--text-dim);
|
|
1837
|
+
background: var(--bg3);
|
|
1838
|
+
padding: 1px 5px;
|
|
1839
|
+
border-radius: 3px;
|
|
1840
|
+
text-transform: uppercase;
|
|
1841
|
+
}
|
|
1842
|
+
.native-firebase-time {
|
|
1843
|
+
margin-left: auto;
|
|
1844
|
+
font-size: 10px;
|
|
1845
|
+
color: var(--text-dim);
|
|
1846
|
+
font-variant-numeric: tabular-nums;
|
|
1847
|
+
}
|
|
1848
|
+
.native-firebase-params {
|
|
1849
|
+
margin-top: 6px;
|
|
1850
|
+
padding: 6px 8px;
|
|
1851
|
+
background: var(--bg2);
|
|
1852
|
+
border-radius: 4px;
|
|
1853
|
+
border: 1px solid var(--border);
|
|
1854
|
+
}
|
|
1855
|
+
.native-firebase-param {
|
|
1856
|
+
display: flex;
|
|
1857
|
+
gap: 8px;
|
|
1858
|
+
padding: 2px 0;
|
|
1859
|
+
font-size: 10px;
|
|
1860
|
+
border-bottom: 1px solid var(--border);
|
|
1861
|
+
}
|
|
1862
|
+
.native-firebase-param:last-child { border-bottom: none; }
|
|
1863
|
+
.native-firebase-param-key {
|
|
1864
|
+
color: var(--accent);
|
|
1865
|
+
font-weight: 600;
|
|
1866
|
+
min-width: 120px;
|
|
1867
|
+
flex-shrink: 0;
|
|
1868
|
+
}
|
|
1869
|
+
.native-firebase-param-val {
|
|
1870
|
+
color: var(--text-mid);
|
|
1871
|
+
word-break: break-all;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1802
1874
|
/* ── Detail Panel Search ───────────────────────────────────────────────────── */
|
|
1803
1875
|
.detail-search-wrap { display: flex; align-items: center; gap: 4px; margin-left: auto; padding: 0 6px; }
|
|
1804
1876
|
.detail-search-input { width: 150px; font-size: 10px; padding: 3px 6px; border: 1px solid var(--border); background: var(--bg2); color: var(--text); border-radius: 3px; outline: none; }
|