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,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
|
+
};
|