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.
@@ -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,GACjF,iBAAiB,CAwCnB"}
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(String(device.platform).toUpperCase());
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>Device</strong>' + escapeHtml(deviceLog.deviceId) + '</div>';
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 || 'unknown').toUpperCase()) + '</span>';
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 &nbsp; <span class="kbd">j</span><span class="kbd">k</span> navigate &nbsp; <span class="kbd">Enter</span> expand &nbsp; <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">&#9112;</button></div>';
1263
1311
  html += '<div class="log-expand">' + (isExpanded ? '&#9654;' : '&#9654;') + '</div>';
1264
1312
  html += '</div>';
1265
- html += '<div class="log-detail' + (isExpanded ? '' : '') + '" id="detail-' + rowId + '">';
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 + '\')">&#9112; 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
  });
@@ -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
- return [
36
- slugPart(device.platform),
37
- slugPart(device.model),
38
- slugPart(source && source.ip),
39
- ].join('_');
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
- deviceLog.report.logs[type].push(...entries);
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",
4
- "description": "A local-first React Native debugging bridge with in-app logs, desktop daemon, Web Console, HTTP API, and MCP support",
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
- let c = `curl -X ${log.request.method} '${log.request.url}'`;
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
- c += ` \\\n -d '${typeof log.request.body === 'string' ? log.request.body : JSON.stringify(log.request.body)}'`;
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
  }