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,944 @@
1
+ import {
2
+ LogBox,
3
+ NativeModules,
4
+ TurboModuleRegistry,
5
+ NativeEventEmitter,
6
+ Platform,
7
+ AppState,
8
+ Dimensions,
9
+ } from 'react-native';
10
+ import {CrashRecord, ParsedStackFrame, CrashBreadcrumb} from '../types';
11
+ import {CrashExportFormat, CrashType} from '../types/enums';
12
+ import {addLogFromCrash} from './consoleLogger';
13
+ import {
14
+ showNativeFloatingButton,
15
+ setNativeFloatingButtonBadge,
16
+ } from '../native/NativeInspector';
17
+ import {t} from '../i18n';
18
+
19
+ export interface CrashEventPayload {
20
+ error: Error | any;
21
+ isFatal?: boolean;
22
+ message: string;
23
+ stack?: string;
24
+ timestamp: number;
25
+ logId?: number;
26
+ crashRecord?: CrashRecord;
27
+ }
28
+
29
+ export {CrashExportFormat};
30
+
31
+ type CrashListener = (payload: CrashEventPayload) => void;
32
+
33
+ let crashListeners: CrashListener[] = [];
34
+ let isCrashHandlerInitialized = false;
35
+ let lastHandledErrorTimestamp = 0;
36
+ let crashRecordsStore: CrashRecord[] = [];
37
+ let breadcrumbsStore: CrashBreadcrumb[] = [];
38
+ const MAX_BREADCRUMBS = 50;
39
+ let maxStoredCrashes = 50;
40
+ const appStartTime = Date.now();
41
+ let isCrashModuleEnabled = false;
42
+
43
+ export const setCrashModuleEnabled = (enabled: boolean) => {
44
+ isCrashModuleEnabled = enabled;
45
+ };
46
+
47
+ export const getCrashModuleEnabled = () => isCrashModuleEnabled;
48
+
49
+ // ─── BREADCRUMB MANAGERS ───────────────────────────────────────────────────────
50
+
51
+ export const setMaxCrashLogsLimit = (max: number): void => {
52
+ maxStoredCrashes = Math.max(10, max);
53
+ if (crashRecordsStore.length > maxStoredCrashes) {
54
+ crashRecordsStore = crashRecordsStore.slice(0, maxStoredCrashes);
55
+ }
56
+ };
57
+
58
+ export const getMaxCrashLogsLimit = (): number => maxStoredCrashes;
59
+
60
+ export const pruneCrashRecords = (targetCount?: number): number => {
61
+ const countToKeep = targetCount !== undefined ? Math.max(0, targetCount) : Math.floor(crashRecordsStore.length / 2);
62
+ const pruned = crashRecordsStore.length - countToKeep;
63
+ if (pruned > 0) {
64
+ crashRecordsStore = crashRecordsStore.slice(0, countToKeep);
65
+ }
66
+ if (breadcrumbsStore.length > 20) {
67
+ breadcrumbsStore = breadcrumbsStore.slice(0, 20);
68
+ }
69
+ return Math.max(0, pruned);
70
+ };
71
+
72
+ export const addCrashBreadcrumb = (
73
+ type: CrashBreadcrumb['type'],
74
+ message: string,
75
+ data?: any,
76
+ ): void => {
77
+ if (!isCrashModuleEnabled) return;
78
+ try {
79
+ const entry: CrashBreadcrumb = {
80
+ type,
81
+ message,
82
+ timestamp: Date.now(),
83
+ data,
84
+ };
85
+ breadcrumbsStore.unshift(entry);
86
+ if (breadcrumbsStore.length > MAX_BREADCRUMBS) {
87
+ breadcrumbsStore = breadcrumbsStore.slice(0, MAX_BREADCRUMBS);
88
+ }
89
+ } catch {}
90
+ };
91
+
92
+ export const recordNavigationBreadcrumb = (
93
+ fromRoute: string,
94
+ toRoute: string,
95
+ ): void => {
96
+ addCrashBreadcrumb(
97
+ 'navigation',
98
+ t('crash.breadcrumbNavigation', {from: fromRoute || '/', to: toRoute || '/'}),
99
+ {
100
+ from: fromRoute,
101
+ to: toRoute,
102
+ },
103
+ );
104
+ };
105
+
106
+ export const recordNetworkBreadcrumb = (
107
+ url: string,
108
+ method: string,
109
+ status?: number,
110
+ duration?: number,
111
+ ): void => {
112
+ const statusStr = status != null ? ` [${status}]` : '';
113
+ const durStr = duration != null ? ` (${Math.round(duration)}ms)` : '';
114
+ addCrashBreadcrumb(
115
+ 'network',
116
+ `${method.toUpperCase()} ${url}${statusStr}${durStr}`,
117
+ {
118
+ url,
119
+ method,
120
+ status,
121
+ duration,
122
+ },
123
+ );
124
+ };
125
+
126
+ export const recordReduxBreadcrumb = (
127
+ actionType: string,
128
+ payload?: any,
129
+ ): void => {
130
+ addCrashBreadcrumb('redux', t('crash.breadcrumbAction', {actionType}), {
131
+ type: actionType,
132
+ payloadSummary:
133
+ typeof payload === 'object' && payload !== null
134
+ ? Object.keys(payload)
135
+ : typeof payload,
136
+ });
137
+ };
138
+
139
+ export const recordUserActionBreadcrumb = (
140
+ action: string,
141
+ metadata?: any,
142
+ ): void => {
143
+ addCrashBreadcrumb('user', action, metadata);
144
+ };
145
+
146
+ export const clearCrashBreadcrumbs = (): void => {
147
+ breadcrumbsStore = [];
148
+ };
149
+
150
+ export const getCrashBreadcrumbs = (): CrashBreadcrumb[] => {
151
+ return [...breadcrumbsStore];
152
+ };
153
+
154
+ // ─── STACK TRACE PARSING & INTELLIGENT SOURCE DETECTION ───────────────────────
155
+
156
+ /**
157
+ * Intelligent stack trace parser with multi-engine support:
158
+ * - Hermes bytecode & source maps
159
+ * - Android Java / Kotlin native frames
160
+ * - iOS Objective-C / Swift symbols
161
+ * - JavaScript standard V8 & SpiderMonkey
162
+ */
163
+ export const parseCrashStackTrace = (stack?: string): ParsedStackFrame[] => {
164
+ if (!stack || typeof stack !== 'string') return [];
165
+ const lines = stack.split('\n');
166
+ const frames: ParsedStackFrame[] = [];
167
+
168
+ for (const line of lines) {
169
+ const trimmed = line.trim();
170
+ if (!trimmed) continue;
171
+
172
+ // Filter out internal wrapper noise
173
+ if (
174
+ trimmed.includes('crashHandler.ts') ||
175
+ trimmed.includes('handleInterceptedCrash') ||
176
+ trimmed.includes('addLogFromCrash') ||
177
+ trimmed.includes('setupGlobalCrashHandler')
178
+ ) {
179
+ continue;
180
+ }
181
+
182
+ // Pattern 1: iOS/Hermes format: method@file:line:column or method@url:line:col
183
+ const iosMatch = trimmed.match(/^([^@]+)@(.*):(\d+):(\d+)$/);
184
+ if (iosMatch) {
185
+ const [, method, file, lineNum, colNum] = iosMatch;
186
+ const cleanFile = file.split('?')[0].split('/').pop() || file;
187
+ const isAppCode =
188
+ !file.includes('node_modules') &&
189
+ !file.includes('react-native') &&
190
+ !file.includes('react-dom') &&
191
+ !file.includes('hermes') &&
192
+ !file.includes('internal');
193
+ frames.push({
194
+ method: method.trim(),
195
+ file: cleanFile,
196
+ lineNumber: parseInt(lineNum, 10),
197
+ column: parseInt(colNum, 10),
198
+ raw: trimmed,
199
+ isAppCode,
200
+ });
201
+ continue;
202
+ }
203
+
204
+ // Pattern 2: V8/Android JS format: at method (file:line:column)
205
+ const androidMatch = trimmed.match(/^at\s+(.+)\s+\((.+):(\d+):(\d+)\)$/);
206
+ if (androidMatch) {
207
+ const [, method, file, lineNum, colNum] = androidMatch;
208
+ const cleanFile = file.split('?')[0].split('/').pop() || file;
209
+ const isAppCode =
210
+ !file.includes('node_modules') &&
211
+ !file.includes('react-native') &&
212
+ !file.includes('react-dom') &&
213
+ !file.includes('internal');
214
+ frames.push({
215
+ method: method.trim(),
216
+ file: cleanFile,
217
+ lineNumber: parseInt(lineNum, 10),
218
+ column: parseInt(colNum, 10),
219
+ raw: trimmed,
220
+ isAppCode,
221
+ });
222
+ continue;
223
+ }
224
+
225
+ // Pattern 3: Simple at file:line:column
226
+ const simpleMatch = trimmed.match(/^at\s+(.+):(\d+):(\d+)$/);
227
+ if (simpleMatch) {
228
+ const [, file, lineNum, colNum] = simpleMatch;
229
+ const cleanFile = file.split('?')[0].split('/').pop() || file;
230
+ const isAppCode =
231
+ !file.includes('node_modules') && !file.includes('react-native');
232
+ frames.push({
233
+ method: '<anonymous>',
234
+ file: cleanFile,
235
+ lineNumber: parseInt(lineNum, 10),
236
+ column: parseInt(colNum, 10),
237
+ raw: trimmed,
238
+ isAppCode,
239
+ });
240
+ continue;
241
+ }
242
+
243
+ // Pattern 4: Java / Android Native Frame: at com.pkg.Class.method(Class.java:123)
244
+ const javaMatch = trimmed.match(
245
+ /^at\s+([a-zA-Z0-9_$.]+)\(([a-zA-Z0-9_$]+\.java):(\d+)\)$/,
246
+ );
247
+ if (javaMatch) {
248
+ const [, methodPath, fileName, lineNum] = javaMatch;
249
+ const isAppCode =
250
+ !methodPath.startsWith('com.facebook.react') &&
251
+ !methodPath.startsWith('android.');
252
+ frames.push({
253
+ method: methodPath,
254
+ file: fileName,
255
+ lineNumber: parseInt(lineNum, 10),
256
+ column: 0,
257
+ raw: trimmed,
258
+ isAppCode,
259
+ });
260
+ continue;
261
+ }
262
+
263
+ // Pattern 5: iOS Mach-O Symbol: 0 AppName 0x000000010... -[ViewController method] + 48
264
+ const machOMatch = trimmed.match(
265
+ /^\d+\s+([^\s]+)\s+(0x[0-9a-fA-F]+)\s+(.+)$/,
266
+ );
267
+ if (machOMatch) {
268
+ const [, binaryName, address, symbol] = machOMatch;
269
+ const isAppCode =
270
+ !binaryName.startsWith('lib') &&
271
+ !binaryName.startsWith('Core') &&
272
+ !binaryName.startsWith('React');
273
+ frames.push({
274
+ method: symbol,
275
+ file: binaryName,
276
+ lineNumber: 0,
277
+ column: 0,
278
+ raw: trimmed,
279
+ isAppCode,
280
+ });
281
+ continue;
282
+ }
283
+
284
+ // Pattern 6: Fallback for generic frame lines
285
+ frames.push({
286
+ method: trimmed.startsWith('at ') ? trimmed.slice(3) : trimmed,
287
+ file: 'runtime',
288
+ lineNumber: 0,
289
+ column: 0,
290
+ raw: trimmed,
291
+ isAppCode: false,
292
+ });
293
+ }
294
+
295
+ return frames;
296
+ };
297
+
298
+ // ─── DIAGNOSTICS & SYSTEM METRICS ─────────────────────────────────────────────
299
+
300
+ const getMemoryDiagnostics = () => {
301
+ try {
302
+ const performance = (globalThis as any).performance;
303
+ if (performance && performance.memory) {
304
+ return {
305
+ usedJSHeapSize: Math.round(
306
+ performance.memory.usedJSHeapSize / (1024 * 1024),
307
+ ),
308
+ totalJSHeapSize: Math.round(
309
+ performance.memory.totalJSHeapSize / (1024 * 1024),
310
+ ),
311
+ };
312
+ }
313
+ } catch {}
314
+ return undefined;
315
+ };
316
+
317
+ const getScreenDimensions = () => {
318
+ try {
319
+ const window = Dimensions.get('window');
320
+ return `${Math.round(window.width)}x${Math.round(window.height)} (${
321
+ window.scale
322
+ }x)`;
323
+ } catch {
324
+ return t('crash.unknown');
325
+ }
326
+ };
327
+
328
+ /**
329
+ * Computes a unique fingerprint signature to group recurring crash occurrences.
330
+ */
331
+ export const computeCrashFingerprint = (crash: CrashRecord): string => {
332
+ const type = crash.type || 'js';
333
+ const name = crash.name || 'Error';
334
+ const topFrame =
335
+ crash.parsedStack && crash.parsedStack.length > 0
336
+ ? crash.parsedStack[0]
337
+ : null;
338
+ const location = topFrame
339
+ ? `${topFrame.file}:${topFrame.lineNumber}`
340
+ : 'unknown';
341
+ return `${type}_${name}_${location}`;
342
+ };
343
+
344
+ // ─── CRASH RECORD MANAGEMENT & INTERCEPTION ───────────────────────────────────
345
+
346
+ export const getCrashRecords = (): CrashRecord[] => {
347
+ return [...crashRecordsStore];
348
+ };
349
+
350
+ export const clearCrashRecords = (): void => {
351
+ crashRecordsStore = [];
352
+ emitCrashEvent({
353
+ error: null,
354
+ message: '__CLEARED__',
355
+ timestamp: Date.now(),
356
+ });
357
+ };
358
+
359
+ export const subscribeCrashEvents = (listener: CrashListener): (() => void) => {
360
+ crashListeners.push(listener);
361
+ return () => {
362
+ crashListeners = crashListeners.filter(l => l !== listener);
363
+ };
364
+ };
365
+
366
+ export const emitCrashEvent = (payload: CrashEventPayload): void => {
367
+ if (payload.message !== '__CLEARED__') {
368
+ const now = Date.now();
369
+ // Throttle duplicate rapid crash emissions within 250ms
370
+ if (now - lastHandledErrorTimestamp < 250) {
371
+ return;
372
+ }
373
+ lastHandledErrorTimestamp = now;
374
+ }
375
+
376
+ crashListeners.forEach(listener => {
377
+ try {
378
+ listener(payload);
379
+ } catch {}
380
+ });
381
+ };
382
+
383
+ /**
384
+ * Core exception interceptor. Ingests all forms of errors, parses call stacks,
385
+ * collects device telemetry, records breadcrumbs, and updates the store.
386
+ */
387
+ export const handleInterceptedCrash = (
388
+ errorOrTitle: any,
389
+ rawStack?: any,
390
+ isFatal = false,
391
+ customType?: CrashType,
392
+ componentStack?: string,
393
+ ): CrashRecord => {
394
+ try {
395
+ const errorObj =
396
+ errorOrTitle instanceof Error
397
+ ? errorOrTitle
398
+ : typeof errorOrTitle === 'object' && errorOrTitle !== null
399
+ ? errorOrTitle
400
+ : new Error(String(errorOrTitle || t('crash.runtimeException')));
401
+
402
+ const rawMsg =
403
+ errorObj.message ||
404
+ (typeof errorOrTitle === 'string' ? errorOrTitle : t('crash.runtimeException'));
405
+
406
+ let stackString = '';
407
+ if (typeof rawStack === 'string') {
408
+ stackString = rawStack;
409
+ } else if (Array.isArray(rawStack)) {
410
+ stackString = rawStack
411
+ .map(
412
+ f =>
413
+ `at ${f.methodName || '<anonymous>'} (${f.file || 'unknown'}:${
414
+ f.lineNumber || 0
415
+ }:${f.column || 0})`,
416
+ )
417
+ .join('\n');
418
+ } else {
419
+ stackString = errorObj.stack || new Error().stack || '';
420
+ }
421
+
422
+ // Determine Crash Type
423
+ let inferredType: CrashType = customType || CrashType.Js;
424
+ const lowMsg = rawMsg.toLowerCase();
425
+ if (!customType) {
426
+ if (
427
+ lowMsg.includes('native') ||
428
+ lowMsg.includes('sigsegv') ||
429
+ lowMsg.includes('nsrange') ||
430
+ lowMsg.includes('nullpointerexception') ||
431
+ lowMsg.includes('fatal signal')
432
+ ) {
433
+ inferredType = CrashType.Native;
434
+ } else if (
435
+ lowMsg.includes('promise') ||
436
+ lowMsg.includes('unhandled rejection') ||
437
+ lowMsg.includes('unhandledrejection')
438
+ ) {
439
+ inferredType = CrashType.Promise;
440
+ } else if (
441
+ lowMsg.includes('render') ||
442
+ lowMsg.includes('errorboundary') ||
443
+ componentStack
444
+ ) {
445
+ inferredType = CrashType.Render;
446
+ } else {
447
+ inferredType = CrashType.Js;
448
+ }
449
+ }
450
+
451
+ const now = new Date();
452
+ const dateStr = now.toLocaleDateString();
453
+ const timeStr = now.toLocaleTimeString();
454
+
455
+ const parsedStack = parseCrashStackTrace(stackString);
456
+
457
+ const log = addLogFromCrash(
458
+ errorObj,
459
+ `[${isFatal ? t('crash.logFatalCrash') : t('crash.logUnhandledError')}] ${rawMsg}`,
460
+ stackString,
461
+ isFatal,
462
+ );
463
+
464
+ const isHermes = typeof (globalThis as any).HermesInternal !== 'undefined';
465
+ const isFabric =
466
+ typeof (globalThis as any).nativeFabricUIManager !== 'undefined';
467
+
468
+ const crashRecord: CrashRecord = {
469
+ id: `crash_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
470
+ error: errorObj,
471
+ isFatal,
472
+ type: inferredType,
473
+ message: rawMsg,
474
+ name: errorObj.name || (isFatal ? t('crash.errorNameFatal') : t('crash.errorNameUnhandled')),
475
+ stack: stackString,
476
+ parsedStack,
477
+ componentStack: componentStack || undefined,
478
+ timestamp: Date.now(),
479
+ dateStr,
480
+ timeStr,
481
+ deviceInfo: {
482
+ platform: Platform.OS,
483
+ osVersion: String(Platform.Version),
484
+ rnVersion: (Platform.constants as any)?.reactNativeVersion
485
+ ? `${(Platform.constants as any).reactNativeVersion.major}.${
486
+ (Platform.constants as any).reactNativeVersion.minor
487
+ }.${(Platform.constants as any).reactNativeVersion.patch}`
488
+ : t('crash.unknown'),
489
+ isHermes,
490
+ isFabric,
491
+ appState: AppState.currentState || 'active',
492
+ },
493
+ memoryInfo: getMemoryDiagnostics(),
494
+ breadcrumbs: [...breadcrumbsStore],
495
+ logId: log?.id,
496
+ };
497
+
498
+ crashRecordsStore.unshift(crashRecord);
499
+ if (crashRecordsStore.length > maxStoredCrashes) {
500
+ crashRecordsStore = crashRecordsStore.slice(0, maxStoredCrashes);
501
+ }
502
+
503
+ emitCrashEvent({
504
+ error: errorObj,
505
+ isFatal,
506
+ message: `[${isFatal ? t('crash.logFatalCrash') : t('crash.logUnhandledError')}] ${rawMsg}`,
507
+ stack: stackString,
508
+ timestamp: Date.now(),
509
+ logId: log?.id,
510
+ crashRecord,
511
+ });
512
+
513
+ // Ensure native floating icon is visible & badged even if React UI is broken
514
+ try {
515
+ showNativeFloatingButton();
516
+ setNativeFloatingButtonBadge(true);
517
+ } catch {}
518
+
519
+ return crashRecord;
520
+ } catch (err: any) {
521
+ const fallbackRecord: CrashRecord = {
522
+ id: `crash_${Date.now()}`,
523
+ isFatal: true,
524
+ type: CrashType.Js,
525
+ message: String(errorOrTitle || t('crash.unknownException')),
526
+ timestamp: Date.now(),
527
+ dateStr: new Date().toLocaleDateString(),
528
+ timeStr: new Date().toLocaleTimeString(),
529
+ };
530
+ return fallbackRecord;
531
+ }
532
+ };
533
+
534
+ /**
535
+ * Manually report a caught error with custom metadata.
536
+ */
537
+ export const recordCustomCrash = (
538
+ error: Error | string,
539
+ options?: {
540
+ isFatal?: boolean;
541
+ type?: CrashType;
542
+ componentStack?: string;
543
+ },
544
+ ): CrashRecord => {
545
+ return handleInterceptedCrash(
546
+ error,
547
+ typeof error === 'object' ? (error as Error).stack : undefined,
548
+ options?.isFatal ?? false,
549
+ options?.type || CrashType.Custom,
550
+ options?.componentStack,
551
+ );
552
+ };
553
+
554
+ /**
555
+ * Simulates a crash for developer testing without crashing the host app.
556
+ * Uses real runtime Error objects — no mocked file paths or fake stacks.
557
+ */
558
+ export const simulateTestCrash = (
559
+ type: CrashType = CrashType.Js,
560
+ customMessage?: string,
561
+ customStack?: string,
562
+ ): CrashRecord => {
563
+ const err = new Error(customMessage || _defaultSimMessage(type));
564
+
565
+ return handleInterceptedCrash(
566
+ type === CrashType.Native ? err.message : err,
567
+ customStack || err.stack,
568
+ type === CrashType.Native,
569
+ type,
570
+ type === CrashType.Render ? _buildComponentStack() : undefined,
571
+ );
572
+ };
573
+
574
+ /** Generates a generic error message per type — no fake references. */
575
+ const _defaultSimMessage = (type: CrashType): string => {
576
+ switch (type) {
577
+ case 'native':
578
+ return t('crash.simNativeMessage');
579
+ case 'promise':
580
+ return t('crash.simPromiseMessage');
581
+ case 'render':
582
+ return t('crash.simRenderMessage');
583
+ default:
584
+ return t('crash.simJsMessage');
585
+ }
586
+ };
587
+
588
+ /** Builds a component stack from the real React tree if possible. */
589
+ const _buildComponentStack = (): string | undefined => {
590
+ try {
591
+ const err = new Error();
592
+ if (err.stack) {
593
+ // Return the live stack as a pseudo component-stack
594
+ return err.stack
595
+ .split('\n')
596
+ .slice(1, 6)
597
+ .map(line => ` ${line.trim()}`)
598
+ .join('\n');
599
+ }
600
+ } catch {}
601
+ return undefined;
602
+ };
603
+
604
+ // ─── EXPORT & REPORTING UTILITIES ─────────────────────────────────────────────
605
+
606
+ /**
607
+ * Formats a crash into Text, Markdown, or JSON.
608
+ */
609
+ export const exportCrashReport = (
610
+ crash: CrashRecord,
611
+ format: CrashExportFormat = 'text',
612
+ ): string => {
613
+ if (format === 'json') {
614
+ return JSON.stringify(crash, null, 2);
615
+ }
616
+
617
+ const uptimeSec = Math.round((crash.timestamp - appStartTime) / 1000);
618
+
619
+ if (format === 'markdown') {
620
+ const lines: string[] = [];
621
+ lines.push(`## ${t('crash.mdReportTitle', {name: crash.name || 'Error'})}`);
622
+ lines.push(`> **${crash.message}**`);
623
+ lines.push('');
624
+ lines.push(
625
+ `- **${t('crash.reportType')}** \`${crash.type.toUpperCase()}\``,
626
+ );
627
+ lines.push(
628
+ `- **${t('crash.reportFatal')}** \`${
629
+ crash.isFatal
630
+ ? t('crash.mdSeverityFatal')
631
+ : t('crash.mdSeverityHandled')
632
+ }\``,
633
+ );
634
+ lines.push(
635
+ `- **${t('crash.reportTimestamp')}** ${crash.dateStr} ${crash.timeStr}`,
636
+ );
637
+ lines.push(`- **${t('crash.reportUptime')}** ${uptimeSec}s`);
638
+ lines.push(
639
+ `- **${t(
640
+ 'crash.reportPlatform',
641
+ )}** ${crash.deviceInfo?.platform?.toUpperCase()} (v${
642
+ crash.deviceInfo?.osVersion
643
+ })`,
644
+ );
645
+ lines.push(
646
+ `- **${t('crash.reportReactNative')}** v${
647
+ crash.deviceInfo?.rnVersion || 'N/A'
648
+ }`,
649
+ );
650
+ lines.push(
651
+ `- **${t('crash.jsEngine')}** ${
652
+ crash.deviceInfo?.isHermes ? t('crash.hermesEngine') : t('crash.jsc')
653
+ }`,
654
+ );
655
+ lines.push(
656
+ `- **${t('crash.reportArchitecture')}** ${
657
+ crash.deviceInfo?.isFabric
658
+ ? t('crash.reportFabricNew')
659
+ : t('crash.reportPaperLegacy')
660
+ }`,
661
+ );
662
+ if (crash.memoryInfo) {
663
+ lines.push(
664
+ `- **${t('crash.reportJsMemory')}** ${
665
+ crash.memoryInfo.usedJSHeapSize
666
+ } MB / ${crash.memoryInfo.totalJSHeapSize} MB`,
667
+ );
668
+ }
669
+ lines.push('');
670
+ lines.push(`### ${t('crash.mdStackTrace')}`);
671
+ lines.push('```');
672
+ lines.push(crash.stack || t('crash.reportNoStackTrace'));
673
+ lines.push('```');
674
+ if (crash.componentStack) {
675
+ lines.push('');
676
+ lines.push(`### ${t('crash.mdComponentHierarchy')}`);
677
+ lines.push('```');
678
+ lines.push(crash.componentStack);
679
+ lines.push('```');
680
+ }
681
+ if (crash.breadcrumbs && crash.breadcrumbs.length > 0) {
682
+ lines.push('');
683
+ lines.push(`### ${t('crash.mdRecentBreadcrumbs')}`);
684
+ crash.breadcrumbs.forEach(b => {
685
+ const time = new Date(b.timestamp).toLocaleTimeString();
686
+ lines.push(
687
+ `- \`[${time}]\` **[${b.type.toUpperCase()}]** ${b.message}`,
688
+ );
689
+ });
690
+ }
691
+ return lines.join('\n');
692
+ }
693
+
694
+ // Standard Text format
695
+ const lines: string[] = [];
696
+ lines.push('====================================================');
697
+ lines.push(` ${t('crash.reportTitle')} `);
698
+ lines.push('====================================================');
699
+ lines.push(`${t('crash.reportErrorName')} ${crash.name || 'Error'}`);
700
+ lines.push(`${t('crash.reportMessage')} ${crash.message}`);
701
+ lines.push(`${t('crash.reportType')} ${crash.type.toUpperCase()}`);
702
+ lines.push(
703
+ `${t('crash.reportFatal')} ${
704
+ crash.isFatal ? t('crash.reportFatalYes') : t('crash.reportFatalNo')
705
+ }`,
706
+ );
707
+ lines.push(
708
+ `${t('crash.reportTimestamp')} ${crash.dateStr} ${crash.timeStr} (${
709
+ crash.timestamp
710
+ })`,
711
+ );
712
+ lines.push(
713
+ `${t('crash.reportUptime')} ${t('crash.reportUptimeValue', {
714
+ seconds: uptimeSec,
715
+ })}`,
716
+ );
717
+ lines.push(
718
+ `${t(
719
+ 'crash.reportPlatform',
720
+ )} ${crash.deviceInfo?.platform?.toUpperCase()} (v${
721
+ crash.deviceInfo?.osVersion
722
+ })`,
723
+ );
724
+ lines.push(
725
+ `${t('crash.reportReactNative')} ${crash.deviceInfo?.rnVersion || 'N/A'}`,
726
+ );
727
+ lines.push(
728
+ `${t('crash.reportHermes')} ${
729
+ crash.deviceInfo?.isHermes
730
+ ? t('crash.reportEnabled')
731
+ : t('crash.reportDisabled')
732
+ }`,
733
+ );
734
+ lines.push(
735
+ `${t('crash.reportArchitecture')} ${
736
+ crash.deviceInfo?.isFabric
737
+ ? t('crash.reportFabricNew')
738
+ : t('crash.reportPaperLegacy')
739
+ }`,
740
+ );
741
+ lines.push(`${t('crash.reportScreenSize')} ${getScreenDimensions()}`);
742
+ lines.push(
743
+ `${t('crash.reportAppState')} ${
744
+ crash.deviceInfo?.appState || 'active'
745
+ }`,
746
+ );
747
+ if (crash.memoryInfo) {
748
+ lines.push(
749
+ `${t('crash.reportJsMemory')} ${
750
+ crash.memoryInfo.usedJSHeapSize
751
+ } MB / ${crash.memoryInfo.totalJSHeapSize} MB`,
752
+ );
753
+ }
754
+ lines.push('----------------------------------------------------');
755
+ lines.push(`${t('crash.reportStackTrace')}`);
756
+ lines.push(crash.stack || t('crash.reportNoStackTrace'));
757
+ if (crash.componentStack) {
758
+ lines.push('----------------------------------------------------');
759
+ lines.push(`${t('crash.reportComponentHierarchy')}`);
760
+ lines.push(crash.componentStack);
761
+ }
762
+ if (crash.breadcrumbs && crash.breadcrumbs.length > 0) {
763
+ lines.push('----------------------------------------------------');
764
+ lines.push(`${t('crash.reportRecentBreadcrumbs')}`);
765
+ crash.breadcrumbs.forEach(b => {
766
+ const time = new Date(b.timestamp).toLocaleTimeString();
767
+ lines.push(` [${time}] [${b.type.toUpperCase()}] ${b.message}`);
768
+ });
769
+ }
770
+ lines.push('====================================================');
771
+ return lines.join('\n');
772
+ };
773
+
774
+ // ─── GLOBAL HANDLER INITIALIZATION ────────────────────────────────────────────
775
+
776
+ /**
777
+ * Natively intercepts and prevents crashes, native RedBox dialogues,
778
+ * unhandled JS errors, and unhandled promise rejections.
779
+ */
780
+ export const setupGlobalCrashHandler = (): void => {
781
+ if (isCrashHandlerInitialized) return;
782
+ isCrashHandlerInitialized = true;
783
+
784
+ // 1. Completely silence React Native LogBox & RedBox overlays
785
+ try {
786
+ if (LogBox && typeof LogBox.ignoreAllLogs === 'function') {
787
+ LogBox.ignoreAllLogs(true);
788
+ }
789
+ } catch {}
790
+
791
+ try {
792
+ // @ts-ignore
793
+ const LogBoxData = (require as any)(
794
+ 'react-native/Libraries/LogBox/Data/LogBoxData',
795
+ );
796
+ if (LogBoxData && typeof LogBoxData.setDisabled === 'function') {
797
+ LogBoxData.setDisabled(true);
798
+ }
799
+ } catch {}
800
+
801
+ // 2. Intercept Native Module: NetworkInspectorModule (Android & iOS)
802
+ try {
803
+ const networkInspectorModule = NativeModules?.NetworkInspectorModule;
804
+ if (networkInspectorModule) {
805
+ if (
806
+ typeof networkInspectorModule.enableNativeCrashProtection === 'function'
807
+ ) {
808
+ networkInspectorModule.enableNativeCrashProtection();
809
+ }
810
+
811
+ const nativeEmitter = new NativeEventEmitter(networkInspectorModule);
812
+ nativeEmitter.addListener('onNativeCrash', (event: any) => {
813
+ const platform = event?.platform || Platform.OS;
814
+ const msg = `[${platform.toUpperCase()} ${t('crash.nativeCrashTitle')}] ${
815
+ event?.message || t('crash.nativeUncaughtException')
816
+ }`;
817
+ const stack = event?.stack || '';
818
+ handleInterceptedCrash(msg, stack, true, 'native');
819
+ });
820
+ }
821
+ } catch {}
822
+
823
+ // 3. Intercept Native ExceptionsManager & RedBox modules (Bridge & TurboModules)
824
+ try {
825
+ const tmReg =
826
+ (TurboModuleRegistry as any) || (globalThis as any).__turboModuleProxy;
827
+ const nativeExceptionsManager =
828
+ NativeModules?.ExceptionsManager ||
829
+ (tmReg?.get ? tmReg.get('ExceptionsManager') : null);
830
+
831
+ if (nativeExceptionsManager) {
832
+ nativeExceptionsManager.reportFatalException = (
833
+ title: string,
834
+ stack: any,
835
+ exceptionId?: number,
836
+ ) => {
837
+ handleInterceptedCrash(title, stack, true, 'native');
838
+ try {
839
+ if (typeof nativeExceptionsManager.dismissRedbox === 'function') {
840
+ nativeExceptionsManager.dismissRedbox();
841
+ }
842
+ } catch {}
843
+ };
844
+
845
+ nativeExceptionsManager.reportSoftException = (
846
+ title: string,
847
+ stack: any,
848
+ exceptionId?: number,
849
+ ) => {
850
+ handleInterceptedCrash(title, stack, false, 'js');
851
+ try {
852
+ if (typeof nativeExceptionsManager.dismissRedbox === 'function') {
853
+ nativeExceptionsManager.dismissRedbox();
854
+ }
855
+ } catch {}
856
+ };
857
+
858
+ nativeExceptionsManager.reportException = (data: any) => {
859
+ const msg = data?.message || data?.title || t('crash.nativeException');
860
+ const stack = data?.stack || data?.rawStack;
861
+ handleInterceptedCrash(msg, stack, data?.isFatal ?? true, 'js');
862
+ try {
863
+ if (typeof nativeExceptionsManager.dismissRedbox === 'function') {
864
+ nativeExceptionsManager.dismissRedbox();
865
+ }
866
+ } catch {}
867
+ };
868
+
869
+ nativeExceptionsManager.updateExceptionMessage = () => {};
870
+ try {
871
+ if (typeof nativeExceptionsManager.dismissRedbox === 'function') {
872
+ nativeExceptionsManager.dismissRedbox();
873
+ }
874
+ } catch {}
875
+ }
876
+
877
+ const nativeRedBox =
878
+ NativeModules?.RedBox ||
879
+ NativeModules?.RCTRedBox ||
880
+ (tmReg?.get ? tmReg.get('RedBox') || tmReg.get('RCTRedBox') : null);
881
+
882
+ if (nativeRedBox) {
883
+ if (typeof nativeRedBox.showErrorMessage === 'function') {
884
+ nativeRedBox.showErrorMessage = () => {};
885
+ }
886
+ if (typeof nativeRedBox.showUserError === 'function') {
887
+ nativeRedBox.showUserError = () => {};
888
+ }
889
+ if (typeof nativeRedBox.dismiss === 'function') {
890
+ nativeRedBox.dismiss();
891
+ }
892
+ }
893
+ } catch {}
894
+
895
+ // 4. Hook React Native ErrorUtils
896
+ try {
897
+ const globalObj = globalThis as any;
898
+ const errorUtils = globalObj.ErrorUtils;
899
+
900
+ if (errorUtils) {
901
+ if (typeof errorUtils.setGlobalHandler === 'function') {
902
+ errorUtils.setGlobalHandler((error: Error, isFatal?: boolean) => {
903
+ handleInterceptedCrash(error, error?.stack, isFatal ?? false, 'js');
904
+ // Suppress calling default crashing handler to prevent termination & RedBox
905
+ });
906
+ }
907
+ if (typeof errorUtils.reportFatalError === 'function') {
908
+ errorUtils.reportFatalError = (error: Error) => {
909
+ handleInterceptedCrash(error, error?.stack, true, 'js');
910
+ };
911
+ }
912
+ if (typeof errorUtils.reportError === 'function') {
913
+ errorUtils.reportError = (error: Error) => {
914
+ handleInterceptedCrash(error, error?.stack, false, 'js');
915
+ };
916
+ }
917
+ }
918
+ } catch {}
919
+
920
+ // 5. Hook global Promise unhandled rejections
921
+ try {
922
+ const globalObj = globalThis as any;
923
+
924
+ const handleUnhandledPromise = (eventOrError: any) => {
925
+ try {
926
+ const reason =
927
+ eventOrError?.reason || eventOrError?.detail?.reason || eventOrError;
928
+ const message =
929
+ reason?.message || String(reason || t('crash.unhandledPromiseRejection'));
930
+ const stack =
931
+ reason?.stack || (eventOrError?.stack ?? new Error().stack);
932
+
933
+ handleInterceptedCrash(reason, stack, false, 'promise');
934
+ } catch {}
935
+ };
936
+
937
+ if (typeof globalObj.addEventListener === 'function') {
938
+ globalObj.addEventListener('unhandledrejection', handleUnhandledPromise);
939
+ }
940
+ if (typeof globalObj.onunhandledrejection !== 'undefined') {
941
+ globalObj.onunhandledrejection = handleUnhandledPromise;
942
+ }
943
+ } catch {}
944
+ };