react-native-debug-toolkit 3.1.4 → 3.1.5
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 +82 -66
- package/README.zh-CN.md +81 -65
- package/bin/debug-toolkit.js +10 -2
- package/lib/commonjs/features/network/NetworkLogTab.js +7 -3
- package/lib/commonjs/features/network/NetworkLogTab.js.map +1 -1
- package/lib/commonjs/utils/DaemonClient.js +25 -1
- package/lib/commonjs/utils/DaemonClient.js.map +1 -1
- package/lib/commonjs/utils/deviceReport.js +1 -0
- package/lib/commonjs/utils/deviceReport.js.map +1 -1
- package/lib/module/features/network/NetworkLogTab.js +7 -3
- package/lib/module/features/network/NetworkLogTab.js.map +1 -1
- package/lib/module/utils/DaemonClient.js +25 -1
- package/lib/module/utils/DaemonClient.js.map +1 -1
- package/lib/module/utils/deviceReport.js +1 -0
- package/lib/module/utils/deviceReport.js.map +1 -1
- package/lib/typescript/src/features/network/NetworkLogTab.d.ts.map +1 -1
- package/lib/typescript/src/utils/DaemonClient.d.ts +1 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -1
- package/lib/typescript/src/utils/deviceReport.d.ts +6 -0
- package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -1
- package/node/daemon/src/console/console.html +103 -12
- package/node/daemon/src/store.js +45 -6
- package/package.json +6 -2
- package/src/features/network/NetworkLogTab.tsx +6 -3
- package/src/utils/DaemonClient.ts +26 -1
- package/src/utils/deviceReport.ts +8 -1
|
@@ -10,12 +10,18 @@ export interface DeviceInfo {
|
|
|
10
10
|
osVersion: string;
|
|
11
11
|
appVersion: string;
|
|
12
12
|
}
|
|
13
|
+
export interface SessionInfo {
|
|
14
|
+
id: string;
|
|
15
|
+
startedAt: number;
|
|
16
|
+
}
|
|
13
17
|
export interface DebugDeviceReport {
|
|
14
18
|
version: 2;
|
|
15
19
|
device: DeviceInfo;
|
|
20
|
+
session?: SessionInfo;
|
|
16
21
|
logs: Record<string, unknown[] | undefined>;
|
|
17
22
|
}
|
|
18
23
|
export declare function createDebugDeviceReport(options?: DebugDeviceReportOptions & {
|
|
19
24
|
featureProvider?: FeatureDataProvider;
|
|
25
|
+
session?: SessionInfo;
|
|
20
26
|
}): DebugDeviceReport;
|
|
21
27
|
//# sourceMappingURL=deviceReport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deviceReport.d.ts","sourceRoot":"","sources":["../../../../src/utils/deviceReport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAOpD,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC;CAC7C;AAuID,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,wBAAwB,GAAG;IAAE,eAAe,CAAC,EAAE,mBAAmB,CAAA;CAAO,
|
|
1
|
+
{"version":3,"file":"deviceReport.d.ts","sourceRoot":"","sources":["../../../../src/utils/deviceReport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAOpD,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC,CAAC;CAC7C;AAuID,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,wBAAwB,GAAG;IAAE,eAAe,CAAC,EAAE,mBAAmB,CAAC;IAAC,OAAO,CAAC,EAAE,WAAW,CAAA;CAAO,GACxG,iBAAiB,CAyCnB"}
|
|
@@ -472,6 +472,23 @@ header h1 span{color:var(--text3);font-weight:400}
|
|
|
472
472
|
.nav-arrow{color:var(--cyan);font-size:14px}
|
|
473
473
|
.nav-to{color:var(--text);font-weight:500}
|
|
474
474
|
|
|
475
|
+
/* Session divider */
|
|
476
|
+
.session-divider{
|
|
477
|
+
display:flex;align-items:center;gap:12px;
|
|
478
|
+
padding:10px 0;margin:6px 0;
|
|
479
|
+
}
|
|
480
|
+
.session-divider-line{flex:1;height:1px;background:var(--border2)}
|
|
481
|
+
.session-divider-label{
|
|
482
|
+
font-family:var(--font-mono);font-size:10px;
|
|
483
|
+
color:var(--text3);white-space:nowrap;letter-spacing:.04em;
|
|
484
|
+
display:flex;align-items:center;gap:6px;
|
|
485
|
+
}
|
|
486
|
+
.session-divider-dot{
|
|
487
|
+
width:5px;height:5px;border-radius:50%;
|
|
488
|
+
background:var(--amber);flex-shrink:0;
|
|
489
|
+
}
|
|
490
|
+
.session-new .session-divider-dot{background:var(--cyan)}
|
|
491
|
+
|
|
475
492
|
/* Entry footer */
|
|
476
493
|
.entry-footer{
|
|
477
494
|
display:flex;align-items:center;gap:8px;
|
|
@@ -691,11 +708,18 @@ mark{
|
|
|
691
708
|
return labels[type] || (type ? type.charAt(0).toUpperCase() + type.slice(1) : 'Unknown');
|
|
692
709
|
}
|
|
693
710
|
|
|
711
|
+
function capitalizePlatform(p) {
|
|
712
|
+
if (p === 'ios') return 'iOS';
|
|
713
|
+
if (p === 'android') return 'Android';
|
|
714
|
+
var s = String(p || 'unknown');
|
|
715
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
716
|
+
}
|
|
717
|
+
|
|
694
718
|
function formatDevice(device) {
|
|
695
719
|
if (!device || typeof device !== 'object') return 'Unknown device';
|
|
696
720
|
var parts = [];
|
|
697
|
-
if (device.platform) parts.push(
|
|
698
|
-
if (device.model) parts.push(String(device.model));
|
|
721
|
+
if (device.platform) parts.push(capitalizePlatform(device.platform));
|
|
722
|
+
if (device.model && String(device.model) !== 'unknown') parts.push(String(device.model));
|
|
699
723
|
if (device.osVersion) parts.push('OS ' + String(device.osVersion));
|
|
700
724
|
return parts.length ? parts.join(' / ') : 'Unknown device';
|
|
701
725
|
}
|
|
@@ -937,9 +961,37 @@ mark{
|
|
|
937
961
|
html += renderCollapsibleSection('Response Headers', resHeadersContent, response.headers || null, true);
|
|
938
962
|
}
|
|
939
963
|
|
|
964
|
+
// cURL command
|
|
965
|
+
var curlStr = buildCurlCommand(entry);
|
|
966
|
+
if (curlStr) {
|
|
967
|
+
var curlContent = '<pre class="json-block" style="max-height:none;font-size:11px;line-height:1.6">' + escapeHtml(curlStr) + '</pre>';
|
|
968
|
+
html += renderCollapsibleSection('cURL', curlContent, curlStr, true);
|
|
969
|
+
}
|
|
970
|
+
|
|
940
971
|
return html;
|
|
941
972
|
}
|
|
942
973
|
|
|
974
|
+
// Keep in sync with src/features/network/NetworkLogTab.tsx buildCurl()
|
|
975
|
+
function buildCurlCommand(entry) {
|
|
976
|
+
var request = readObject(entry.request) || entry;
|
|
977
|
+
if (!request.url) return '';
|
|
978
|
+
var method = (request.method || 'GET').toUpperCase();
|
|
979
|
+
function q(s) { return String(s).replace(/'/g, "'\\''"); }
|
|
980
|
+
var parts = ["curl -X " + method + " '" + q(request.url) + "'"];
|
|
981
|
+
var headers = request.headers;
|
|
982
|
+
if (headers && typeof headers === 'object') {
|
|
983
|
+
Object.keys(headers).forEach(function(k) {
|
|
984
|
+
parts.push(" -H '" + q(k) + ": " + q(headers[k]) + "'");
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
var body = request.body;
|
|
988
|
+
if (body != null) {
|
|
989
|
+
var bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
|
990
|
+
parts.push(" -d '" + q(bodyStr) + "'");
|
|
991
|
+
}
|
|
992
|
+
return parts.join(' \\\n');
|
|
993
|
+
}
|
|
994
|
+
|
|
943
995
|
function renderNavigationDetails(entry) {
|
|
944
996
|
var hero = '<div class="nav-hero">';
|
|
945
997
|
if (entry.from || entry.path) hero += '<span class="nav-from">' + escapeHtml(entry.from || entry.path || '-') + '</span>';
|
|
@@ -1060,7 +1112,7 @@ mark{
|
|
|
1060
1112
|
html += '<div><div class="device-title">' + escapeHtml(deviceText) + '</div>';
|
|
1061
1113
|
html += '<div class="device-subtitle">IP ' + escapeHtml(ipText) + '</div></div>';
|
|
1062
1114
|
html += '<div class="device-meta-group">';
|
|
1063
|
-
html += '<div class="device-meta-line"><strong>
|
|
1115
|
+
html += '<div class="device-meta-line"><strong>ID</strong>' + escapeHtml(deviceLog.deviceId) + '</div>';
|
|
1064
1116
|
html += '<div class="device-meta-line"><strong>Last seen</strong>' + formatTime(deviceLog.lastSeenAt || deviceLog.receivedAt) + '</div>';
|
|
1065
1117
|
html += '</div>';
|
|
1066
1118
|
html += '<div class="device-tags">' + renderDeviceTags(lc) + '</div>';
|
|
@@ -1121,7 +1173,7 @@ mark{
|
|
|
1121
1173
|
if (device && typeof device === 'object') {
|
|
1122
1174
|
html += '<div class="device-info">';
|
|
1123
1175
|
var pClass = device.platform === 'ios' ? 'platform-ios' : device.platform === 'android' ? 'platform-android' : '';
|
|
1124
|
-
html += '<span class="device-badge ' + pClass + '">' + escapeHtml((device.platform
|
|
1176
|
+
html += '<span class="device-badge ' + pClass + '">' + escapeHtml(capitalizePlatform(device.platform)) + '</span>';
|
|
1125
1177
|
if (device.model) html += '<span class="device-badge">' + escapeHtml(device.model) + '</span>';
|
|
1126
1178
|
if (device.osVersion) html += '<span class="device-badge">OS ' + escapeHtml(device.osVersion) + '</span>';
|
|
1127
1179
|
if (device.appVersion) html += '<span class="device-badge">v' + escapeHtml(device.appVersion) + '</span>';
|
|
@@ -1168,10 +1220,6 @@ mark{
|
|
|
1168
1220
|
|
|
1169
1221
|
// Actions
|
|
1170
1222
|
html += '<div class="actions">';
|
|
1171
|
-
html += '<button class="btn" onclick="copyJSON()">';
|
|
1172
|
-
html += '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
1173
|
-
html += 'Copy JSON</button>';
|
|
1174
|
-
html += '<span style="flex:1"></span>';
|
|
1175
1223
|
html += '<span style="font-size:10px;color:var(--text3);font-family:var(--font-mono)"><span class="kbd">/</span> search <span class="kbd">j</span><span class="kbd">k</span> navigate <span class="kbd">Enter</span> expand <span class="kbd">Esc</span> back</span>';
|
|
1176
1224
|
html += '</div>';
|
|
1177
1225
|
|
|
@@ -1262,12 +1310,9 @@ mark{
|
|
|
1262
1310
|
html += '<div class="log-copy" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')"><button class="copy-btn" title="Copy entry JSON">⎘</button></div>';
|
|
1263
1311
|
html += '<div class="log-expand">' + (isExpanded ? '▶' : '▶') + '</div>';
|
|
1264
1312
|
html += '</div>';
|
|
1265
|
-
html += '<div class="log-detail
|
|
1313
|
+
html += '<div class="log-detail" id="detail-' + rowId + '">';
|
|
1266
1314
|
html += '<div class="log-detail-inner"><div class="detail-sections">';
|
|
1267
1315
|
html += renderLogDetails(entry, lt);
|
|
1268
|
-
html += '<div class="entry-footer">';
|
|
1269
|
-
html += '<button class="btn btn-sm" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')">⎘ Copy JSON</button>';
|
|
1270
|
-
html += '</div>';
|
|
1271
1316
|
html += '</div></div></div>';
|
|
1272
1317
|
html += '</div>';
|
|
1273
1318
|
return html;
|
|
@@ -1292,8 +1337,24 @@ mark{
|
|
|
1292
1337
|
}
|
|
1293
1338
|
|
|
1294
1339
|
focusedIndex = -1;
|
|
1340
|
+
var currentSessionId = null;
|
|
1341
|
+
var knownSessionIds = new Set();
|
|
1342
|
+
if (currentDevice && currentDevice.session) {
|
|
1343
|
+
knownSessionIds.add(currentDevice.session.id);
|
|
1344
|
+
}
|
|
1295
1345
|
var html = '<div class="log-list">';
|
|
1296
1346
|
entries.forEach(function(item, i) {
|
|
1347
|
+
var entrySessionId = item.entry && typeof item.entry === 'object' ? item.entry.sessionId : null;
|
|
1348
|
+
if (entrySessionId && entrySessionId !== currentSessionId) {
|
|
1349
|
+
var isNewSession = knownSessionIds.has(entrySessionId);
|
|
1350
|
+
var label = isNewSession ? 'Current session' : ('Session ' + entrySessionId.slice(0, 8));
|
|
1351
|
+
html += '<div class="session-divider' + (isNewSession ? ' session-new' : '') + '">';
|
|
1352
|
+
html += '<div class="session-divider-line"></div>';
|
|
1353
|
+
html += '<span class="session-divider-label"><span class="session-divider-dot"></span>' + label + '</span>';
|
|
1354
|
+
html += '<div class="session-divider-line"></div>';
|
|
1355
|
+
html += '</div>';
|
|
1356
|
+
currentSessionId = entrySessionId;
|
|
1357
|
+
}
|
|
1297
1358
|
var absoluteIndex = startIndex + i;
|
|
1298
1359
|
var rowId = getLogEntryKey(item.entry, item.type, absoluteIndex);
|
|
1299
1360
|
html += renderLogEntryHtml(item.entry, item.type, rowId, i, expandedRows[rowId]);
|
|
@@ -1670,7 +1731,37 @@ mark{
|
|
|
1670
1731
|
}
|
|
1671
1732
|
|
|
1672
1733
|
var html = '';
|
|
1734
|
+
var liveCurrentSessionId = null;
|
|
1735
|
+
var liveKnownSessionIds = new Set();
|
|
1736
|
+
if (currentDevice && currentDevice.session) {
|
|
1737
|
+
liveKnownSessionIds.add(currentDevice.session.id);
|
|
1738
|
+
}
|
|
1739
|
+
// find existing sessionId from first entry in list for divider continuity
|
|
1740
|
+
var existingFirst = list.querySelector('.log-entry');
|
|
1741
|
+
if (existingFirst) {
|
|
1742
|
+
var existingIndex = parseInt(existingFirst.getAttribute('data-index'), 10);
|
|
1743
|
+
var existingEntries = collectLogEntries(
|
|
1744
|
+
currentDevice.report ? currentDevice.report.logs : {},
|
|
1745
|
+
readVisibleLogOptions().type,
|
|
1746
|
+
readVisibleLogOptions().failedOnly
|
|
1747
|
+
);
|
|
1748
|
+
var existingSlice = existingEntries.slice(0, PAGE_SIZE);
|
|
1749
|
+
if (existingSlice.length > 0 && existingSlice[0].entry && typeof existingSlice[0].entry === 'object') {
|
|
1750
|
+
liveCurrentSessionId = existingSlice[0].entry.sessionId || null;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1673
1753
|
items.forEach(function(item) {
|
|
1754
|
+
var entrySessionId = item.entry && typeof item.entry === 'object' ? item.entry.sessionId : null;
|
|
1755
|
+
if (entrySessionId && entrySessionId !== liveCurrentSessionId) {
|
|
1756
|
+
var isNewSession = liveKnownSessionIds.has(entrySessionId);
|
|
1757
|
+
var label = isNewSession ? 'Current session' : ('Session ' + entrySessionId.slice(0, 8));
|
|
1758
|
+
html += '<div class="session-divider' + (isNewSession ? ' session-new' : '') + '">';
|
|
1759
|
+
html += '<div class="session-divider-line"></div>';
|
|
1760
|
+
html += '<span class="session-divider-label"><span class="session-divider-dot"></span>' + label + '</span>';
|
|
1761
|
+
html += '<div class="session-divider-line"></div>';
|
|
1762
|
+
html += '</div>';
|
|
1763
|
+
liveCurrentSessionId = entrySessionId;
|
|
1764
|
+
}
|
|
1674
1765
|
var rowId = getLogEntryKey(item.entry, item.type, 'live-' + (liveSequence += 1));
|
|
1675
1766
|
html += renderLogEntryHtml(item.entry, item.type, rowId, 0, false);
|
|
1676
1767
|
});
|
package/node/daemon/src/store.js
CHANGED
|
@@ -28,15 +28,33 @@ function slugPart(value) {
|
|
|
28
28
|
.slice(0, 80) || 'unknown';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function ipTail(ip) {
|
|
32
|
+
if (!ip || typeof ip !== 'string') return '0';
|
|
33
|
+
const parts = ip.split('.');
|
|
34
|
+
return parts.length >= 2 ? parts[parts.length - 1] : slugPart(ip);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSimulatorIp(ip) {
|
|
38
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '10.0.2.2' || ip === 'localhost';
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
function createDeviceId(report, source) {
|
|
32
42
|
const device = report && typeof report === 'object' && report.device && typeof report.device === 'object'
|
|
33
43
|
? report.device
|
|
34
44
|
: {};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
const platform = slugPart(device.platform);
|
|
46
|
+
const ip = source && source.ip ? String(source.ip) : '';
|
|
47
|
+
const sim = isSimulatorIp(ip);
|
|
48
|
+
let model = slugPart(device.model);
|
|
49
|
+
if (model === 'unknown' && platform !== 'unknown') {
|
|
50
|
+
model = sim ? 'sim' : 'device';
|
|
51
|
+
}
|
|
52
|
+
const ver = device.appVersion ? slugPart(device.appVersion) : '';
|
|
53
|
+
const tail = sim ? 'sim' : ipTail(ip);
|
|
54
|
+
const parts = [platform, model];
|
|
55
|
+
if (ver && ver !== 'unknown') parts.push(ver);
|
|
56
|
+
parts.push(tail);
|
|
57
|
+
return parts.join('_');
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
function readPersistedDevices(storagePath, maxDevices) {
|
|
@@ -92,6 +110,18 @@ function createMemoryStore(options = {}) {
|
|
|
92
110
|
const deviceId = createDeviceId(report, source);
|
|
93
111
|
const existingIndex = devices.findIndex((item) => item.deviceId === deviceId);
|
|
94
112
|
const existing = existingIndex >= 0 ? devices[existingIndex] : null;
|
|
113
|
+
const reportSessionId = report.session ? report.session.id : null;
|
|
114
|
+
if (reportSessionId && report.logs) {
|
|
115
|
+
Object.entries(report.logs).forEach(function(pair) {
|
|
116
|
+
if (!Array.isArray(pair[1])) return;
|
|
117
|
+
report.logs[pair[0]] = pair[1].map(function(entry) {
|
|
118
|
+
if (entry && typeof entry === 'object' && !entry.sessionId) {
|
|
119
|
+
return Object.assign({}, entry, { sessionId: reportSessionId });
|
|
120
|
+
}
|
|
121
|
+
return entry;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
95
125
|
const deviceLog = {
|
|
96
126
|
deviceId,
|
|
97
127
|
firstSeenAt: existing ? existing.firstSeenAt : receivedAt,
|
|
@@ -99,6 +129,7 @@ function createMemoryStore(options = {}) {
|
|
|
99
129
|
receivedAt,
|
|
100
130
|
source,
|
|
101
131
|
device: report.device || null,
|
|
132
|
+
session: report.session || null,
|
|
102
133
|
report,
|
|
103
134
|
logCount: createLogCount(report),
|
|
104
135
|
};
|
|
@@ -124,6 +155,7 @@ function createMemoryStore(options = {}) {
|
|
|
124
155
|
}
|
|
125
156
|
|
|
126
157
|
const deltaLogs = (delta && delta.logs) || {};
|
|
158
|
+
const currentSessionId = deviceLog.session ? deviceLog.session.id : null;
|
|
127
159
|
Object.entries(deltaLogs).forEach(([type, entries]) => {
|
|
128
160
|
if (!Array.isArray(entries)) {
|
|
129
161
|
return;
|
|
@@ -131,7 +163,13 @@ function createMemoryStore(options = {}) {
|
|
|
131
163
|
if (!deviceLog.report.logs[type]) {
|
|
132
164
|
deviceLog.report.logs[type] = [];
|
|
133
165
|
}
|
|
134
|
-
|
|
166
|
+
const tagged = entries.map(function(entry) {
|
|
167
|
+
if (entry && typeof entry === 'object' && currentSessionId && !entry.sessionId) {
|
|
168
|
+
return Object.assign({}, entry, { sessionId: currentSessionId });
|
|
169
|
+
}
|
|
170
|
+
return entry;
|
|
171
|
+
});
|
|
172
|
+
deviceLog.report.logs[type].push(...tagged);
|
|
135
173
|
});
|
|
136
174
|
|
|
137
175
|
deviceLog.lastSeenAt = new Date(Date.now()).toISOString();
|
|
@@ -153,6 +191,7 @@ function createMemoryStore(options = {}) {
|
|
|
153
191
|
receivedAt: deviceLog.receivedAt,
|
|
154
192
|
device: deviceLog.device || null,
|
|
155
193
|
source: deviceLog.source || null,
|
|
194
|
+
session: deviceLog.session || null,
|
|
156
195
|
logCount: deviceLog.logCount,
|
|
157
196
|
}));
|
|
158
197
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-debug-toolkit",
|
|
3
|
-
"version": "3.1.
|
|
4
|
-
"description": "A local-first React Native
|
|
3
|
+
"version": "3.1.5",
|
|
4
|
+
"description": "A local-first React Native debug toolkit with Web Console, HTTP API, and MCP support for AI-readable app logs",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
7
7
|
"types": "lib/typescript/src/index.d.ts",
|
|
@@ -30,8 +30,12 @@
|
|
|
30
30
|
"keywords": [
|
|
31
31
|
"react-native",
|
|
32
32
|
"debug",
|
|
33
|
+
"log-bridge",
|
|
34
|
+
"web-console",
|
|
35
|
+
"ai-debugging",
|
|
33
36
|
"toolkit",
|
|
34
37
|
"http-inspector",
|
|
38
|
+
"mcp",
|
|
35
39
|
"development-tools",
|
|
36
40
|
"floating-panel"
|
|
37
41
|
],
|
|
@@ -26,13 +26,16 @@ const formatSize = (data: unknown): string => {
|
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
// Keep in sync with console.html buildCurlCommand()
|
|
29
30
|
const buildCurl = (log: NetworkLogEntry): string => {
|
|
30
|
-
|
|
31
|
+
const q = (s: string) => s.replace(/'/g, "'\\''");
|
|
32
|
+
let c = `curl -X ${log.request.method} '${q(log.request.url)}'`;
|
|
31
33
|
if (log.request.headers) {
|
|
32
|
-
Object.entries(log.request.headers).forEach(([k, v]) => (c += ` \\\n -H '${k}: ${v}'`));
|
|
34
|
+
Object.entries(log.request.headers).forEach(([k, v]) => (c += ` \\\n -H '${q(k)}: ${q(String(v))}'`));
|
|
33
35
|
}
|
|
34
36
|
if (log.request.body) {
|
|
35
|
-
|
|
37
|
+
const bodyStr = typeof log.request.body === 'string' ? log.request.body : JSON.stringify(log.request.body);
|
|
38
|
+
c += ` \\\n -d '${q(bodyStr)}'`;
|
|
36
39
|
}
|
|
37
40
|
return c;
|
|
38
41
|
};
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createDebugDeviceReport,
|
|
7
7
|
type DebugDeviceReport,
|
|
8
8
|
type DebugDeviceReportOptions,
|
|
9
|
+
type SessionInfo,
|
|
9
10
|
} from './deviceReport';
|
|
10
11
|
import { safeStringify } from './safeStringify';
|
|
11
12
|
|
|
@@ -147,6 +148,7 @@ interface StreamState {
|
|
|
147
148
|
debounceMs: number;
|
|
148
149
|
timeoutMs: number;
|
|
149
150
|
deviceId: string | null;
|
|
151
|
+
session: SessionInfo;
|
|
150
152
|
sending: boolean;
|
|
151
153
|
debounceTimer: ReturnType<typeof setTimeout> | null;
|
|
152
154
|
retryTimer: ReturnType<typeof setTimeout> | null;
|
|
@@ -183,6 +185,7 @@ export class DaemonClient {
|
|
|
183
185
|
private _featureProvider: FeatureDataProvider;
|
|
184
186
|
private _onEndpointDetected: ((url: string) => void) | undefined;
|
|
185
187
|
private _restorePromise: Promise<void> | null = null;
|
|
188
|
+
private _sessionId: SessionInfo | null = null;
|
|
186
189
|
|
|
187
190
|
constructor(options: DaemonClientOptions) {
|
|
188
191
|
this._fetch = options.fetch;
|
|
@@ -264,6 +267,10 @@ export class DaemonClient {
|
|
|
264
267
|
connect(options: StreamToDaemonOptions = {}): void {
|
|
265
268
|
if (this._stream) return;
|
|
266
269
|
|
|
270
|
+
if (!this._sessionId) {
|
|
271
|
+
this._sessionId = { id: generateSessionId(), startedAt: Date.now() };
|
|
272
|
+
}
|
|
273
|
+
|
|
267
274
|
const endpoint = options.endpoint || this.resolveEndpoint();
|
|
268
275
|
const reportUrl = buildDaemonUrl(endpoint, '/report');
|
|
269
276
|
const ingestUrl = buildDaemonUrl(endpoint, '/ingest');
|
|
@@ -280,6 +287,7 @@ export class DaemonClient {
|
|
|
280
287
|
debounceMs: options.debounceMs || DEFAULT_DEBOUNCE_MS,
|
|
281
288
|
timeoutMs: Math.max(0, options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
282
289
|
deviceId: null,
|
|
290
|
+
session: this._sessionId,
|
|
283
291
|
sending: false,
|
|
284
292
|
debounceTimer: null,
|
|
285
293
|
retryTimer: null,
|
|
@@ -315,6 +323,7 @@ export class DaemonClient {
|
|
|
315
323
|
if (!this._stream) return;
|
|
316
324
|
const state = this._stream;
|
|
317
325
|
this._stream = null;
|
|
326
|
+
this._sessionId = null;
|
|
318
327
|
|
|
319
328
|
if (state.debounceTimer) clearTimeout(state.debounceTimer);
|
|
320
329
|
if (state.retryTimer) clearTimeout(state.retryTimer);
|
|
@@ -458,6 +467,7 @@ export class DaemonClient {
|
|
|
458
467
|
this._settings = { mode: 'simulator', endpoint: '', deviceHost: '', token: '' };
|
|
459
468
|
this._streamingEnabled = null;
|
|
460
469
|
this._restorePromise = null;
|
|
470
|
+
this._sessionId = null;
|
|
461
471
|
}
|
|
462
472
|
|
|
463
473
|
// ---- Private: Transport ----
|
|
@@ -629,7 +639,7 @@ export class DaemonClient {
|
|
|
629
639
|
}
|
|
630
640
|
|
|
631
641
|
private async doSendFullReport(state: StreamState): Promise<SendResult> {
|
|
632
|
-
const report = createDebugDeviceReport({ featureProvider: this._featureProvider });
|
|
642
|
+
const report = createDebugDeviceReport({ featureProvider: this._featureProvider, session: state.session });
|
|
633
643
|
const response = await this.doPost(
|
|
634
644
|
state.reportUrl,
|
|
635
645
|
this.fetchHeaders(state),
|
|
@@ -843,3 +853,18 @@ function readLogCount(value: unknown): Record<string, number> | undefined {
|
|
|
843
853
|
export function _resetDaemonClientForTesting(): void {
|
|
844
854
|
daemonClient._resetForTesting();
|
|
845
855
|
}
|
|
856
|
+
|
|
857
|
+
function generateSessionId(): string {
|
|
858
|
+
try {
|
|
859
|
+
return (globalThis as { crypto?: { randomUUID?: () => string } }).crypto?.randomUUID?.() ?? fallbackSessionId();
|
|
860
|
+
} catch {
|
|
861
|
+
return fallbackSessionId();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function fallbackSessionId(): string {
|
|
866
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
867
|
+
const r = Math.random() * 16 | 0;
|
|
868
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
@@ -21,9 +21,15 @@ export interface DeviceInfo {
|
|
|
21
21
|
appVersion: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface SessionInfo {
|
|
25
|
+
id: string;
|
|
26
|
+
startedAt: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
export interface DebugDeviceReport {
|
|
25
30
|
version: 2;
|
|
26
31
|
device: DeviceInfo;
|
|
32
|
+
session?: SessionInfo;
|
|
27
33
|
logs: Record<string, unknown[] | undefined>;
|
|
28
34
|
}
|
|
29
35
|
|
|
@@ -161,7 +167,7 @@ function sanitizeValue(
|
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
export function createDebugDeviceReport(
|
|
164
|
-
options: DebugDeviceReportOptions & { featureProvider?: FeatureDataProvider } = {},
|
|
170
|
+
options: DebugDeviceReportOptions & { featureProvider?: FeatureDataProvider; session?: SessionInfo } = {},
|
|
165
171
|
): DebugDeviceReport {
|
|
166
172
|
const provider = options.featureProvider ?? debugToolkit;
|
|
167
173
|
const maxPerType = Math.max(1, Math.floor(options.maxPerType ?? DEFAULT_MAX_PER_TYPE));
|
|
@@ -200,6 +206,7 @@ export function createDebugDeviceReport(
|
|
|
200
206
|
osVersion: Platform.Version == null ? 'unknown' : String(Platform.Version),
|
|
201
207
|
appVersion: (constants?.appVersion as string) || 'unknown',
|
|
202
208
|
},
|
|
209
|
+
session: options.session,
|
|
203
210
|
logs,
|
|
204
211
|
};
|
|
205
212
|
}
|