reactoradar 1.2.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -223
- package/app.js +425 -81
- package/index.html +5 -1
- package/main.js +14 -12
- package/package.json +1 -1
- package/preload.js +2 -2
- package/sdk/RNDebugSDK.js +117 -4
- package/styles.css +216 -13
package/app.js
CHANGED
|
@@ -6,7 +6,7 @@ const state = {
|
|
|
6
6
|
activePanel: 'console',
|
|
7
7
|
ports: {},
|
|
8
8
|
|
|
9
|
-
console: { logs: [],
|
|
9
|
+
console: { logs: [], levelFilters: { log: true, info: true, warn: true, error: true, debug: true }, searchFilter: '' },
|
|
10
10
|
|
|
11
11
|
network: {
|
|
12
12
|
requests: {},
|
|
@@ -106,8 +106,7 @@ document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
|
106
106
|
|
|
107
107
|
// Global filter removed — each panel has its own search input
|
|
108
108
|
|
|
109
|
-
// ─── Clear (
|
|
110
|
-
$('btnClear').addEventListener('click', clearActiveTab);
|
|
109
|
+
// ─── Clear (each panel has its own clear button now) ─────────────────────────
|
|
111
110
|
|
|
112
111
|
function clearActiveTab() {
|
|
113
112
|
switch (state.activePanel) {
|
|
@@ -139,6 +138,13 @@ function clearActiveTab() {
|
|
|
139
138
|
$('sBadge').textContent = '0';
|
|
140
139
|
renderStorage();
|
|
141
140
|
break;
|
|
141
|
+
case 'ga4':
|
|
142
|
+
ga4State.events = [];
|
|
143
|
+
ga4State.selected = -1;
|
|
144
|
+
$('ga4Badge').textContent = '0';
|
|
145
|
+
renderGA4List();
|
|
146
|
+
renderGA4Summary();
|
|
147
|
+
break;
|
|
142
148
|
case 'performance':
|
|
143
149
|
perfState.fps = [];
|
|
144
150
|
perfState.jsThread = [];
|
|
@@ -213,6 +219,8 @@ if (window.electronAPI) {
|
|
|
213
219
|
window.electronAPI.on('network-event', handleNetworkEvent);
|
|
214
220
|
window.electronAPI.on('storage-event', handleStorageEvent);
|
|
215
221
|
|
|
222
|
+
window.electronAPI.on('ga4-event', handleGA4Event);
|
|
223
|
+
|
|
216
224
|
window.electronAPI.on('perf-event', event => {
|
|
217
225
|
handlePerfEvent(event);
|
|
218
226
|
handleMemoryEvent(event);
|
|
@@ -225,17 +233,30 @@ if (window.electronAPI) {
|
|
|
225
233
|
|
|
226
234
|
window.electronAPI.on('clear-all-ui', clearAll);
|
|
227
235
|
|
|
236
|
+
window.electronAPI.on('app-version', (version) => {
|
|
237
|
+
state._appVersion = version;
|
|
238
|
+
const el = $('aboutVersion');
|
|
239
|
+
if (el) el.textContent = 'v' + version;
|
|
240
|
+
});
|
|
241
|
+
|
|
228
242
|
window.electronAPI.on('update-available', ({ current, latest }) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
// Show in settings only, not as a banner
|
|
244
|
+
state._updateAvailable = { current, latest };
|
|
245
|
+
const el = $('aboutVersion');
|
|
246
|
+
if (el) el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
|
|
247
|
+
// Add update button in settings if not already there
|
|
248
|
+
if (!$('updateBtn')) {
|
|
249
|
+
const aboutEl = document.querySelector('.settings-about');
|
|
250
|
+
if (aboutEl) {
|
|
251
|
+
const btn = document.createElement('div');
|
|
252
|
+
btn.style.cssText = 'margin-top:10px';
|
|
253
|
+
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px">Download v' + latest + '</button>';
|
|
254
|
+
aboutEl.appendChild(btn);
|
|
255
|
+
$('updateBtn')?.addEventListener('click', () => {
|
|
256
|
+
window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
239
260
|
});
|
|
240
261
|
|
|
241
262
|
window.electronAPI.on('trigger-open-cdp', () => {
|
|
@@ -272,26 +293,40 @@ function updateDeviceBanner(service, connected) {
|
|
|
272
293
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
273
294
|
// CONSOLE PANEL
|
|
274
295
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
296
|
+
// Load saved log level filters from localStorage
|
|
297
|
+
function getStoredLogLevels() {
|
|
298
|
+
try {
|
|
299
|
+
const saved = localStorage.getItem('rn-debug-log-levels');
|
|
300
|
+
if (saved) return JSON.parse(saved);
|
|
301
|
+
} catch {}
|
|
302
|
+
return { log: true, info: true, warn: true, error: true, debug: true };
|
|
303
|
+
}
|
|
304
|
+
function setStoredLogLevels(levels) {
|
|
305
|
+
try { localStorage.setItem('rn-debug-log-levels', JSON.stringify(levels)); } catch {}
|
|
306
|
+
}
|
|
307
|
+
|
|
275
308
|
function initConsolePanel() {
|
|
276
309
|
const panel = $('panel-console');
|
|
310
|
+
const levels = getStoredLogLevels();
|
|
311
|
+
state.console.levelFilters = levels;
|
|
312
|
+
|
|
277
313
|
panel.innerHTML = `
|
|
278
314
|
<div class="panel-toolbar">
|
|
279
315
|
<span class="panel-label">Console</span>
|
|
280
316
|
<span class="badge" id="cBadge">0</span>
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
<button class="
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
</label>
|
|
317
|
+
<input id="consoleSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter logs..." />
|
|
318
|
+
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
319
|
+
<button class="panel-clear-btn" id="consoleClear" title="Clear console">Clear</button>
|
|
320
|
+
<div class="console-level-dropdown" id="consoleLevelDropdown">
|
|
321
|
+
<button class="console-level-btn" id="consoleLevelBtn">Levels ▾</button>
|
|
322
|
+
<div class="console-level-menu" id="consoleLevelMenu">
|
|
323
|
+
<label class="console-level-option"><input type="checkbox" data-level="log" ${levels.log ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--text-mid)"></span>Log</label>
|
|
324
|
+
<label class="console-level-option"><input type="checkbox" data-level="info" ${levels.info ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--accent)"></span>Info</label>
|
|
325
|
+
<label class="console-level-option"><input type="checkbox" data-level="warn" ${levels.warn ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--yellow)"></span>Warn</label>
|
|
326
|
+
<label class="console-level-option"><input type="checkbox" data-level="error" ${levels.error ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--red)"></span>Error</label>
|
|
327
|
+
<label class="console-level-option"><input type="checkbox" data-level="debug" ${levels.debug ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--accent2)"></span>Debug</label>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
295
330
|
</div>
|
|
296
331
|
</div>
|
|
297
332
|
<div class="scroll-area" id="consoleList">
|
|
@@ -302,25 +337,60 @@ function initConsolePanel() {
|
|
|
302
337
|
</div>
|
|
303
338
|
</div>`;
|
|
304
339
|
|
|
340
|
+
// Search filter
|
|
305
341
|
$('consoleSearch').addEventListener('input', (e) => {
|
|
306
342
|
state.console.searchFilter = e.target.value.toLowerCase().trim();
|
|
307
343
|
renderConsole();
|
|
308
344
|
});
|
|
309
345
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
$('
|
|
314
|
-
|
|
315
|
-
|
|
346
|
+
// Level dropdown toggle
|
|
347
|
+
$('consoleLevelBtn').addEventListener('click', (e) => {
|
|
348
|
+
e.stopPropagation();
|
|
349
|
+
$('consoleLevelMenu').classList.toggle('open');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Close dropdown when clicking outside
|
|
353
|
+
document.addEventListener('click', (e) => {
|
|
354
|
+
if (!e.target.closest('#consoleLevelDropdown')) {
|
|
355
|
+
$('consoleLevelMenu')?.classList.remove('open');
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Level checkbox changes
|
|
360
|
+
$('consoleLevelMenu').addEventListener('change', (e) => {
|
|
361
|
+
const checkbox = e.target;
|
|
362
|
+
const level = checkbox.dataset.level;
|
|
363
|
+
if (level) {
|
|
364
|
+
state.console.levelFilters[level] = checkbox.checked;
|
|
365
|
+
setStoredLogLevels(state.console.levelFilters);
|
|
366
|
+
updateLevelBtnText();
|
|
367
|
+
renderConsole();
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
updateLevelBtnText();
|
|
372
|
+
|
|
373
|
+
$('consoleClear').addEventListener('click', () => {
|
|
374
|
+
state.console.logs = [];
|
|
375
|
+
_consolePending = [];
|
|
376
|
+
$('cBadge').textContent = '0';
|
|
377
|
+
renderConsole();
|
|
316
378
|
});
|
|
317
379
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
380
|
+
|
|
381
|
+
function updateLevelBtnText() {
|
|
382
|
+
const levels = state.console.levelFilters;
|
|
383
|
+
const allOn = Object.values(levels).every(v => v);
|
|
384
|
+
const allOff = Object.values(levels).every(v => !v);
|
|
385
|
+
const btn = $('consoleLevelBtn');
|
|
386
|
+
if (!btn) return;
|
|
387
|
+
if (allOn) btn.textContent = 'All Levels ▾';
|
|
388
|
+
else if (allOff) btn.textContent = 'None ▾';
|
|
389
|
+
else {
|
|
390
|
+
const active = Object.entries(levels).filter(([, v]) => v).map(([k]) => k.charAt(0).toUpperCase() + k.slice(1));
|
|
391
|
+
btn.textContent = active.join(', ') + ' ▾';
|
|
392
|
+
}
|
|
393
|
+
}
|
|
324
394
|
|
|
325
395
|
// Console is fed via IPC (network-event handled in IPC section above)
|
|
326
396
|
|
|
@@ -349,12 +419,12 @@ function flushConsoleBatch() {
|
|
|
349
419
|
const empty = $('consoleEmpty');
|
|
350
420
|
if (!list) return;
|
|
351
421
|
|
|
352
|
-
const {
|
|
422
|
+
const { levelFilters, searchFilter } = state.console;
|
|
353
423
|
const frag = document.createDocumentFragment();
|
|
354
424
|
let added = 0;
|
|
355
425
|
|
|
356
426
|
batch.forEach(l => {
|
|
357
|
-
if (
|
|
427
|
+
if (levelFilters && !levelFilters[l.level]) return;
|
|
358
428
|
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return;
|
|
359
429
|
frag.appendChild(buildLogRow(l));
|
|
360
430
|
added++;
|
|
@@ -621,27 +691,23 @@ function buildLogRow(l) {
|
|
|
621
691
|
lvlSpan.textContent = l.level;
|
|
622
692
|
div.appendChild(lvlSpan);
|
|
623
693
|
|
|
624
|
-
//
|
|
694
|
+
// Arrow (inline, not inside body-wrap)
|
|
695
|
+
const arrow = document.createElement('span');
|
|
696
|
+
arrow.className = 'log-arrow';
|
|
697
|
+
arrow.textContent = '\u25B6';
|
|
698
|
+
div.appendChild(arrow);
|
|
699
|
+
|
|
700
|
+
// Body wrapper
|
|
625
701
|
const bodyWrap = document.createElement('div');
|
|
626
702
|
bodyWrap.className = 'log-body-wrap';
|
|
627
703
|
|
|
628
|
-
// Single-line preview
|
|
704
|
+
// Single-line preview: message text + caller
|
|
629
705
|
const preview = document.createElement('div');
|
|
630
706
|
preview.className = 'log-preview';
|
|
631
707
|
const msgText = (l.message || '').replace(/\n/g, ' ').slice(0, 200);
|
|
632
708
|
const previewText = document.createElement('span');
|
|
633
709
|
previewText.textContent = msgText + ((l.message || '').length > 200 ? '...' : '');
|
|
634
710
|
preview.appendChild(previewText);
|
|
635
|
-
if (l.caller) {
|
|
636
|
-
// Extract short filename:line from caller like "at Component (file.js:42:10)"
|
|
637
|
-
const callerShort = extractCallerShort(l.caller);
|
|
638
|
-
if (callerShort) {
|
|
639
|
-
const callerTag = document.createElement('span');
|
|
640
|
-
callerTag.className = 'log-caller-inline';
|
|
641
|
-
callerTag.textContent = callerShort;
|
|
642
|
-
preview.appendChild(callerTag);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
711
|
bodyWrap.appendChild(preview);
|
|
646
712
|
|
|
647
713
|
// Full content (hidden by default)
|
|
@@ -649,20 +715,8 @@ function buildLogRow(l) {
|
|
|
649
715
|
full.className = 'log-full';
|
|
650
716
|
full.style.display = 'none';
|
|
651
717
|
full.appendChild(buildLogBody(l));
|
|
652
|
-
if (l.caller) {
|
|
653
|
-
const callerSpan = document.createElement('span');
|
|
654
|
-
callerSpan.className = 'log-caller';
|
|
655
|
-
callerSpan.textContent = l.caller;
|
|
656
|
-
full.appendChild(callerSpan);
|
|
657
|
-
}
|
|
658
718
|
bodyWrap.appendChild(full);
|
|
659
719
|
|
|
660
|
-
// Expand/collapse arrow
|
|
661
|
-
const arrow = document.createElement('span');
|
|
662
|
-
arrow.className = 'log-arrow';
|
|
663
|
-
arrow.textContent = '\u25B6';
|
|
664
|
-
bodyWrap.prepend(arrow);
|
|
665
|
-
|
|
666
720
|
let expanded = false;
|
|
667
721
|
// Only toggle on click, NOT on text selection drag
|
|
668
722
|
let _mouseDownPos = null;
|
|
@@ -761,9 +815,9 @@ function renderConsole() {
|
|
|
761
815
|
const empty = $('consoleEmpty');
|
|
762
816
|
if (!list) return;
|
|
763
817
|
|
|
764
|
-
const {
|
|
818
|
+
const { levelFilters, searchFilter } = state.console;
|
|
765
819
|
const visible = state.console.logs.filter(l => {
|
|
766
|
-
if (
|
|
820
|
+
if (levelFilters && !levelFilters[l.level]) return false;
|
|
767
821
|
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return false;
|
|
768
822
|
return true;
|
|
769
823
|
});
|
|
@@ -792,13 +846,13 @@ function renderConsole() {
|
|
|
792
846
|
// NETWORK PANEL (Chrome DevTools-style)
|
|
793
847
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
794
848
|
const NET_COLS = [
|
|
795
|
-
{ key: 'name', label: 'Name', width:
|
|
849
|
+
{ key: 'name', label: 'Name', width: 380, min: 150 },
|
|
796
850
|
{ key: 'status', label: 'Status', width: 60, min: 40 },
|
|
797
851
|
{ key: 'type', label: 'Type', width: 70, min: 40 },
|
|
798
|
-
{ key: 'initiator', label: 'Initiator', width:
|
|
799
|
-
{ key: 'size', label: 'Size', width:
|
|
800
|
-
{ key: 'time', label: 'Time', width:
|
|
801
|
-
{ key: 'waterfall', label: 'Waterfall', width:
|
|
852
|
+
{ key: 'initiator', label: 'Initiator', width: 80, min: 50 },
|
|
853
|
+
{ key: 'size', label: 'Size', width: 65, min: 40 },
|
|
854
|
+
{ key: 'time', label: 'Time', width: 65, min: 40 },
|
|
855
|
+
{ key: 'waterfall', label: 'Waterfall', width: 100, min: 60 },
|
|
802
856
|
];
|
|
803
857
|
|
|
804
858
|
function initNetworkPanel() {
|
|
@@ -808,6 +862,7 @@ function initNetworkPanel() {
|
|
|
808
862
|
<span class="panel-label">Network</span>
|
|
809
863
|
<span class="badge" id="nBadge">0</span>
|
|
810
864
|
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
865
|
+
<button class="panel-clear-btn" id="networkClear" title="Clear network">Clear</button>
|
|
811
866
|
<label class="toggle-label" for="netToggle">
|
|
812
867
|
<span class="toggle-text" id="netToggleText">Capture ON</span>
|
|
813
868
|
<input type="checkbox" id="netToggle" class="toggle-input" checked />
|
|
@@ -886,6 +941,16 @@ function initNetworkPanel() {
|
|
|
886
941
|
window.electronAPI?.setNetworkThrottle(state.network.throttle);
|
|
887
942
|
});
|
|
888
943
|
|
|
944
|
+
// Clear network
|
|
945
|
+
$('networkClear').addEventListener('click', () => {
|
|
946
|
+
state.network.requests = {};
|
|
947
|
+
state.network.order = [];
|
|
948
|
+
state.network.selectedId = null;
|
|
949
|
+
closeNetDetail();
|
|
950
|
+
$('nBadge').textContent = '0';
|
|
951
|
+
renderNetwork();
|
|
952
|
+
});
|
|
953
|
+
|
|
889
954
|
// Close detail button
|
|
890
955
|
$('netDetailClose').addEventListener('click', closeNetDetail);
|
|
891
956
|
|
|
@@ -1003,13 +1068,15 @@ function matchNetType(r, type) {
|
|
|
1003
1068
|
const ct = (r.responseHeaders?.['content-type'] || r.responseHeaders?.['Content-Type'] || '').toLowerCase();
|
|
1004
1069
|
const url = (r.url || '').toLowerCase();
|
|
1005
1070
|
switch (type) {
|
|
1006
|
-
case 'fetch':
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
case '
|
|
1010
|
-
case '
|
|
1011
|
-
case '
|
|
1012
|
-
case '
|
|
1071
|
+
case 'fetch': // Fetch/XHR — show API calls (JSON, text, form data), exclude static assets
|
|
1072
|
+
return !ct.includes('image') && !ct.includes('font') && !ct.includes('video') && !ct.includes('audio')
|
|
1073
|
+
&& !/\.(png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf|otf|eot|mp4|mp3|css)(\?|$)/.test(url);
|
|
1074
|
+
case 'js': return ct.includes('javascript') || /\.(js|jsx|bundle)(\?|$)/.test(url);
|
|
1075
|
+
case 'css': return ct.includes('css') || /\.css(\?|$)/.test(url);
|
|
1076
|
+
case 'img': return ct.includes('image') || /\.(png|jpg|jpeg|gif|svg|webp|ico|avif|bmp)(\?|$)/.test(url);
|
|
1077
|
+
case 'media': return ct.includes('video') || ct.includes('audio') || /\.(mp4|mp3|wav|webm|ogg|m3u8)(\?|$)/.test(url);
|
|
1078
|
+
case 'font': return ct.includes('font') || /\.(woff2?|ttf|otf|eot)(\?|$)/.test(url);
|
|
1079
|
+
case 'doc': return ct.includes('html') || ct.includes('xml') || /\.(html?|xml)(\?|$)/.test(url);
|
|
1013
1080
|
case 'ws': return url.startsWith('ws://') || url.startsWith('wss://');
|
|
1014
1081
|
default: return true;
|
|
1015
1082
|
}
|
|
@@ -1380,6 +1447,273 @@ function buildCurlCommand(r) {
|
|
|
1380
1447
|
return cmd;
|
|
1381
1448
|
}
|
|
1382
1449
|
|
|
1450
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1451
|
+
// GA4 EVENT INSPECTOR
|
|
1452
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1453
|
+
const ga4State = { events: [], selected: -1, searchFilter: '', sortDir: 'desc' };
|
|
1454
|
+
|
|
1455
|
+
function initGA4Panel() {
|
|
1456
|
+
const panel = $('panel-ga4');
|
|
1457
|
+
panel.innerHTML = `
|
|
1458
|
+
<div class="panel-toolbar">
|
|
1459
|
+
<span class="panel-label">GA4 Events</span>
|
|
1460
|
+
<span class="badge" id="ga4Badge">0</span>
|
|
1461
|
+
<input id="ga4Search" class="net-search-input" style="margin-left:12px" placeholder="Filter events..." />
|
|
1462
|
+
<div class="ml-auto">
|
|
1463
|
+
<button class="panel-clear-btn" id="ga4Clear" title="Clear GA4 events">Clear</button>
|
|
1464
|
+
</div>
|
|
1465
|
+
</div>
|
|
1466
|
+
<div class="ga4-layout">
|
|
1467
|
+
<div class="ga4-list-pane">
|
|
1468
|
+
<div class="ga4-list-header">
|
|
1469
|
+
<span class="ga4-hcell ga4-sort-btn" id="ga4SortBtn" style="width:90px;cursor:pointer" title="Click to toggle sort order">Time <span id="ga4SortIcon">\u25BC</span></span>
|
|
1470
|
+
<span class="ga4-hcell" style="flex:1">Event</span>
|
|
1471
|
+
</div>
|
|
1472
|
+
<div class="scroll-area" id="ga4List">
|
|
1473
|
+
<div class="empty-state" id="ga4Empty">
|
|
1474
|
+
<div class="icon" style="font-size:28px;opacity:.2">📊</div>
|
|
1475
|
+
<div class="label">No GA4 events yet</div>
|
|
1476
|
+
<div class="hint">Events from @react-native-firebase/analytics will appear here</div>
|
|
1477
|
+
</div>
|
|
1478
|
+
</div>
|
|
1479
|
+
</div>
|
|
1480
|
+
<div class="ga4-resize-handle" id="ga4ResizeHandle"></div>
|
|
1481
|
+
<div class="ga4-detail-pane" id="ga4DetailPane">
|
|
1482
|
+
<div class="ga4-detail-header">EVENT DETAIL</div>
|
|
1483
|
+
<div class="scroll-area ga4-detail-content" id="ga4Detail">
|
|
1484
|
+
<span style="color:var(--text-dim);padding:16px;display:block">Click an event to inspect</span>
|
|
1485
|
+
</div>
|
|
1486
|
+
</div>
|
|
1487
|
+
</div>
|
|
1488
|
+
<div class="ga4-summary" id="ga4Summary">
|
|
1489
|
+
<span class="ga4-summary-label">Total: 0</span>
|
|
1490
|
+
</div>`;
|
|
1491
|
+
|
|
1492
|
+
$('ga4Search').addEventListener('input', (e) => {
|
|
1493
|
+
ga4State.searchFilter = e.target.value.toLowerCase().trim();
|
|
1494
|
+
renderGA4List();
|
|
1495
|
+
renderGA4Summary(); // update active chip highlight
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
$('ga4Clear').addEventListener('click', () => {
|
|
1499
|
+
ga4State.events = [];
|
|
1500
|
+
ga4State.selected = -1;
|
|
1501
|
+
$('ga4Badge').textContent = '0';
|
|
1502
|
+
renderGA4List();
|
|
1503
|
+
renderGA4Summary();
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
$('ga4SortBtn').addEventListener('click', () => {
|
|
1507
|
+
ga4State.sortDir = ga4State.sortDir === 'desc' ? 'asc' : 'desc';
|
|
1508
|
+
$('ga4SortIcon').textContent = ga4State.sortDir === 'desc' ? '\u25BC' : '\u25B2';
|
|
1509
|
+
renderGA4List();
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
// Resizable divider between list and detail
|
|
1513
|
+
const resizeHandle = $('ga4ResizeHandle');
|
|
1514
|
+
const detailPane = $('ga4DetailPane');
|
|
1515
|
+
resizeHandle.addEventListener('mousedown', (e) => {
|
|
1516
|
+
e.preventDefault();
|
|
1517
|
+
const startX = e.clientX;
|
|
1518
|
+
const startWidth = detailPane.offsetWidth;
|
|
1519
|
+
document.body.style.cursor = 'col-resize';
|
|
1520
|
+
document.body.style.userSelect = 'none';
|
|
1521
|
+
function onMove(ev) {
|
|
1522
|
+
const delta = startX - ev.clientX;
|
|
1523
|
+
detailPane.style.width = Math.max(200, Math.min(window.innerWidth * 0.8, startWidth + delta)) + 'px';
|
|
1524
|
+
}
|
|
1525
|
+
function onUp() {
|
|
1526
|
+
document.body.style.cursor = '';
|
|
1527
|
+
document.body.style.userSelect = '';
|
|
1528
|
+
document.removeEventListener('mousemove', onMove);
|
|
1529
|
+
document.removeEventListener('mouseup', onUp);
|
|
1530
|
+
}
|
|
1531
|
+
document.addEventListener('mousemove', onMove);
|
|
1532
|
+
document.addEventListener('mouseup', onUp);
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function handleGA4Event(event) {
|
|
1537
|
+
ga4State.events.push({
|
|
1538
|
+
name: event.name || '?',
|
|
1539
|
+
params: event.params || {},
|
|
1540
|
+
tag: event.tag || 'GA4',
|
|
1541
|
+
source: event.source || '',
|
|
1542
|
+
ts: event.ts || Date.now(),
|
|
1543
|
+
index: ga4State.events.length,
|
|
1544
|
+
});
|
|
1545
|
+
$('ga4Badge').textContent = ga4State.events.length;
|
|
1546
|
+
|
|
1547
|
+
// Append to list (batched via rAF)
|
|
1548
|
+
if (!ga4State._raf) {
|
|
1549
|
+
ga4State._raf = requestAnimationFrame(() => {
|
|
1550
|
+
ga4State._raf = null;
|
|
1551
|
+
renderGA4List();
|
|
1552
|
+
renderGA4Summary();
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function renderGA4List() {
|
|
1558
|
+
const list = $('ga4List');
|
|
1559
|
+
const empty = $('ga4Empty');
|
|
1560
|
+
if (!list) return;
|
|
1561
|
+
|
|
1562
|
+
const { searchFilter, sortDir } = ga4State;
|
|
1563
|
+
let visible = ga4State.events.filter(e =>
|
|
1564
|
+
!searchFilter || e.name.toLowerCase().includes(searchFilter)
|
|
1565
|
+
);
|
|
1566
|
+
|
|
1567
|
+
// Sort: newest first (desc) or oldest first (asc)
|
|
1568
|
+
if (sortDir === 'desc') {
|
|
1569
|
+
visible = [...visible].reverse();
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
empty.style.display = visible.length ? 'none' : 'flex';
|
|
1573
|
+
list.querySelectorAll('.ga4-row').forEach(e => e.remove());
|
|
1574
|
+
|
|
1575
|
+
// Cap at 500 rows
|
|
1576
|
+
const MAX = 500;
|
|
1577
|
+
const toRender = visible.length > MAX ? visible.slice(0, MAX) : visible;
|
|
1578
|
+
|
|
1579
|
+
const frag = document.createDocumentFragment();
|
|
1580
|
+
toRender.forEach(e => {
|
|
1581
|
+
const row = document.createElement('div');
|
|
1582
|
+
row.className = 'ga4-row' + (e.index === ga4State.selected ? ' selected' : '');
|
|
1583
|
+
|
|
1584
|
+
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 });
|
|
1585
|
+
|
|
1586
|
+
row.innerHTML = `
|
|
1587
|
+
<span class="ga4-cell ga4-time">${time}</span>
|
|
1588
|
+
<span class="ga4-cell ga4-name">${esc(e.name)}</span>`;
|
|
1589
|
+
|
|
1590
|
+
row.addEventListener('click', () => {
|
|
1591
|
+
ga4State.selected = e.index;
|
|
1592
|
+
list.querySelectorAll('.ga4-row').forEach(r => r.classList.remove('selected'));
|
|
1593
|
+
row.classList.add('selected');
|
|
1594
|
+
renderGA4Detail(e);
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
// Right-click to copy
|
|
1598
|
+
row.addEventListener('contextmenu', (ev) => {
|
|
1599
|
+
ev.preventDefault();
|
|
1600
|
+
showContextMenu(ev, [
|
|
1601
|
+
{ label: 'Copy Event Name', action: () => navigator.clipboard.writeText(e.name) },
|
|
1602
|
+
{ label: 'Copy as JSON', action: () => navigator.clipboard.writeText(JSON.stringify({ event: e.name, params: e.params }, null, 2)) },
|
|
1603
|
+
]);
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
frag.appendChild(row);
|
|
1607
|
+
});
|
|
1608
|
+
list.appendChild(frag);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function renderGA4Detail(e) {
|
|
1612
|
+
const detail = $('ga4Detail');
|
|
1613
|
+
if (!detail) return;
|
|
1614
|
+
|
|
1615
|
+
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 });
|
|
1616
|
+
|
|
1617
|
+
detail.innerHTML = '';
|
|
1618
|
+
|
|
1619
|
+
// Header info
|
|
1620
|
+
const header = document.createElement('div');
|
|
1621
|
+
header.className = 'ga4-detail-info';
|
|
1622
|
+
header.innerHTML = `
|
|
1623
|
+
<div class="ga4-detail-row"><span class="ga4-detail-key">Event Name</span><span class="ga4-detail-val" style="color:var(--accent);font-weight:600">${esc(e.name)}</span></div>
|
|
1624
|
+
<div class="ga4-detail-row"><span class="ga4-detail-key">Timestamp</span><span class="ga4-detail-val">${time}</span></div>
|
|
1625
|
+
`;
|
|
1626
|
+
detail.appendChild(header);
|
|
1627
|
+
|
|
1628
|
+
// Separator
|
|
1629
|
+
const sep = document.createElement('div');
|
|
1630
|
+
sep.className = 'ga4-detail-sep';
|
|
1631
|
+
detail.appendChild(sep);
|
|
1632
|
+
|
|
1633
|
+
// Parameters as key-value list with collapsible objects
|
|
1634
|
+
if (e.params && typeof e.params === 'object') {
|
|
1635
|
+
const keys = Object.keys(e.params).sort();
|
|
1636
|
+
keys.forEach(key => {
|
|
1637
|
+
const val = e.params[key];
|
|
1638
|
+
const row = document.createElement('div');
|
|
1639
|
+
row.className = 'ga4-param-row';
|
|
1640
|
+
|
|
1641
|
+
const keyEl = document.createElement('span');
|
|
1642
|
+
keyEl.className = 'ga4-param-key';
|
|
1643
|
+
keyEl.textContent = key;
|
|
1644
|
+
row.appendChild(keyEl);
|
|
1645
|
+
|
|
1646
|
+
if (val && typeof val === 'object') {
|
|
1647
|
+
// Collapsible object tree
|
|
1648
|
+
const treeWrap = document.createElement('span');
|
|
1649
|
+
treeWrap.className = 'ga4-param-val';
|
|
1650
|
+
treeWrap.appendChild(createTreeNode(null, val, true));
|
|
1651
|
+
row.appendChild(treeWrap);
|
|
1652
|
+
} else {
|
|
1653
|
+
const valEl = document.createElement('span');
|
|
1654
|
+
valEl.className = 'ga4-param-val';
|
|
1655
|
+
valEl.textContent = val === null ? 'null' : val === undefined ? 'undefined' : JSON.stringify(val);
|
|
1656
|
+
if (typeof val === 'string') valEl.style.color = 'var(--green)';
|
|
1657
|
+
else if (typeof val === 'number') valEl.style.color = 'var(--orange)';
|
|
1658
|
+
else if (typeof val === 'boolean') valEl.style.color = 'var(--accent2)';
|
|
1659
|
+
row.appendChild(valEl);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
detail.appendChild(row);
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// Right-click on detail
|
|
1667
|
+
detail.addEventListener('contextmenu', (ev) => {
|
|
1668
|
+
ev.preventDefault();
|
|
1669
|
+
showContextMenu(ev, [
|
|
1670
|
+
{ label: 'Copy All Parameters', action: () => navigator.clipboard.writeText(JSON.stringify(e.params, null, 2)) },
|
|
1671
|
+
{ label: 'Copy Event JSON', action: () => navigator.clipboard.writeText(JSON.stringify({ event: e.name, params: e.params, timestamp: e.ts }, null, 2)) },
|
|
1672
|
+
]);
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function renderGA4Summary() {
|
|
1677
|
+
const summary = $('ga4Summary');
|
|
1678
|
+
if (!summary) return;
|
|
1679
|
+
|
|
1680
|
+
const counts = {};
|
|
1681
|
+
ga4State.events.forEach(e => {
|
|
1682
|
+
counts[e.name] = (counts[e.name] || 0) + 1;
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
1686
|
+
|
|
1687
|
+
summary.innerHTML = '';
|
|
1688
|
+
|
|
1689
|
+
const totalLabel = document.createElement('span');
|
|
1690
|
+
totalLabel.className = 'ga4-summary-label';
|
|
1691
|
+
totalLabel.textContent = `Total: ${ga4State.events.length}`;
|
|
1692
|
+
summary.appendChild(totalLabel);
|
|
1693
|
+
|
|
1694
|
+
sorted.forEach(([name, count]) => {
|
|
1695
|
+
const chip = document.createElement('span');
|
|
1696
|
+
const isActive = ga4State.searchFilter === name.toLowerCase();
|
|
1697
|
+
chip.className = 'ga4-summary-chip' + (isActive ? ' active' : '');
|
|
1698
|
+
chip.innerHTML = `<b>${esc(name)}</b><span class="chip-count">${count}</span>`;
|
|
1699
|
+
chip.addEventListener('click', () => {
|
|
1700
|
+
const search = $('ga4Search');
|
|
1701
|
+
if (isActive) {
|
|
1702
|
+
// Clear filter
|
|
1703
|
+
ga4State.searchFilter = '';
|
|
1704
|
+
if (search) search.value = '';
|
|
1705
|
+
} else {
|
|
1706
|
+
// Set filter to this event name
|
|
1707
|
+
ga4State.searchFilter = name.toLowerCase();
|
|
1708
|
+
if (search) search.value = name;
|
|
1709
|
+
}
|
|
1710
|
+
renderGA4List();
|
|
1711
|
+
renderGA4Summary();
|
|
1712
|
+
});
|
|
1713
|
+
summary.appendChild(chip);
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1383
1717
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1384
1718
|
// REDUX PANEL
|
|
1385
1719
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1389,8 +1723,9 @@ function initReduxPanel() {
|
|
|
1389
1723
|
<div class="panel-toolbar">
|
|
1390
1724
|
<span class="panel-label">Redux</span>
|
|
1391
1725
|
<span class="badge" id="rBadge">0</span>
|
|
1726
|
+
<input id="reduxSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter actions..." />
|
|
1392
1727
|
<div class="ml-auto" style="display:flex;align-items:center;gap:8px">
|
|
1393
|
-
<
|
|
1728
|
+
<button class="panel-clear-btn" id="reduxClear" title="Clear redux">Clear</button>
|
|
1394
1729
|
<div class="time-travel-bar" style="border:none;padding:0;margin:0">
|
|
1395
1730
|
<button class="tt-btn" onclick="reduxJumpTo(state.redux.selected-1)">◀</button>
|
|
1396
1731
|
<span class="tt-label" id="ttLabel">—/—</span>
|
|
@@ -1410,6 +1745,14 @@ function initReduxPanel() {
|
|
|
1410
1745
|
state.redux.searchFilter = e.target.value.toLowerCase().trim();
|
|
1411
1746
|
renderRedux();
|
|
1412
1747
|
});
|
|
1748
|
+
|
|
1749
|
+
$('reduxClear').addEventListener('click', () => {
|
|
1750
|
+
state.redux.actions = [];
|
|
1751
|
+
state.redux.states = [];
|
|
1752
|
+
state.redux.selected = -1;
|
|
1753
|
+
$('rBadge').textContent = '0';
|
|
1754
|
+
renderRedux();
|
|
1755
|
+
});
|
|
1413
1756
|
}
|
|
1414
1757
|
|
|
1415
1758
|
window.reduxJumpTo = idx => {
|
|
@@ -1880,7 +2223,7 @@ function initSettingsPanel() {
|
|
|
1880
2223
|
<div class="settings-section-title">About</div>
|
|
1881
2224
|
<div class="settings-about">
|
|
1882
2225
|
<div class="about-name" id="aboutAppName">${getStoredAppName()}</div>
|
|
1883
|
-
<div class="about-version">
|
|
2226
|
+
<div class="about-version" id="aboutVersion">v${state._appVersion || '...'}</div>
|
|
1884
2227
|
<div class="about-desc">A standalone macOS debugger for React Native apps.<br/>Supports Hermes, New Architecture, and React Native 0.74+.</div>
|
|
1885
2228
|
<div class="about-links" style="display:flex;gap:16px;justify-content:center">
|
|
1886
2229
|
<span class="about-link" id="linkGithub">GitHub</span>
|
|
@@ -2442,6 +2785,7 @@ function handleMemoryEvent(event) {
|
|
|
2442
2785
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2443
2786
|
initConsolePanel();
|
|
2444
2787
|
initNetworkPanel();
|
|
2788
|
+
initGA4Panel();
|
|
2445
2789
|
initPerformancePanel();
|
|
2446
2790
|
initMemoryPanel();
|
|
2447
2791
|
initReduxPanel();
|