react-native-inapp-inspector 2.2.2 → 2.2.3

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.
Files changed (97) hide show
  1. package/dist/commonjs/constants/version.d.ts +1 -1
  2. package/dist/commonjs/constants/version.js +1 -1
  3. package/dist/esm/constants/version.d.ts +1 -1
  4. package/dist/esm/constants/version.js +1 -1
  5. package/package.json +2 -1
  6. package/src/analytics.ts +35 -0
  7. package/src/bundle.ts +25 -0
  8. package/src/components/AnalyticsDetail.tsx +865 -0
  9. package/src/components/AnalyticsEventCard.tsx +441 -0
  10. package/src/components/AnalyticsGraph.tsx +583 -0
  11. package/src/components/AnimatedEntrance.tsx +64 -0
  12. package/src/components/AppHeaderLogo.tsx +109 -0
  13. package/src/components/BrandCircleIcon.tsx +144 -0
  14. package/src/components/BrandSquareIcon.tsx +144 -0
  15. package/src/components/CodeSnippet.tsx +725 -0
  16. package/src/components/ConsoleLogCard.tsx +628 -0
  17. package/src/components/CopyButton.tsx +87 -0
  18. package/src/components/DiffViewer.tsx +82 -0
  19. package/src/components/DomainHeader.tsx +237 -0
  20. package/src/components/EmptyState.tsx +73 -0
  21. package/src/components/EndOfListFooter.tsx +100 -0
  22. package/src/components/ErrorBoundary.tsx +688 -0
  23. package/src/components/HeadersSection.tsx +192 -0
  24. package/src/components/HighlightText.tsx +100 -0
  25. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  26. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  27. package/src/components/Inspector/BundleTab.tsx +4731 -0
  28. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  29. package/src/components/Inspector/CrashDetail.tsx +941 -0
  30. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  31. package/src/components/Inspector/CrashTab.tsx +871 -0
  32. package/src/components/Inspector/FabLauncher.tsx +80 -0
  33. package/src/components/Inspector/InspectorContext.tsx +28 -0
  34. package/src/components/Inspector/InspectorHeader.tsx +973 -0
  35. package/src/components/Inspector/LogDetail.tsx +1427 -0
  36. package/src/components/Inspector/MainScreen.tsx +258 -0
  37. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  38. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  39. package/src/components/Inspector/NetworkTab.tsx +1095 -0
  40. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  41. package/src/components/Inspector/PerformanceTab.tsx +1894 -0
  42. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  43. package/src/components/Inspector/ReduxTab.tsx +954 -0
  44. package/src/components/Inspector/SettingsPanel.tsx +3181 -0
  45. package/src/components/Inspector/TabBar.tsx +187 -0
  46. package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
  47. package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
  48. package/src/components/JsonViewer.tsx +486 -0
  49. package/src/components/LogCard.tsx +491 -0
  50. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  51. package/src/components/MetaAccordion.tsx +340 -0
  52. package/src/components/MiniBarChart.tsx +42 -0
  53. package/src/components/MiniLineChart.tsx +33 -0
  54. package/src/components/NetworkIcons.tsx +2118 -0
  55. package/src/components/SectionHeader.tsx +113 -0
  56. package/src/components/SegmentedTabs.tsx +83 -0
  57. package/src/components/Slider.tsx +300 -0
  58. package/src/components/SourcePageCard.tsx +148 -0
  59. package/src/components/Toast.tsx +131 -0
  60. package/src/components/TouchableScale.tsx +91 -0
  61. package/src/components/TreeNode.tsx +186 -0
  62. package/src/console.ts +24 -0
  63. package/src/constants/index.ts +38 -0
  64. package/src/constants/version.ts +3 -0
  65. package/src/crash.ts +32 -0
  66. package/src/customHooks/analyticsLogger.ts +336 -0
  67. package/src/customHooks/bundleAnalyzer.ts +1231 -0
  68. package/src/customHooks/consoleLogger.ts +497 -0
  69. package/src/customHooks/crashHandler.ts +944 -0
  70. package/src/customHooks/logFilters.ts +32 -0
  71. package/src/customHooks/networkLogger.ts +419 -0
  72. package/src/customHooks/performanceTracker.ts +1014 -0
  73. package/src/customHooks/reduxLogger.ts +406 -0
  74. package/src/customHooks/useAccordion.tsx +60 -0
  75. package/src/decorators/index.ts +184 -0
  76. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  77. package/src/helpers/index.ts +860 -0
  78. package/src/helpers/memoryManager.ts +138 -0
  79. package/src/helpers/searchQueryParser.ts +283 -0
  80. package/src/helpers/settingsStore.ts +171 -0
  81. package/src/helpers/telemetry.ts +505 -0
  82. package/src/helpers/toast.ts +17 -0
  83. package/src/i18n/index.ts +69 -0
  84. package/src/i18n/locales/en.json +1052 -0
  85. package/src/index.tsx +2336 -0
  86. package/src/native/NativeInspector.ts +397 -0
  87. package/src/native/NativeNetworkInspector.ts +24 -0
  88. package/src/network.ts +22 -0
  89. package/src/performance.ts +38 -0
  90. package/src/redux.ts +23 -0
  91. package/src/styles/AppColors.ts +408 -0
  92. package/src/styles/AppFonts.ts +8 -0
  93. package/src/styles/common.ts +209 -0
  94. package/src/styles/index.ts +1570 -0
  95. package/src/types/enums.ts +194 -0
  96. package/src/types/index.ts +34 -0
  97. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,860 @@
1
+ import {
2
+ Platform,
3
+ ToastAndroid,
4
+ Alert,
5
+ NativeModules,
6
+ Linking,
7
+ } from 'react-native';
8
+ import Clipboard from '@react-native-clipboard/clipboard';
9
+ import {showToast} from './toast';
10
+
11
+ // Stylesheet
12
+ import {AppColors} from '../styles/AppColors';
13
+
14
+ // Constants
15
+ import {DOMAIN_COLORS, DURATION_FAST_MS, DURATION_SLOW_MS} from '../constants';
16
+
17
+ // Type Definition
18
+ import {
19
+ NetworkLog,
20
+ RouteInfo,
21
+ DiffResult,
22
+ JsonContent,
23
+ StackFrameType,
24
+ } from '../types';
25
+ export * from './searchQueryParser';
26
+ export * from './telemetry';
27
+ export * from './memoryManager';
28
+
29
+ export const getDomainColor = (domain: string): string => {
30
+ if (!domain) return DOMAIN_COLORS[0];
31
+ let hash = 0;
32
+ for (let i = 0; i < domain.length; i++) {
33
+ hash = domain.charCodeAt(i) + ((hash << 5) - hash);
34
+ }
35
+ return DOMAIN_COLORS[Math.abs(hash) % DOMAIN_COLORS.length];
36
+ };
37
+
38
+ export const formatDateTime = (timestamp: number): string => {
39
+ const date = new Date(timestamp);
40
+ const pad = (n: number, len = 2) => String(n).padStart(len, '0');
41
+ const day = pad(date.getDate());
42
+ const month = pad(date.getMonth() + 1);
43
+ const year = date.getFullYear();
44
+ const hours = pad(date.getHours());
45
+ const minutes = pad(date.getMinutes());
46
+ const seconds = pad(date.getSeconds());
47
+ return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
48
+ };
49
+
50
+ export const formatTimestamp = (timestamp: number): string => {
51
+ try {
52
+ const date = new Date(timestamp);
53
+ const hours = date.getHours().toString().padStart(2, '0');
54
+ const minutes = date.getMinutes().toString().padStart(2, '0');
55
+ const seconds = date.getSeconds().toString().padStart(2, '0');
56
+ const ms = date.getMilliseconds().toString().padStart(3, '0');
57
+ return `${hours}:${minutes}:${seconds}.${ms}`;
58
+ } catch {
59
+ return '—';
60
+ }
61
+ };
62
+
63
+ export const getStatusColor = (status: number | null): string => {
64
+ if (!status || status === 0) return AppColors.errorColor;
65
+ if (status >= 500) return AppColors.errorColor;
66
+ if (status >= 400) return AppColors.darkOrange;
67
+ if (status >= 300) return AppColors.warningIconGold;
68
+ return AppColors.greenColor;
69
+ };
70
+
71
+ export const getDurationColor = (duration: number | null): string => {
72
+ if (duration == null) return AppColors.grayTextWeak;
73
+ if (duration < DURATION_FAST_MS) return AppColors.greenColor;
74
+ if (duration < DURATION_SLOW_MS) return AppColors.lightOrange;
75
+ return AppColors.errorColor;
76
+ };
77
+
78
+ export const getSize = (data: unknown): string => {
79
+ try {
80
+ const bytes = JSON.stringify(data)?.length ?? 0;
81
+ return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
82
+ } catch {
83
+ return '—';
84
+ }
85
+ };
86
+
87
+ export const copyToClipboard = (value: unknown, label: string): void => {
88
+ const resolved = typeof value === 'function' ? (value as Function)() : value;
89
+ let textToCopy = '';
90
+ if (typeof resolved === 'string') {
91
+ textToCopy = resolved;
92
+ } else {
93
+ try {
94
+ textToCopy = JSON.stringify(resolved, null, 2);
95
+ } catch {
96
+ textToCopy = String(resolved);
97
+ }
98
+ }
99
+
100
+ // Use @react-native-clipboard/clipboard npm package
101
+ try {
102
+ if (typeof Clipboard?.setString === 'function') {
103
+ Clipboard.setString(textToCopy);
104
+ } else if (typeof (Clipboard as any)?.default?.setString === 'function') {
105
+ (Clipboard as any).default.setString(textToCopy);
106
+ }
107
+ } catch (err) {
108
+ if (__DEV__) {
109
+ console.warn('[NetworkInspector] Clipboard.setString failed:', err);
110
+ }
111
+ }
112
+
113
+ // Trigger floating in-app bottom toast notification
114
+ try {
115
+ showToast(label ? `${label} copied to clipboard` : 'Copied to clipboard');
116
+ } catch {}
117
+
118
+ // Native Android Toast fallback
119
+ try {
120
+ if (Platform.OS === 'android' && ToastAndroid?.show) {
121
+ ToastAndroid.show(
122
+ label ? `${label} copied to clipboard` : 'Copied to clipboard',
123
+ ToastAndroid.SHORT,
124
+ );
125
+ }
126
+ } catch {}
127
+ };
128
+
129
+ export const getPath = (url: string): string => {
130
+ try {
131
+ const u = new URL(url);
132
+ return u.pathname + (u.search ? u.search : '');
133
+ } catch {
134
+ const withoutDomain = url.replace(/^https?:\/\/[^/?#]+/, '');
135
+ return withoutDomain || '/';
136
+ }
137
+ };
138
+
139
+ export const getBaseUrl = (url: string): string => {
140
+ try {
141
+ const u = new URL(url);
142
+ return `${u.protocol}//${u.host}`;
143
+ } catch {
144
+ const match = url.match(/^(https?:\/\/[^/]+)/);
145
+ return match ? match[1] : '';
146
+ }
147
+ };
148
+
149
+ export const getCurlCommand = (log: NetworkLog): string => {
150
+ let cmd = `curl -X ${log.method} "${log.url}"`;
151
+ if (log.requestHeaders) {
152
+ Object.entries(log.requestHeaders).forEach(([k, v]) => {
153
+ cmd += ` \\\n -H "${k}: ${v}"`;
154
+ });
155
+ }
156
+ if (log.request && log.method !== 'GET') {
157
+ const body =
158
+ typeof log.request === 'string'
159
+ ? log.request
160
+ : JSON.stringify(log.request);
161
+ cmd += ` \\\n -d '${body.replace(/'/g, "'\\''")}'`;
162
+ }
163
+ return cmd;
164
+ };
165
+
166
+ export const getFetchCommand = (log: NetworkLog): string => {
167
+ const opts: Record<string, unknown> = {method: log.method};
168
+ if (log.requestHeaders && Object.keys(log.requestHeaders).length > 0) {
169
+ opts.headers = log.requestHeaders;
170
+ }
171
+ if (log.request && log.method !== 'GET') {
172
+ opts.body = JSON.stringify(log.request);
173
+ }
174
+ return `fetch("${log.url}", ${JSON.stringify(opts, null, 2)})`;
175
+ };
176
+
177
+ export const deduplicateLogs = (raw: NetworkLog[]): NetworkLog[] => {
178
+ const map = new Map<number, NetworkLog>();
179
+ raw.forEach(entry => {
180
+ if (!map.has(entry.id)) {
181
+ map.set(entry.id, entry);
182
+ } else {
183
+ const existing = map.get(entry.id)!;
184
+ if (existing.status == null && entry.status != null) {
185
+ map.set(entry.id, entry);
186
+ } else if ((entry.startTime ?? 0) >= (existing.startTime ?? 0)) {
187
+ map.set(entry.id, entry);
188
+ }
189
+ }
190
+ });
191
+ return Array.from(map.values()).sort((a, b) => b.id - a.id);
192
+ };
193
+
194
+ export const escapeRegex = (str: string): string => {
195
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
196
+ };
197
+
198
+ export const getNavigationInfo = (
199
+ state: any,
200
+ path: string[] = [],
201
+ ): RouteInfo => {
202
+ if (!state?.routes) {
203
+ return {
204
+ path: path.length > 0 ? path.join(' ➔ ') : 'Navigators',
205
+ params: null,
206
+ };
207
+ }
208
+ const route = state.routes[state.index ?? 0];
209
+ if (!route) {
210
+ return {
211
+ path: path.length > 0 ? path.join(' ➔ ') : 'Navigators',
212
+ params: null,
213
+ };
214
+ }
215
+ const currentPath = route.name ? [...path, route.name] : path;
216
+
217
+ if (route?.state) {
218
+ return getNavigationInfo(route.state, currentPath);
219
+ }
220
+ const resolved =
221
+ currentPath.length > 0 ? currentPath.join(' ➔ ') : 'Navigators';
222
+ return {path: resolved, params: route.params || null};
223
+ };
224
+
225
+ export const flattenObject = (obj: any, prefix = ''): Record<string, any> => {
226
+ let result: Record<string, any> = {};
227
+ if (typeof obj === 'object' && obj !== null) {
228
+ if (Array.isArray(obj)) {
229
+ obj.forEach((v, i) => {
230
+ Object.assign(
231
+ result,
232
+ flattenObject(v, prefix ? `${prefix}[${i}]` : `[${i}]`),
233
+ );
234
+ });
235
+ } else {
236
+ Object.keys(obj).forEach(k => {
237
+ Object.assign(
238
+ result,
239
+ flattenObject(obj[k], prefix ? `${prefix}.${k}` : k),
240
+ );
241
+ });
242
+ }
243
+ } else {
244
+ result[prefix || 'root'] = obj;
245
+ }
246
+ return result;
247
+ };
248
+
249
+ export const getDiff = (oldObj: any, newObj: any): DiffResult[] => {
250
+ const oldFlat = flattenObject(oldObj);
251
+ const newFlat = flattenObject(newObj);
252
+ const diff: DiffResult[] = [];
253
+
254
+ const allKeys = new Set([...Object.keys(oldFlat), ...Object.keys(newFlat)]);
255
+ allKeys.forEach(k => {
256
+ if (!(k in oldFlat)) {
257
+ diff.push({type: 'added', path: k, newVal: newFlat[k]});
258
+ } else if (!(k in newFlat)) {
259
+ diff.push({type: 'removed', path: k, oldVal: oldFlat[k]});
260
+ } else if (oldFlat[k] !== newFlat[k]) {
261
+ diff.push({
262
+ type: 'changed',
263
+ path: k,
264
+ oldVal: oldFlat[k],
265
+ newVal: newFlat[k],
266
+ });
267
+ }
268
+ });
269
+ return diff.sort((a, b) => a.path.localeCompare(b.path));
270
+ };
271
+
272
+ export const formatDisplayUrl = (url: string) => {
273
+ if (!url) return '';
274
+ if (url.startsWith('http://') || url.startsWith('https://')) return url;
275
+ return `https://${url}`;
276
+ };
277
+
278
+ export const getLocalizedFilePath = (
279
+ path: string | any,
280
+ country: string | any,
281
+ language: string | any,
282
+ ) => {
283
+ const url = path.replace(
284
+ 'country-language',
285
+ `${country?.toLowerCase()}-${language?.toLowerCase()}`,
286
+ );
287
+ return url;
288
+ };
289
+
290
+ export const getLocalizedFilePathWithSlash = (
291
+ path: string | any,
292
+ country: string | any,
293
+ language: string | any,
294
+ ) => {
295
+ const url = path.replace(
296
+ 'country/language',
297
+ `${country?.toLowerCase()}/${language?.toLowerCase()}`,
298
+ );
299
+ return url;
300
+ };
301
+
302
+ export const isAllValuesEmpty = (obj: Record<string, any>) => {
303
+ if (!obj || typeof obj !== 'object') return true;
304
+
305
+ return Object.values(obj).every(
306
+ value =>
307
+ value &&
308
+ typeof value === 'object' &&
309
+ !Array.isArray(value) &&
310
+ Object.keys(value).length === 0,
311
+ );
312
+ };
313
+
314
+ export const formatDateTimeToAnalytics = (ts: number): string => {
315
+ const d = new Date(ts);
316
+
317
+ return d?.toLocaleString(undefined, {
318
+ month: 'short',
319
+ day: 'numeric',
320
+ hour: '2-digit',
321
+ minute: '2-digit',
322
+ second: '2-digit',
323
+ });
324
+ };
325
+
326
+ export const getBundleIdentifier = (): string => {
327
+ const RNDeviceInfo = NativeModules.RNDeviceInfo;
328
+ if (RNDeviceInfo && typeof RNDeviceInfo.bundleId === 'string') {
329
+ return RNDeviceInfo.bundleId;
330
+ }
331
+ if (RNDeviceInfo && typeof RNDeviceInfo.getBundleId === 'function') {
332
+ try {
333
+ const res = RNDeviceInfo.getBundleId();
334
+ if (typeof res === 'string') return res;
335
+ } catch (e) {}
336
+ }
337
+
338
+ const ExponentConstants = NativeModules.ExponentConstants;
339
+ if (ExponentConstants && ExponentConstants.manifest) {
340
+ const manifest = ExponentConstants.manifest;
341
+ if (manifest.ios && manifest.ios.bundleIdentifier) {
342
+ return manifest.ios.bundleIdentifier;
343
+ }
344
+ if (manifest.android && manifest.android.package) {
345
+ return manifest.android.package;
346
+ }
347
+ }
348
+
349
+ const ExpoApplication = NativeModules.ExpoApplication;
350
+ if (ExpoApplication && typeof ExpoApplication.applicationId === 'string') {
351
+ return ExpoApplication.applicationId;
352
+ }
353
+
354
+ const SourceCode = NativeModules.SourceCode;
355
+ if (SourceCode && typeof SourceCode.scriptURL === 'string') {
356
+ const url = SourceCode.scriptURL;
357
+ if (url.includes('assets/')) {
358
+ try {
359
+ const match = url.match(/assets\/([^/?#]+)/);
360
+ if (match && match[1]) {
361
+ return match[1];
362
+ }
363
+ } catch (e) {}
364
+ }
365
+ }
366
+
367
+ return 'org.reactjs.native.example';
368
+ };
369
+
370
+ export const getAppName = (): string => {
371
+ // Try iOS via PlatformConstants
372
+ const constants = NativeModules.PlatformConstants;
373
+ if (constants && typeof constants.interfaceIdiom === 'string') {
374
+ // Fallback: try to parse from SourceCode
375
+ }
376
+
377
+ // Try react-native-device-info
378
+ const RNDeviceInfo = NativeModules.RNDeviceInfo;
379
+ if (RNDeviceInfo && typeof RNDeviceInfo.appName === 'string') {
380
+ return RNDeviceInfo.appName;
381
+ }
382
+
383
+ // Try Expo
384
+ const ExpoApplication = NativeModules.ExpoApplication;
385
+ if (ExpoApplication && typeof ExpoApplication.applicationName === 'string') {
386
+ return ExpoApplication.applicationName;
387
+ }
388
+
389
+ const ExponentConstants = NativeModules.ExponentConstants;
390
+ if (ExponentConstants && ExponentConstants.manifest) {
391
+ const manifest = ExponentConstants.manifest;
392
+ if (manifest.name) return manifest.name;
393
+ }
394
+
395
+ // Android: try to get from AndroidInfoModule
396
+ const AppInfo = NativeModules.AppInfo;
397
+ if (AppInfo && typeof AppInfo.appName === 'string') {
398
+ return AppInfo.appName;
399
+ }
400
+
401
+ // Fallback: derive from bundle ID (last segment, cleaned up)
402
+ const bundleId = getBundleIdentifier();
403
+ if (bundleId && bundleId !== 'org.reactjs.native.example') {
404
+ const parts = bundleId.split('.');
405
+ const last = parts[parts.length - 1];
406
+ // Capitalize first letter
407
+ return last ? last.charAt(0).toUpperCase() + last.slice(1) : 'App';
408
+ }
409
+
410
+ return 'App';
411
+ };
412
+
413
+ export const handleOpenExternalLink = (url: string): void => {
414
+ if (!url) return;
415
+ const openUrl = formatDisplayUrl(url);
416
+ Alert.alert(
417
+ 'Open Link',
418
+ `Do you want to open this link in your web browser?\n\n${openUrl}`,
419
+ [
420
+ {text: 'Cancel', style: 'cancel'},
421
+ {
422
+ text: 'Open',
423
+ onPress: () => {
424
+ Linking.openURL(openUrl).catch(() => {});
425
+ },
426
+ },
427
+ ],
428
+ );
429
+ };
430
+
431
+ /** Formats a timestamp as HH:MM:SS (no milliseconds). */
432
+ export const formatTimeShort = (ts: number): string => {
433
+ const d = new Date(ts);
434
+ const hh = String(d.getHours()).padStart(2, '0');
435
+ const mm = String(d.getMinutes()).padStart(2, '0');
436
+ const ss = String(d.getSeconds()).padStart(2, '0');
437
+ return `${hh}:${mm}:${ss}`;
438
+ };
439
+
440
+ /** Formats a timestamp as HH:MM:SS.mmm (with milliseconds). */
441
+ export const formatTime = (ts: number): string => {
442
+ const d = new Date(ts);
443
+ const hh = String(d.getHours()).padStart(2, '0');
444
+ const mm = String(d.getMinutes()).padStart(2, '0');
445
+ const ss = String(d.getSeconds()).padStart(2, '0');
446
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
447
+ return `${hh}:${mm}:${ss}.${ms}`;
448
+ };
449
+
450
+ /** Formats a relative gap like "+2s" / "+1m 5s" / "+350ms". */
451
+ export const formatGap = (ms: number): string => {
452
+ if (ms < 1000) return `+${ms}ms`;
453
+ const s = Math.round(ms / 1000);
454
+ if (s < 60) return `+${s}s`;
455
+ const m = Math.floor(s / 60);
456
+ const rem = s % 60;
457
+ return rem > 0 ? `+${m}m ${rem}s` : `+${m}m`;
458
+ };
459
+
460
+ /** Finds the first JSON object/array embedded anywhere in a log message. */
461
+ export const getJsonContent = (message: string): JsonContent | null => {
462
+ if (!message) return null;
463
+
464
+ const indices: number[] = [];
465
+ for (let i = 0; i < message.length; i++) {
466
+ if (message[i] === '{' || message[i] === '[') {
467
+ indices.push(i);
468
+ }
469
+ }
470
+
471
+ for (const index of indices) {
472
+ const candidate = message.substring(index).trim();
473
+ try {
474
+ const parsed = JSON.parse(candidate);
475
+ if (parsed !== null && typeof parsed === 'object') {
476
+ const header = message.substring(0, index).trim();
477
+ return {header, data: parsed};
478
+ }
479
+ } catch (e) {
480
+ // Ignore
481
+ }
482
+ }
483
+
484
+ return null;
485
+ };
486
+
487
+ /** Pretty-prints JSON data showing 3-4 lines with trailing "..." */
488
+ export const getJsonPreviewText = (
489
+ data: any,
490
+ maxLines = 4,
491
+ ): {text: string; hasMore: boolean} => {
492
+ try {
493
+ const formatted = JSON.stringify(data, null, 2);
494
+ const lines = formatted.split('\n');
495
+ if (lines.length > maxLines) {
496
+ return {
497
+ text: lines.slice(0, maxLines).join('\n') + '\n...',
498
+ hasMore: true,
499
+ };
500
+ }
501
+ return {
502
+ text: formatted,
503
+ hasMore: false,
504
+ };
505
+ } catch (e) {
506
+ return {
507
+ text: String(data),
508
+ hasMore: false,
509
+ };
510
+ }
511
+ };
512
+
513
+ export interface ParsedStackFrame {
514
+ raw: string;
515
+ functionName: string;
516
+ fileName: string;
517
+ fullPath: string;
518
+ fileExt: string;
519
+ frameType: StackFrameType | 'app' | 'dependency' | 'runtime' | 'native';
520
+ isUserCode: boolean;
521
+ isRuntimeNoise: boolean;
522
+ rawFilePath?: string;
523
+ lineNumber?: string;
524
+ columnNumber?: string;
525
+ isOrigin?: boolean;
526
+ copyableLocation: string;
527
+ }
528
+
529
+ /** Parses a stack trace line to extract function name, file name, extension (.tsx/.jsx/.ts), line, and column numbers */
530
+ export const parseStackLine = (
531
+ rawLine: string,
532
+ isOrigin = false,
533
+ ): ParsedStackFrame => {
534
+ let line = rawLine.trim().replace(/^at /, '');
535
+
536
+ // Format: func@file:line:col (JSC / Hermes format)
537
+ if (line.includes('@')) {
538
+ const atIndex = line.indexOf('@');
539
+ const funcPart = line.substring(0, atIndex).trim();
540
+ const pathPart = line.substring(atIndex + 1).trim();
541
+ line = funcPart ? `${funcPart} (${pathPart})` : pathPart;
542
+ }
543
+
544
+ // Check for "func (path:line:col)" or "path:line:col"
545
+ const parenMatch = line.match(/^(.*?)\s*\((.*?)\)$/);
546
+ let functionName = '<anonymous>';
547
+ let locationPart = line;
548
+
549
+ if (parenMatch) {
550
+ functionName = parenMatch[1].trim() || '<anonymous>';
551
+ locationPart = parenMatch[2].trim();
552
+ }
553
+
554
+ // Clean Babel/Hermes artifacts in function names like ?anon_0_, _callee$, etc.
555
+ if (functionName.startsWith('?anon_') || functionName === '?') {
556
+ functionName = 'anonymous';
557
+ }
558
+
559
+ // Remove "address at " prefix if present in Hermes
560
+ locationPart = locationPart.replace(/^address at /, '');
561
+
562
+ // Extract file, line, and col from locationPart (e.g. "path/to/file.tsx:42:15" or "http://...:42:15")
563
+ const locMatch = locationPart.match(/^(.*?):(\d+):(\d+)$/);
564
+ let fullPath = locationPart;
565
+ let lineNumber: string | undefined;
566
+ let columnNumber: string | undefined;
567
+
568
+ if (locMatch) {
569
+ fullPath = locMatch[1];
570
+ lineNumber = locMatch[2];
571
+ columnNumber = locMatch[3];
572
+ }
573
+
574
+ // Clean file name (remove query strings, packager urls, and parent paths)
575
+ let cleanPath = fullPath
576
+ .split('?')[0]
577
+ .split('&')[0]
578
+ .replace(/[)]+$/, '')
579
+ .replace(/\/\/+$/, '');
580
+ // Remove protocol prefixes
581
+ cleanPath = cleanPath.replace(
582
+ /^(?:https?:\/\/[^\/]+\/|file:\/\/\/|webpack:\/\/\/?)/,
583
+ '',
584
+ );
585
+ const fileName = cleanPath.split('/').filter(Boolean).pop() || cleanPath;
586
+
587
+ // Determine file extension (.tsx, .jsx, .ts, .js)
588
+ const extMatch = fileName.match(/\.([a-z0-9]+)$/i);
589
+ const ext = extMatch ? extMatch[1].toLowerCase() : '';
590
+ const fileExt: 'tsx' | 'jsx' | 'ts' | 'js' | 'other' =
591
+ ext === 'tsx' || ext === 'jsx' || ext === 'ts' || ext === 'js'
592
+ ? (ext as 'tsx' | 'jsx' | 'ts' | 'js')
593
+ : 'other';
594
+
595
+ const isNative = locationPart === 'native' || cleanPath === 'native';
596
+ const isInternalBytecode =
597
+ cleanPath.includes('InternalBytecode') ||
598
+ cleanPath.includes('metro-runtime') ||
599
+ cleanPath.includes('regenerator-runtime') ||
600
+ functionName === 'tryCallOne' ||
601
+ functionName === 'asyncGeneratorStep' ||
602
+ functionName === '_next' ||
603
+ (functionName === 'next' && isNative);
604
+
605
+ const isDependency =
606
+ cleanPath.includes('node_modules') ||
607
+ cleanPath.includes('react-native/Libraries') ||
608
+ (cleanPath.includes('react-native-inapp-inspector') &&
609
+ !cleanPath.includes('/example/'));
610
+
611
+ const isUserCode =
612
+ !isNative &&
613
+ !isInternalBytecode &&
614
+ !isDependency &&
615
+ (fileExt === 'tsx' ||
616
+ fileExt === 'jsx' ||
617
+ fileExt === 'ts' ||
618
+ fileExt === 'js' ||
619
+ !fileName.includes('.bundle'));
620
+
621
+ const frameType: 'app' | 'dependency' | 'runtime' | 'native' = isUserCode
622
+ ? 'app'
623
+ : isDependency
624
+ ? 'dependency'
625
+ : isNative
626
+ ? 'native'
627
+ : 'runtime';
628
+
629
+ const isRuntimeNoise =
630
+ isInternalBytecode ||
631
+ isNative ||
632
+ functionName === 'asyncGeneratorStep' ||
633
+ functionName === '_next';
634
+
635
+ // Format clean relative project path
636
+ let relativePath = cleanPath;
637
+ if (relativePath.includes('/example/')) {
638
+ relativePath = relativePath.substring(
639
+ relativePath.indexOf('/example/') + 9,
640
+ );
641
+ } else if (relativePath.includes('/src/')) {
642
+ relativePath = relativePath.substring(relativePath.indexOf('/src/') + 1);
643
+ } else if (relativePath.includes('/node_modules/')) {
644
+ relativePath = relativePath.substring(
645
+ relativePath.indexOf('/node_modules/') + 14,
646
+ );
647
+ }
648
+
649
+ const copyableLocation = lineNumber
650
+ ? `${fileName}:${lineNumber}${columnNumber ? `:${columnNumber}` : ''}`
651
+ : fileName;
652
+
653
+ return {
654
+ raw: rawLine,
655
+ functionName,
656
+ fileName,
657
+ fullPath: relativePath,
658
+ rawFilePath: fullPath,
659
+ fileExt,
660
+ frameType,
661
+ isUserCode,
662
+ isRuntimeNoise,
663
+ lineNumber,
664
+ columnNumber,
665
+ isOrigin,
666
+ copyableLocation,
667
+ };
668
+ };
669
+
670
+ /** Opens a file and line number directly in VS Code / system editor */
671
+ export const openInVSCode = (
672
+ filePath: string,
673
+ lineNumber?: string | number,
674
+ columnNumber?: string | number,
675
+ ) => {
676
+ const numLine = lineNumber ? Number(lineNumber) : 1;
677
+ const numCol = columnNumber ? Number(columnNumber) : 1;
678
+ const line = lineNumber ? `:${lineNumber}` : '';
679
+ const col = columnNumber ? `:${columnNumber}` : '';
680
+ const cleanPath = filePath.replace(/^file:\/\//, '');
681
+
682
+ // 1. Notify Metro dev server on host to launch editor directly on local system
683
+ let extractedOrigin: string | null = null;
684
+ try {
685
+ const scriptURL =
686
+ (NativeModules?.SourceCode as any)?.scriptURL ||
687
+ (NativeModules?.PlatformConstants as any)?.serverHost ||
688
+ (NativeModules?.DevSettings as any)?.serverHost;
689
+ if (typeof scriptURL === 'string' && scriptURL.length > 0) {
690
+ const match = scriptURL.match(/^(https?:\/\/[^/]+)/);
691
+ if (match) {
692
+ extractedOrigin = match[1];
693
+ } else if (!scriptURL.startsWith('http') && scriptURL.includes(':')) {
694
+ extractedOrigin = `http://${scriptURL}`;
695
+ }
696
+ }
697
+ } catch {}
698
+
699
+ const metroHosts = Array.from(
700
+ new Set([
701
+ ...(extractedOrigin ? [extractedOrigin] : []),
702
+ 'http://localhost:8081',
703
+ 'http://127.0.0.1:8081',
704
+ 'http://10.0.2.2:8081',
705
+ ]),
706
+ );
707
+
708
+ metroHosts.forEach(host => {
709
+ try {
710
+ fetch(`${host}/open-stack-frame`, {
711
+ method: 'POST',
712
+ headers: {'Content-Type': 'application/json'},
713
+ body: JSON.stringify({
714
+ file: cleanPath,
715
+ lineNumber: numLine,
716
+ column: numCol,
717
+ }),
718
+ }).catch(() => {});
719
+ } catch {}
720
+ });
721
+
722
+ // 2. Also invoke native URL scheme handlers
723
+ const vscodeUrl = `vscode://file/${cleanPath.replace(/^\/+/, '')}${line}${col}`;
724
+ const cursorUrl = `cursor://file/${cleanPath.replace(/^\/+/, '')}${line}${col}`;
725
+ const vscodeInsidersUrl = `vscode-insiders://file/${cleanPath.replace(/^\/+/, '')}${line}${col}`;
726
+
727
+ Linking.openURL(vscodeUrl).catch(() => {
728
+ Linking.openURL(cursorUrl).catch(() => {
729
+ Linking.openURL(vscodeInsidersUrl).catch(() => {
730
+ // Fallback: Copy to clipboard so user can jump immediately
731
+ copyToClipboard(`${cleanPath}${line}${col}`, 'Location');
732
+ });
733
+ });
734
+ });
735
+ };
736
+
737
+ // ─── Analytics Helpers ────────────────────────────────────────────────────────
738
+
739
+ export const ANALYTICS_EVENT_PALETTE = [
740
+ AppColors.googleBlue,
741
+ AppColors.googleGreen,
742
+ AppColors.googlePurple,
743
+ AppColors.googleTeal,
744
+ AppColors.googleRed,
745
+ AppColors.googleOrange,
746
+ AppColors.blue700,
747
+ AppColors.materialGreen,
748
+ ];
749
+
750
+ export const getEventColor = (name: string): string => {
751
+ const safeName = typeof name === 'string' ? name : String(name || '');
752
+ let hash = 0;
753
+ for (let i = 0; i < safeName.length; i++) {
754
+ hash = (hash * 31 + safeName.charCodeAt(i)) | 0;
755
+ }
756
+ return ANALYTICS_EVENT_PALETTE[
757
+ Math.abs(hash) % ANALYTICS_EVENT_PALETTE.length
758
+ ];
759
+ };
760
+
761
+ export {
762
+ getEventCategory,
763
+ registerGAPlugin,
764
+ type GAEventCategory,
765
+ type GAPlugin,
766
+ } from './gaAnalyticsRegistry';
767
+
768
+ export const getCategoryColors = (category: string) => {
769
+ switch (category) {
770
+ case 'page_view':
771
+ case 'Page View':
772
+ return {
773
+ bg: AppColors.blueBg,
774
+ border: AppColors.blueBorder,
775
+ text: AppColors.blue800,
776
+ };
777
+ case 'ecommerce':
778
+ case 'Ecommerce':
779
+ return {
780
+ bg: AppColors.greenBg,
781
+ border: AppColors.greenBorder,
782
+ text: AppColors.materialGreen,
783
+ };
784
+ case 'system':
785
+ case 'System':
786
+ return {
787
+ bg: AppColors.greyBg,
788
+ border: AppColors.greyBorder,
789
+ text: AppColors.grey600,
790
+ };
791
+ default:
792
+ return {
793
+ bg: AppColors.purpleBg,
794
+ border: AppColors.purpleBorder,
795
+ text: AppColors.purpleText,
796
+ };
797
+ }
798
+ };
799
+
800
+ export interface RuntimeDiagnostics {
801
+ engineType: 'hermes' | 'v8' | 'jsc';
802
+ archType: 'fabric' | 'paper';
803
+ usedHeapMb: number;
804
+ totalAllocMb: number;
805
+ }
806
+
807
+ export const getRuntimeDiagnostics = (): RuntimeDiagnostics => {
808
+ const isHermes = typeof (global as any).HermesInternal !== 'undefined';
809
+ const isV8 = typeof (global as any)._v8runtime !== 'undefined';
810
+ const engineType: 'hermes' | 'v8' | 'jsc' = isHermes
811
+ ? 'hermes'
812
+ : isV8
813
+ ? 'v8'
814
+ : 'jsc';
815
+
816
+ const isFabric =
817
+ typeof (global as any).nativeFabricUIManager !== 'undefined' ||
818
+ Boolean((global as any).__turboModuleProxy);
819
+ const archType: 'fabric' | 'paper' = isFabric ? 'fabric' : 'paper';
820
+
821
+ let usedHeapMb = 32.4;
822
+ let totalAllocMb = 64.0;
823
+
824
+ try {
825
+ const hermesStats = (
826
+ global as any
827
+ ).HermesInternal?.getInstrumentedStats?.();
828
+ if (hermesStats?.js_heap_size) {
829
+ usedHeapMb = Number(
830
+ (hermesStats.js_heap_size / (1024 * 1024)).toFixed(1),
831
+ );
832
+ totalAllocMb = Number(
833
+ (
834
+ (hermesStats.js_allocated_bytes || hermesStats.js_heap_size * 1.6) /
835
+ (1024 * 1024)
836
+ ).toFixed(1),
837
+ );
838
+ } else if ((global as any).performance?.memory?.usedJSHeapSize) {
839
+ usedHeapMb = Number(
840
+ (
841
+ (global as any).performance.memory.usedJSHeapSize /
842
+ (1024 * 1024)
843
+ ).toFixed(1),
844
+ );
845
+ totalAllocMb = Number(
846
+ (
847
+ (global as any).performance.memory.totalJSHeapSize /
848
+ (1024 * 1024)
849
+ ).toFixed(1),
850
+ );
851
+ }
852
+ } catch {}
853
+
854
+ return {
855
+ engineType,
856
+ archType,
857
+ usedHeapMb,
858
+ totalAllocMb,
859
+ };
860
+ };