reactoradar 1.6.11 → 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/app.js +16 -1
- package/main.js +60 -7
- package/package.json +1 -1
- package/panels/console.js +44 -4
- package/panels/native.js +93 -36
- package/panels/network.js +23 -1
- package/panels/redux.js +3 -3
- package/styles.css +72 -0
package/app.js
CHANGED
|
@@ -71,10 +71,25 @@ function syntaxHighlight(json) {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
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
|
+
|
|
74
85
|
function renderJSON(val) {
|
|
75
86
|
if (val == null) return '<span style="color:var(--text-dim)">Empty response</span>';
|
|
76
87
|
try {
|
|
77
|
-
|
|
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);
|
|
78
93
|
if (!str || str === '{}' || str === '""') return '<span style="color:var(--text-dim)">Empty response body</span>';
|
|
79
94
|
return syntaxHighlight(esc(str));
|
|
80
95
|
} catch { try { return esc(JSON.stringify(val)); } catch { return esc('[Unserializable data]'); } }
|
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,14 +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
|
+
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
|
+
}
|
|
404
410
|
if (typeof val === 'function') return `[Function: ${val.name || 'anonymous'}]`;
|
|
405
411
|
return safeStr(val);
|
|
406
412
|
}
|
|
407
413
|
|
|
408
|
-
function createTreeNode(key, val, startCollapsed) {
|
|
414
|
+
function createTreeNode(key, val, startCollapsed, parentPath) {
|
|
409
415
|
const isArray = Array.isArray(val);
|
|
410
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 || '');
|
|
411
421
|
|
|
412
422
|
if (!isObj) {
|
|
413
423
|
// Primitive leaf
|
|
@@ -420,6 +430,16 @@ function createTreeNode(key, val, startCollapsed) {
|
|
|
420
430
|
row.appendChild(k);
|
|
421
431
|
}
|
|
422
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
|
+
});
|
|
423
443
|
return row;
|
|
424
444
|
}
|
|
425
445
|
|
|
@@ -460,7 +480,7 @@ function createTreeNode(key, val, startCollapsed) {
|
|
|
460
480
|
populated = true;
|
|
461
481
|
const entries = collectEntries(val);
|
|
462
482
|
entries.forEach(([k, v]) => {
|
|
463
|
-
children.appendChild(createTreeNode(k, v, true));
|
|
483
|
+
children.appendChild(createTreeNode(k, v, true, fullPath));
|
|
464
484
|
});
|
|
465
485
|
// For arrays show length, for objects show prototype hint
|
|
466
486
|
if (isArray) {
|
|
@@ -497,6 +517,26 @@ function createTreeNode(key, val, startCollapsed) {
|
|
|
497
517
|
}
|
|
498
518
|
});
|
|
499
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
|
+
|
|
500
540
|
container.appendChild(children);
|
|
501
541
|
return container;
|
|
502
542
|
}
|
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
|
@@ -918,6 +918,28 @@ function renderNetDetailContent(r) {
|
|
|
918
918
|
} else {
|
|
919
919
|
body.innerHTML = renderJSON(r.responseBody);
|
|
920
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
|
+
});
|
|
921
943
|
}
|
|
922
944
|
}
|
|
923
945
|
|
|
@@ -944,7 +966,7 @@ function showNetContextMenu(e, r) {
|
|
|
944
966
|
|
|
945
967
|
function showPreviewCopyMenu(e, fullData) {
|
|
946
968
|
const items = [
|
|
947
|
-
{ 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)) },
|
|
948
970
|
];
|
|
949
971
|
const sel = window.getSelection();
|
|
950
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; }
|