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.
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/package.json +2 -1
- package/src/analytics.ts +35 -0
- package/src/bundle.ts +25 -0
- package/src/components/AnalyticsDetail.tsx +865 -0
- package/src/components/AnalyticsEventCard.tsx +441 -0
- package/src/components/AnalyticsGraph.tsx +583 -0
- package/src/components/AnimatedEntrance.tsx +64 -0
- package/src/components/AppHeaderLogo.tsx +109 -0
- package/src/components/BrandCircleIcon.tsx +144 -0
- package/src/components/BrandSquareIcon.tsx +144 -0
- package/src/components/CodeSnippet.tsx +725 -0
- package/src/components/ConsoleLogCard.tsx +628 -0
- package/src/components/CopyButton.tsx +87 -0
- package/src/components/DiffViewer.tsx +82 -0
- package/src/components/DomainHeader.tsx +237 -0
- package/src/components/EmptyState.tsx +73 -0
- package/src/components/EndOfListFooter.tsx +100 -0
- package/src/components/ErrorBoundary.tsx +688 -0
- package/src/components/HeadersSection.tsx +192 -0
- package/src/components/HighlightText.tsx +100 -0
- package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
- package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
- package/src/components/Inspector/BundleTab.tsx +4731 -0
- package/src/components/Inspector/ConsoleTab.tsx +702 -0
- package/src/components/Inspector/CrashDetail.tsx +941 -0
- package/src/components/Inspector/CrashFilterModal.tsx +721 -0
- package/src/components/Inspector/CrashTab.tsx +871 -0
- package/src/components/Inspector/FabLauncher.tsx +80 -0
- package/src/components/Inspector/InspectorContext.tsx +28 -0
- package/src/components/Inspector/InspectorHeader.tsx +973 -0
- package/src/components/Inspector/LogDetail.tsx +1427 -0
- package/src/components/Inspector/MainScreen.tsx +258 -0
- package/src/components/Inspector/NavigationTracker.tsx +13 -0
- package/src/components/Inspector/NetworkDetail.tsx +794 -0
- package/src/components/Inspector/NetworkTab.tsx +1095 -0
- package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
- package/src/components/Inspector/PerformanceTab.tsx +1894 -0
- package/src/components/Inspector/ReduxDetail.tsx +1651 -0
- package/src/components/Inspector/ReduxTab.tsx +954 -0
- package/src/components/Inspector/SettingsPanel.tsx +3181 -0
- package/src/components/Inspector/TabBar.tsx +187 -0
- package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
- package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
- package/src/components/JsonViewer.tsx +486 -0
- package/src/components/LogCard.tsx +491 -0
- package/src/components/LogSyntaxHighlighter.tsx +178 -0
- package/src/components/MetaAccordion.tsx +340 -0
- package/src/components/MiniBarChart.tsx +42 -0
- package/src/components/MiniLineChart.tsx +33 -0
- package/src/components/NetworkIcons.tsx +2118 -0
- package/src/components/SectionHeader.tsx +113 -0
- package/src/components/SegmentedTabs.tsx +83 -0
- package/src/components/Slider.tsx +300 -0
- package/src/components/SourcePageCard.tsx +148 -0
- package/src/components/Toast.tsx +131 -0
- package/src/components/TouchableScale.tsx +91 -0
- package/src/components/TreeNode.tsx +186 -0
- package/src/console.ts +24 -0
- package/src/constants/index.ts +38 -0
- package/src/constants/version.ts +3 -0
- package/src/crash.ts +32 -0
- package/src/customHooks/analyticsLogger.ts +336 -0
- package/src/customHooks/bundleAnalyzer.ts +1231 -0
- package/src/customHooks/consoleLogger.ts +497 -0
- package/src/customHooks/crashHandler.ts +944 -0
- package/src/customHooks/logFilters.ts +32 -0
- package/src/customHooks/networkLogger.ts +419 -0
- package/src/customHooks/performanceTracker.ts +1014 -0
- package/src/customHooks/reduxLogger.ts +406 -0
- package/src/customHooks/useAccordion.tsx +60 -0
- package/src/decorators/index.ts +184 -0
- package/src/helpers/gaAnalyticsRegistry.ts +204 -0
- package/src/helpers/index.ts +860 -0
- package/src/helpers/memoryManager.ts +138 -0
- package/src/helpers/searchQueryParser.ts +283 -0
- package/src/helpers/settingsStore.ts +171 -0
- package/src/helpers/telemetry.ts +505 -0
- package/src/helpers/toast.ts +17 -0
- package/src/i18n/index.ts +69 -0
- package/src/i18n/locales/en.json +1052 -0
- package/src/index.tsx +2336 -0
- package/src/native/NativeInspector.ts +397 -0
- package/src/native/NativeNetworkInspector.ts +24 -0
- package/src/network.ts +22 -0
- package/src/performance.ts +38 -0
- package/src/redux.ts +23 -0
- package/src/styles/AppColors.ts +408 -0
- package/src/styles/AppFonts.ts +8 -0
- package/src/styles/common.ts +209 -0
- package/src/styles/index.ts +1570 -0
- package/src/types/enums.ts +194 -0
- package/src/types/index.ts +34 -0
- package/src/types/interfaces.ts +515 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import {NativeModules} from 'react-native';
|
|
2
|
+
import {ConsoleLog} from '../types';
|
|
3
|
+
import {IGNORED_LOG_PREFIXES} from './logFilters';
|
|
4
|
+
import {setupGlobalCrashHandler} from './crashHandler';
|
|
5
|
+
|
|
6
|
+
let logs: ConsoleLog[] = [];
|
|
7
|
+
let listeners: ((logs: ConsoleLog[]) => void)[] = [];
|
|
8
|
+
let counter = 0;
|
|
9
|
+
let isIntercepting = false;
|
|
10
|
+
let isConsoleModuleEnabled = true;
|
|
11
|
+
|
|
12
|
+
export const setConsoleModuleEnabled = (enabled: boolean) => {
|
|
13
|
+
isConsoleModuleEnabled = enabled;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getConsoleModuleEnabled = () => isConsoleModuleEnabled;
|
|
17
|
+
|
|
18
|
+
let maxConsoleLogsLimit = 100;
|
|
19
|
+
|
|
20
|
+
export const setMaxConsoleLogsLimit = (limit: number): void => {
|
|
21
|
+
maxConsoleLogsLimit = Math.max(10, limit);
|
|
22
|
+
if (logs.length > maxConsoleLogsLimit) {
|
|
23
|
+
logs = logs.slice(0, maxConsoleLogsLimit);
|
|
24
|
+
notify();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getMaxConsoleLogsLimit = (): number => maxConsoleLogsLimit;
|
|
29
|
+
|
|
30
|
+
export const pruneConsoleLogs = (targetCount?: number): number => {
|
|
31
|
+
const countToKeep = targetCount !== undefined ? Math.max(0, targetCount) : Math.floor(logs.length / 2);
|
|
32
|
+
const pruned = logs.length - countToKeep;
|
|
33
|
+
if (pruned > 0) {
|
|
34
|
+
logs = logs.slice(0, countToKeep);
|
|
35
|
+
notify();
|
|
36
|
+
}
|
|
37
|
+
return Math.max(0, pruned);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const formatArgs = (args: any[]): string => {
|
|
43
|
+
return args
|
|
44
|
+
.map(arg => {
|
|
45
|
+
if (arg === null) return 'null';
|
|
46
|
+
if (arg === undefined) return 'undefined';
|
|
47
|
+
if (typeof arg === 'object') {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.stringify(arg, null, 2);
|
|
50
|
+
} catch {
|
|
51
|
+
return Object.prototype.toString.call(arg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return String(arg);
|
|
55
|
+
})
|
|
56
|
+
.join(' ');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ─── Dynamic Symbolication Helper for React Native Metro ────────────────────
|
|
60
|
+
|
|
61
|
+
const getMetroSymbolicateUrl = (): string => {
|
|
62
|
+
try {
|
|
63
|
+
const scriptURL =
|
|
64
|
+
(NativeModules.SourceCode as any)?.scriptURL ||
|
|
65
|
+
(NativeModules?.PlatformConstants as any)?.serverHost ||
|
|
66
|
+
(NativeModules?.DevSettings as any)?.serverHost;
|
|
67
|
+
if (typeof scriptURL === 'string' && scriptURL.length > 0) {
|
|
68
|
+
const match = scriptURL.match(/^(https?:\/\/[^\/]+)/);
|
|
69
|
+
if (match) {
|
|
70
|
+
return `${match[1]}/symbolicate`;
|
|
71
|
+
} else if (!scriptURL.startsWith('http') && scriptURL.includes(':')) {
|
|
72
|
+
return `http://${scriptURL}/symbolicate`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {}
|
|
76
|
+
return 'http://localhost:8081/symbolicate';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface MetroSymbolicatedFrame {
|
|
80
|
+
file: string;
|
|
81
|
+
lineNumber?: number;
|
|
82
|
+
column?: number;
|
|
83
|
+
methodName?: string;
|
|
84
|
+
collapse?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const symbolicateStack = async (
|
|
88
|
+
stackString: string,
|
|
89
|
+
): Promise<MetroSymbolicatedFrame[] | null> => {
|
|
90
|
+
if (typeof __DEV__ === 'undefined' || !__DEV__ || !stackString) return null;
|
|
91
|
+
|
|
92
|
+
// 1. Try React Native built-in symbolicateStackTrace if available
|
|
93
|
+
try {
|
|
94
|
+
let symModule: any = null;
|
|
95
|
+
try {
|
|
96
|
+
// @ts-ignore
|
|
97
|
+
symModule = require('react-native/Libraries/Core/Devtools/symbolicateStackTrace');
|
|
98
|
+
} catch {}
|
|
99
|
+
|
|
100
|
+
const symFn = symModule?.default || symModule;
|
|
101
|
+
if (typeof symFn === 'function') {
|
|
102
|
+
const res = await symFn(stackString);
|
|
103
|
+
if (res && Array.isArray(res.stack) && res.stack.length > 0) {
|
|
104
|
+
return res.stack;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
|
|
109
|
+
// 2. Direct HTTP symbolication to dynamic Metro packager endpoint
|
|
110
|
+
try {
|
|
111
|
+
const metroUrl = getMetroSymbolicateUrl();
|
|
112
|
+
const scriptURL = (NativeModules.SourceCode as any)?.scriptURL || 'http://localhost:8081/index.bundle?platform=ios&dev=true';
|
|
113
|
+
|
|
114
|
+
// Parse Hermes stack string into frame objects for Metro symbolicate endpoint
|
|
115
|
+
const lines = stackString.split('\n');
|
|
116
|
+
const inputFrames: any[] = [];
|
|
117
|
+
|
|
118
|
+
for (const rawLine of lines) {
|
|
119
|
+
const line = rawLine.trim().replace(/^at /, '');
|
|
120
|
+
if (!line) continue;
|
|
121
|
+
|
|
122
|
+
let methodName = '<anonymous>';
|
|
123
|
+
let locationPart = line;
|
|
124
|
+
|
|
125
|
+
const parenMatch = line.match(/^(.*?)\s*\((.*?)\)$/);
|
|
126
|
+
if (parenMatch) {
|
|
127
|
+
methodName = parenMatch[1].trim() || '<anonymous>';
|
|
128
|
+
locationPart = parenMatch[2].trim();
|
|
129
|
+
} else if (line.includes('@')) {
|
|
130
|
+
const atIdx = line.indexOf('@');
|
|
131
|
+
methodName = line.substring(0, atIdx).trim() || '<anonymous>';
|
|
132
|
+
locationPart = line.substring(atIdx + 1).trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
locationPart = locationPart.replace(/^address at /, '');
|
|
136
|
+
|
|
137
|
+
const locMatch = locationPart.match(/^(.*?):(\d+):(\d+)$/);
|
|
138
|
+
if (locMatch) {
|
|
139
|
+
let file = locMatch[1];
|
|
140
|
+
if (!file.startsWith('http') && file !== 'native' && !file.includes('InternalBytecode')) {
|
|
141
|
+
file = scriptURL;
|
|
142
|
+
}
|
|
143
|
+
inputFrames.push({
|
|
144
|
+
file,
|
|
145
|
+
lineNumber: parseInt(locMatch[2], 10),
|
|
146
|
+
column: parseInt(locMatch[3], 10),
|
|
147
|
+
methodName,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (inputFrames.length > 0) {
|
|
153
|
+
const response = await fetch(metroUrl, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: {'Content-Type': 'application/json'},
|
|
156
|
+
body: JSON.stringify({stack: inputFrames}),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (response.ok) {
|
|
160
|
+
const json = await response.json();
|
|
161
|
+
if (json && Array.isArray(json.stack) && json.stack.length > 0) {
|
|
162
|
+
return json.stack;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch {}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const isLoggerInternal = (line: string): boolean => {
|
|
172
|
+
const l = line.toLowerCase();
|
|
173
|
+
return (
|
|
174
|
+
l.includes('getstackdetails') ||
|
|
175
|
+
l.includes('addlog') ||
|
|
176
|
+
l.includes('consolelogger') ||
|
|
177
|
+
l.includes('setupconsolelogger') ||
|
|
178
|
+
l.includes('formatargs') ||
|
|
179
|
+
l.includes('react-native-inapp-inspector') ||
|
|
180
|
+
l.trim() === 'error'
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const getStackDetails = (
|
|
185
|
+
args?: any[],
|
|
186
|
+
): {caller: string; stack?: string; errorStack?: string; stackToUse?: string} => {
|
|
187
|
+
try {
|
|
188
|
+
let errorStack: string | undefined;
|
|
189
|
+
if (args && args.length > 0) {
|
|
190
|
+
for (const arg of args) {
|
|
191
|
+
if (arg instanceof Error && arg.stack) {
|
|
192
|
+
errorStack = arg.stack;
|
|
193
|
+
break;
|
|
194
|
+
} else if (
|
|
195
|
+
arg &&
|
|
196
|
+
typeof arg === 'object' &&
|
|
197
|
+
typeof arg.stack === 'string'
|
|
198
|
+
) {
|
|
199
|
+
errorStack = arg.stack;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const rawStack = new Error().stack;
|
|
206
|
+
const stackToUse = errorStack || rawStack;
|
|
207
|
+
if (!stackToUse) return {caller: 'Unknown', errorStack};
|
|
208
|
+
|
|
209
|
+
const lines = stackToUse.split('\n');
|
|
210
|
+
let userCaller: string | undefined;
|
|
211
|
+
let fallbackCaller: string | undefined;
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
const line = lines[i];
|
|
215
|
+
if (!line || isLoggerInternal(line)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const isInternal =
|
|
220
|
+
line.includes('react-native/Libraries') ||
|
|
221
|
+
line.includes('setUpConsole') ||
|
|
222
|
+
line.includes('ExceptionsManager') ||
|
|
223
|
+
line.includes('MessageQueue') ||
|
|
224
|
+
line.includes('metro-runtime') ||
|
|
225
|
+
line.includes('regenerator-runtime');
|
|
226
|
+
|
|
227
|
+
if (!fallbackCaller && !line.includes('setUpConsole')) {
|
|
228
|
+
fallbackCaller = line.trim().replace(/^at /, '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!isInternal) {
|
|
232
|
+
userCaller = line.trim().replace(/^at /, '');
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const cleanedStack = lines.filter(l => !isLoggerInternal(l)).join('\n');
|
|
238
|
+
const caller = userCaller || fallbackCaller || 'Unknown';
|
|
239
|
+
return {caller, stack: cleanedStack || stackToUse, errorStack, stackToUse};
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return {caller: 'Unknown'};
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const notify = () => {
|
|
246
|
+
const snapshot = [...logs];
|
|
247
|
+
listeners.forEach(cb => cb(snapshot));
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const addLog = (
|
|
251
|
+
type: 'info' | 'warn' | 'error',
|
|
252
|
+
args: any[],
|
|
253
|
+
sourceMethod?: 'log' | 'info' | 'warn' | 'error',
|
|
254
|
+
) => {
|
|
255
|
+
if (!isConsoleModuleEnabled) return;
|
|
256
|
+
if (isIntercepting) return;
|
|
257
|
+
isIntercepting = true;
|
|
258
|
+
try {
|
|
259
|
+
const message = formatArgs(args);
|
|
260
|
+
|
|
261
|
+
// Check if the log should be ignored based on starting prefixes across all categories
|
|
262
|
+
const allPrefixes = [
|
|
263
|
+
...((IGNORED_LOG_PREFIXES && IGNORED_LOG_PREFIXES.info) || []),
|
|
264
|
+
...((IGNORED_LOG_PREFIXES && IGNORED_LOG_PREFIXES.warn) || []),
|
|
265
|
+
...((IGNORED_LOG_PREFIXES && IGNORED_LOG_PREFIXES.error) || []),
|
|
266
|
+
].filter(p => typeof p === 'string' && p.trim().length > 0);
|
|
267
|
+
if (
|
|
268
|
+
allPrefixes.some(
|
|
269
|
+
prefix =>
|
|
270
|
+
message
|
|
271
|
+
.toLowerCase()
|
|
272
|
+
.trim()
|
|
273
|
+
.startsWith(prefix.toLowerCase().trim()) ||
|
|
274
|
+
message.toLowerCase().trim().includes(prefix.toLowerCase().trim()),
|
|
275
|
+
)
|
|
276
|
+
) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const {caller, stack, errorStack, stackToUse} = getStackDetails(args);
|
|
281
|
+
const newLog: ConsoleLog = {
|
|
282
|
+
id: counter++,
|
|
283
|
+
type,
|
|
284
|
+
message,
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
caller,
|
|
287
|
+
stack,
|
|
288
|
+
errorStack,
|
|
289
|
+
rawArgs: args,
|
|
290
|
+
sourceMethod,
|
|
291
|
+
};
|
|
292
|
+
logs.unshift(newLog);
|
|
293
|
+
logs = logs.slice(0, maxConsoleLogsLimit);
|
|
294
|
+
notify();
|
|
295
|
+
|
|
296
|
+
// Asynchronously symbolicate stack trace via Metro in development
|
|
297
|
+
const stackToSymbolicate = errorStack || stackToUse || stack;
|
|
298
|
+
if (stackToSymbolicate && typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
299
|
+
const currentLogId = newLog.id;
|
|
300
|
+
symbolicateStack(stackToSymbolicate)
|
|
301
|
+
.then(symFrames => {
|
|
302
|
+
if (symFrames && Array.isArray(symFrames) && symFrames.length > 0) {
|
|
303
|
+
const symLines: string[] = [];
|
|
304
|
+
let symUserCaller: string | undefined;
|
|
305
|
+
|
|
306
|
+
for (const frame of symFrames) {
|
|
307
|
+
const file = (frame.file || '').replace(/^webpack:\/\/\/?/, '');
|
|
308
|
+
const method = frame.methodName || '<anonymous>';
|
|
309
|
+
const line = frame.lineNumber != null ? `:${frame.lineNumber}` : '';
|
|
310
|
+
const col = frame.column != null ? `:${frame.column}` : '';
|
|
311
|
+
|
|
312
|
+
// Skip logger internal frames
|
|
313
|
+
const isInternal =
|
|
314
|
+
file.includes('customHooks/consoleLogger') ||
|
|
315
|
+
file.includes('setupConsoleLogger') ||
|
|
316
|
+
file.includes('getStackDetails') ||
|
|
317
|
+
file.includes('react-native-inapp-inspector/dist') ||
|
|
318
|
+
file.includes('react-native-inapp-inspector/src') ||
|
|
319
|
+
method === 'getStackDetails' ||
|
|
320
|
+
method === 'addLog' ||
|
|
321
|
+
(method === 'log' && file.includes('consoleLogger'));
|
|
322
|
+
|
|
323
|
+
if (isInternal) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const lineStr = `at ${method} (${file}${line}${col})`;
|
|
328
|
+
symLines.push(lineStr);
|
|
329
|
+
|
|
330
|
+
const isUserFile =
|
|
331
|
+
file &&
|
|
332
|
+
!file.includes('node_modules') &&
|
|
333
|
+
!file.includes('react-native/') &&
|
|
334
|
+
(file.endsWith('.tsx') ||
|
|
335
|
+
file.endsWith('.jsx') ||
|
|
336
|
+
file.endsWith('.ts') ||
|
|
337
|
+
file.endsWith('.js'));
|
|
338
|
+
|
|
339
|
+
if (!symUserCaller && isUserFile) {
|
|
340
|
+
const cleanFile = file.split('/').pop() || file;
|
|
341
|
+
symUserCaller = `${method} (${cleanFile}${line}${col})`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const targetLog = logs.find(l => l.id === currentLogId);
|
|
346
|
+
if (targetLog) {
|
|
347
|
+
if (symUserCaller) {
|
|
348
|
+
targetLog.caller = symUserCaller;
|
|
349
|
+
} else if (symLines.length > 0) {
|
|
350
|
+
targetLog.caller = symLines[0].replace(/^at /, '');
|
|
351
|
+
}
|
|
352
|
+
if (symLines.length > 0) {
|
|
353
|
+
targetLog.stack = symLines.join('\n');
|
|
354
|
+
}
|
|
355
|
+
notify();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
.catch(() => {});
|
|
360
|
+
}
|
|
361
|
+
} catch (e) {
|
|
362
|
+
// Fail-safe to prevent crash during logging
|
|
363
|
+
} finally {
|
|
364
|
+
isIntercepting = false;
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
export const addLogFromCrash = (
|
|
371
|
+
error: any,
|
|
372
|
+
message: string,
|
|
373
|
+
stack?: string,
|
|
374
|
+
isFatal?: boolean,
|
|
375
|
+
): ConsoleLog => {
|
|
376
|
+
const {caller, errorStack} = getStackDetails([error]);
|
|
377
|
+
const newLog: ConsoleLog = {
|
|
378
|
+
id: counter++,
|
|
379
|
+
type: 'error',
|
|
380
|
+
message: message || (error?.message ?? 'Runtime Crash'),
|
|
381
|
+
timestamp: Date.now(),
|
|
382
|
+
caller:
|
|
383
|
+
caller !== 'Unknown'
|
|
384
|
+
? caller
|
|
385
|
+
: isFatal
|
|
386
|
+
? 'Fatal Crash'
|
|
387
|
+
: 'Unhandled Exception',
|
|
388
|
+
stack: stack || error?.stack || errorStack,
|
|
389
|
+
errorStack: stack || error?.stack,
|
|
390
|
+
rawArgs: [error],
|
|
391
|
+
sourceMethod: 'error',
|
|
392
|
+
};
|
|
393
|
+
logs.unshift(newLog);
|
|
394
|
+
logs = logs.slice(0, maxConsoleLogsLimit);
|
|
395
|
+
notify();
|
|
396
|
+
|
|
397
|
+
const stackToSym = stack || error?.stack || errorStack;
|
|
398
|
+
if (stackToSym && typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
399
|
+
symbolicateStack(stackToSym)
|
|
400
|
+
.then(symFrames => {
|
|
401
|
+
if (symFrames && Array.isArray(symFrames) && symFrames.length > 0) {
|
|
402
|
+
const symLines = symFrames.map(
|
|
403
|
+
f =>
|
|
404
|
+
`at ${f.methodName || '<anonymous>'} (${(f.file || '').replace(
|
|
405
|
+
/^webpack:\/\/\/?/,
|
|
406
|
+
'',
|
|
407
|
+
)}:${f.lineNumber ?? 0}:${f.column ?? 0})`,
|
|
408
|
+
);
|
|
409
|
+
const target = logs.find(l => l.id === newLog.id);
|
|
410
|
+
if (target) {
|
|
411
|
+
target.stack = symLines.join('\n');
|
|
412
|
+
notify();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
.catch(() => {});
|
|
417
|
+
}
|
|
418
|
+
return newLog;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export const subscribeConsoleLogs = (
|
|
422
|
+
callback: (logs: ConsoleLog[]) => void,
|
|
423
|
+
) => {
|
|
424
|
+
listeners.push(callback);
|
|
425
|
+
callback([...logs]);
|
|
426
|
+
return () => {
|
|
427
|
+
listeners = listeners.filter(l => l !== callback);
|
|
428
|
+
};
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
export const clearConsoleLogs = () => {
|
|
432
|
+
logs = [];
|
|
433
|
+
notify();
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export const getConsoleLogs = () => [...logs];
|
|
437
|
+
|
|
438
|
+
export const setupConsoleLogger = () => {
|
|
439
|
+
if ((globalThis as any).__CONSOLE_LOGGER_INITIALIZED__) return;
|
|
440
|
+
|
|
441
|
+
const originalConsole = {
|
|
442
|
+
log: console.log,
|
|
443
|
+
info: console.info,
|
|
444
|
+
warn: console.warn,
|
|
445
|
+
error: console.error,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Intercept console.log
|
|
449
|
+
console.log = (...args: any[]) => {
|
|
450
|
+
try {
|
|
451
|
+
addLog('info', args, 'log');
|
|
452
|
+
} catch {}
|
|
453
|
+
originalConsole.log(...args);
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Intercept console.info
|
|
457
|
+
console.info = (...args: any[]) => {
|
|
458
|
+
try {
|
|
459
|
+
addLog('info', args, 'info');
|
|
460
|
+
} catch {}
|
|
461
|
+
originalConsole.info(...args);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Intercept console.warn
|
|
465
|
+
console.warn = (...args: any[]) => {
|
|
466
|
+
try {
|
|
467
|
+
addLog('warn', args, 'warn');
|
|
468
|
+
} catch {}
|
|
469
|
+
originalConsole.warn(...args);
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// Intercept console.error
|
|
473
|
+
console.error = (...args: any[]) => {
|
|
474
|
+
try {
|
|
475
|
+
addLog('error', args, 'error');
|
|
476
|
+
} catch {}
|
|
477
|
+
|
|
478
|
+
const firstArg = args && args[0];
|
|
479
|
+
const isRenderError =
|
|
480
|
+
typeof firstArg === 'string' &&
|
|
481
|
+
(firstArg.includes('The above error occurred in') ||
|
|
482
|
+
firstArg.includes('Render Error') ||
|
|
483
|
+
firstArg.includes('ReferenceError') ||
|
|
484
|
+
firstArg.includes('TypeError'));
|
|
485
|
+
|
|
486
|
+
// Suppress console.error if it's a React render crash to prevent RedBox dialog
|
|
487
|
+
if (!isRenderError) {
|
|
488
|
+
originalConsole.error(...args);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
setupGlobalCrashHandler();
|
|
494
|
+
} catch {}
|
|
495
|
+
|
|
496
|
+
(globalThis as any).__CONSOLE_LOGGER_INITIALIZED__ = true;
|
|
497
|
+
};
|