reactoradar 1.2.5 → 1.4.1
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 +401 -70
- package/assets/icon.svg +283 -38
- package/index.html +5 -1
- package/main.js +9 -17
- package/package.json +1 -1
- package/preload.js +1 -1
- 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);
|
|
@@ -285,26 +293,40 @@ function updateDeviceBanner(service, connected) {
|
|
|
285
293
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
286
294
|
// CONSOLE PANEL
|
|
287
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
|
+
|
|
288
308
|
function initConsolePanel() {
|
|
289
309
|
const panel = $('panel-console');
|
|
310
|
+
const levels = getStoredLogLevels();
|
|
311
|
+
state.console.levelFilters = levels;
|
|
312
|
+
|
|
290
313
|
panel.innerHTML = `
|
|
291
314
|
<div class="panel-toolbar">
|
|
292
315
|
<span class="panel-label">Console</span>
|
|
293
316
|
<span class="badge" id="cBadge">0</span>
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
<button class="
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
</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>
|
|
308
330
|
</div>
|
|
309
331
|
</div>
|
|
310
332
|
<div class="scroll-area" id="consoleList">
|
|
@@ -315,25 +337,60 @@ function initConsolePanel() {
|
|
|
315
337
|
</div>
|
|
316
338
|
</div>`;
|
|
317
339
|
|
|
340
|
+
// Search filter
|
|
318
341
|
$('consoleSearch').addEventListener('input', (e) => {
|
|
319
342
|
state.console.searchFilter = e.target.value.toLowerCase().trim();
|
|
320
343
|
renderConsole();
|
|
321
344
|
});
|
|
322
345
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
$('
|
|
327
|
-
|
|
328
|
-
|
|
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();
|
|
329
378
|
});
|
|
330
379
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
}
|
|
337
394
|
|
|
338
395
|
// Console is fed via IPC (network-event handled in IPC section above)
|
|
339
396
|
|
|
@@ -362,12 +419,12 @@ function flushConsoleBatch() {
|
|
|
362
419
|
const empty = $('consoleEmpty');
|
|
363
420
|
if (!list) return;
|
|
364
421
|
|
|
365
|
-
const {
|
|
422
|
+
const { levelFilters, searchFilter } = state.console;
|
|
366
423
|
const frag = document.createDocumentFragment();
|
|
367
424
|
let added = 0;
|
|
368
425
|
|
|
369
426
|
batch.forEach(l => {
|
|
370
|
-
if (
|
|
427
|
+
if (levelFilters && !levelFilters[l.level]) return;
|
|
371
428
|
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return;
|
|
372
429
|
frag.appendChild(buildLogRow(l));
|
|
373
430
|
added++;
|
|
@@ -634,27 +691,23 @@ function buildLogRow(l) {
|
|
|
634
691
|
lvlSpan.textContent = l.level;
|
|
635
692
|
div.appendChild(lvlSpan);
|
|
636
693
|
|
|
637
|
-
//
|
|
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
|
|
638
701
|
const bodyWrap = document.createElement('div');
|
|
639
702
|
bodyWrap.className = 'log-body-wrap';
|
|
640
703
|
|
|
641
|
-
// Single-line preview
|
|
704
|
+
// Single-line preview: message text + caller
|
|
642
705
|
const preview = document.createElement('div');
|
|
643
706
|
preview.className = 'log-preview';
|
|
644
707
|
const msgText = (l.message || '').replace(/\n/g, ' ').slice(0, 200);
|
|
645
708
|
const previewText = document.createElement('span');
|
|
646
709
|
previewText.textContent = msgText + ((l.message || '').length > 200 ? '...' : '');
|
|
647
710
|
preview.appendChild(previewText);
|
|
648
|
-
if (l.caller) {
|
|
649
|
-
// Extract short filename:line from caller like "at Component (file.js:42:10)"
|
|
650
|
-
const callerShort = extractCallerShort(l.caller);
|
|
651
|
-
if (callerShort) {
|
|
652
|
-
const callerTag = document.createElement('span');
|
|
653
|
-
callerTag.className = 'log-caller-inline';
|
|
654
|
-
callerTag.textContent = callerShort;
|
|
655
|
-
preview.appendChild(callerTag);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
711
|
bodyWrap.appendChild(preview);
|
|
659
712
|
|
|
660
713
|
// Full content (hidden by default)
|
|
@@ -662,20 +715,8 @@ function buildLogRow(l) {
|
|
|
662
715
|
full.className = 'log-full';
|
|
663
716
|
full.style.display = 'none';
|
|
664
717
|
full.appendChild(buildLogBody(l));
|
|
665
|
-
if (l.caller) {
|
|
666
|
-
const callerSpan = document.createElement('span');
|
|
667
|
-
callerSpan.className = 'log-caller';
|
|
668
|
-
callerSpan.textContent = l.caller;
|
|
669
|
-
full.appendChild(callerSpan);
|
|
670
|
-
}
|
|
671
718
|
bodyWrap.appendChild(full);
|
|
672
719
|
|
|
673
|
-
// Expand/collapse arrow
|
|
674
|
-
const arrow = document.createElement('span');
|
|
675
|
-
arrow.className = 'log-arrow';
|
|
676
|
-
arrow.textContent = '\u25B6';
|
|
677
|
-
bodyWrap.prepend(arrow);
|
|
678
|
-
|
|
679
720
|
let expanded = false;
|
|
680
721
|
// Only toggle on click, NOT on text selection drag
|
|
681
722
|
let _mouseDownPos = null;
|
|
@@ -774,9 +815,9 @@ function renderConsole() {
|
|
|
774
815
|
const empty = $('consoleEmpty');
|
|
775
816
|
if (!list) return;
|
|
776
817
|
|
|
777
|
-
const {
|
|
818
|
+
const { levelFilters, searchFilter } = state.console;
|
|
778
819
|
const visible = state.console.logs.filter(l => {
|
|
779
|
-
if (
|
|
820
|
+
if (levelFilters && !levelFilters[l.level]) return false;
|
|
780
821
|
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return false;
|
|
781
822
|
return true;
|
|
782
823
|
});
|
|
@@ -805,13 +846,13 @@ function renderConsole() {
|
|
|
805
846
|
// NETWORK PANEL (Chrome DevTools-style)
|
|
806
847
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
807
848
|
const NET_COLS = [
|
|
808
|
-
{ key: 'name', label: 'Name', width:
|
|
849
|
+
{ key: 'name', label: 'Name', width: 380, min: 150 },
|
|
809
850
|
{ key: 'status', label: 'Status', width: 60, min: 40 },
|
|
810
851
|
{ key: 'type', label: 'Type', width: 70, min: 40 },
|
|
811
|
-
{ key: 'initiator', label: 'Initiator', width:
|
|
812
|
-
{ key: 'size', label: 'Size', width:
|
|
813
|
-
{ key: 'time', label: 'Time', width:
|
|
814
|
-
{ 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 },
|
|
815
856
|
];
|
|
816
857
|
|
|
817
858
|
function initNetworkPanel() {
|
|
@@ -821,6 +862,7 @@ function initNetworkPanel() {
|
|
|
821
862
|
<span class="panel-label">Network</span>
|
|
822
863
|
<span class="badge" id="nBadge">0</span>
|
|
823
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>
|
|
824
866
|
<label class="toggle-label" for="netToggle">
|
|
825
867
|
<span class="toggle-text" id="netToggleText">Capture ON</span>
|
|
826
868
|
<input type="checkbox" id="netToggle" class="toggle-input" checked />
|
|
@@ -899,6 +941,16 @@ function initNetworkPanel() {
|
|
|
899
941
|
window.electronAPI?.setNetworkThrottle(state.network.throttle);
|
|
900
942
|
});
|
|
901
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
|
+
|
|
902
954
|
// Close detail button
|
|
903
955
|
$('netDetailClose').addEventListener('click', closeNetDetail);
|
|
904
956
|
|
|
@@ -1016,13 +1068,15 @@ function matchNetType(r, type) {
|
|
|
1016
1068
|
const ct = (r.responseHeaders?.['content-type'] || r.responseHeaders?.['Content-Type'] || '').toLowerCase();
|
|
1017
1069
|
const url = (r.url || '').toLowerCase();
|
|
1018
1070
|
switch (type) {
|
|
1019
|
-
case 'fetch':
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
case '
|
|
1023
|
-
case '
|
|
1024
|
-
case '
|
|
1025
|
-
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);
|
|
1026
1080
|
case 'ws': return url.startsWith('ws://') || url.startsWith('wss://');
|
|
1027
1081
|
default: return true;
|
|
1028
1082
|
}
|
|
@@ -1393,6 +1447,273 @@ function buildCurlCommand(r) {
|
|
|
1393
1447
|
return cmd;
|
|
1394
1448
|
}
|
|
1395
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
|
+
|
|
1396
1717
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1397
1718
|
// REDUX PANEL
|
|
1398
1719
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1402,8 +1723,9 @@ function initReduxPanel() {
|
|
|
1402
1723
|
<div class="panel-toolbar">
|
|
1403
1724
|
<span class="panel-label">Redux</span>
|
|
1404
1725
|
<span class="badge" id="rBadge">0</span>
|
|
1726
|
+
<input id="reduxSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter actions..." />
|
|
1405
1727
|
<div class="ml-auto" style="display:flex;align-items:center;gap:8px">
|
|
1406
|
-
<
|
|
1728
|
+
<button class="panel-clear-btn" id="reduxClear" title="Clear redux">Clear</button>
|
|
1407
1729
|
<div class="time-travel-bar" style="border:none;padding:0;margin:0">
|
|
1408
1730
|
<button class="tt-btn" onclick="reduxJumpTo(state.redux.selected-1)">◀</button>
|
|
1409
1731
|
<span class="tt-label" id="ttLabel">—/—</span>
|
|
@@ -1423,6 +1745,14 @@ function initReduxPanel() {
|
|
|
1423
1745
|
state.redux.searchFilter = e.target.value.toLowerCase().trim();
|
|
1424
1746
|
renderRedux();
|
|
1425
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
|
+
});
|
|
1426
1756
|
}
|
|
1427
1757
|
|
|
1428
1758
|
window.reduxJumpTo = idx => {
|
|
@@ -2455,6 +2785,7 @@ function handleMemoryEvent(event) {
|
|
|
2455
2785
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2456
2786
|
initConsolePanel();
|
|
2457
2787
|
initNetworkPanel();
|
|
2788
|
+
initGA4Panel();
|
|
2458
2789
|
initPerformancePanel();
|
|
2459
2790
|
initMemoryPanel();
|
|
2460
2791
|
initReduxPanel();
|