react-native-inapp-inspector 2.2.2 → 2.2.4

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 (159) hide show
  1. package/dist/commonjs/components/AnalyticsDetail.js +5 -1
  2. package/dist/commonjs/components/AnalyticsEventCard.js +2 -1
  3. package/dist/commonjs/components/DiffViewer.js +1 -1
  4. package/dist/commonjs/components/Inspector/BundleTab.js +22 -0
  5. package/dist/commonjs/components/Inspector/DeviceInfoTab.d.ts +3 -0
  6. package/dist/commonjs/components/Inspector/DeviceInfoTab.js +835 -0
  7. package/dist/commonjs/components/Inspector/InspectorHeader.js +12 -14
  8. package/dist/commonjs/components/Inspector/LogDetail.js +6 -3
  9. package/dist/commonjs/components/Inspector/MainScreen.js +7 -3
  10. package/dist/commonjs/components/Inspector/NetworkTab.js +92 -15
  11. package/dist/commonjs/components/Inspector/PerformanceTab.js +38 -12
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +440 -229
  13. package/dist/commonjs/components/Inspector/StorageTab.d.ts +3 -0
  14. package/dist/commonjs/components/Inspector/StorageTab.js +892 -0
  15. package/dist/commonjs/components/Inspector/TabBar.js +14 -0
  16. package/dist/commonjs/components/Inspector/TelemetryConsentModal.js +11 -2
  17. package/dist/commonjs/components/Inspector/UpdateAvailableModal.js +4 -1
  18. package/dist/commonjs/components/NetworkIcons.d.ts +6 -0
  19. package/dist/commonjs/components/NetworkIcons.js +30 -1
  20. package/dist/commonjs/constants/version.d.ts +1 -1
  21. package/dist/commonjs/constants/version.js +1 -1
  22. package/dist/commonjs/customHooks/bundleAnalyzer.js +44 -22
  23. package/dist/commonjs/customHooks/storageInspector.d.ts +67 -0
  24. package/dist/commonjs/customHooks/storageInspector.js +486 -0
  25. package/dist/commonjs/i18n/locales/en.json +8 -2
  26. package/dist/commonjs/index.d.ts +1 -0
  27. package/dist/commonjs/index.js +16 -1
  28. package/dist/commonjs/storage.d.ts +5 -0
  29. package/dist/commonjs/storage.js +18 -0
  30. package/dist/commonjs/types/enums.d.ts +5 -0
  31. package/dist/commonjs/types/enums.js +5 -0
  32. package/dist/esm/components/AnalyticsDetail.js +6 -2
  33. package/dist/esm/components/AnalyticsEventCard.js +2 -1
  34. package/dist/esm/components/DiffViewer.js +1 -1
  35. package/dist/esm/components/Inspector/BundleTab.js +22 -0
  36. package/dist/esm/components/Inspector/DeviceInfoTab.d.ts +3 -0
  37. package/dist/esm/components/Inspector/DeviceInfoTab.js +796 -0
  38. package/dist/esm/components/Inspector/InspectorHeader.js +13 -15
  39. package/dist/esm/components/Inspector/LogDetail.js +7 -4
  40. package/dist/esm/components/Inspector/MainScreen.js +7 -3
  41. package/dist/esm/components/Inspector/NetworkTab.js +93 -16
  42. package/dist/esm/components/Inspector/PerformanceTab.js +39 -13
  43. package/dist/esm/components/Inspector/SettingsPanel.js +441 -230
  44. package/dist/esm/components/Inspector/StorageTab.d.ts +3 -0
  45. package/dist/esm/components/Inspector/StorageTab.js +853 -0
  46. package/dist/esm/components/Inspector/TabBar.js +15 -1
  47. package/dist/esm/components/Inspector/TelemetryConsentModal.js +11 -2
  48. package/dist/esm/components/Inspector/UpdateAvailableModal.js +4 -1
  49. package/dist/esm/components/NetworkIcons.d.ts +6 -0
  50. package/dist/esm/components/NetworkIcons.js +23 -0
  51. package/dist/esm/constants/version.d.ts +1 -1
  52. package/dist/esm/constants/version.js +1 -1
  53. package/dist/esm/customHooks/bundleAnalyzer.js +44 -22
  54. package/dist/esm/customHooks/storageInspector.d.ts +67 -0
  55. package/dist/esm/customHooks/storageInspector.js +469 -0
  56. package/dist/esm/i18n/locales/en.json +8 -2
  57. package/dist/esm/index.d.ts +1 -0
  58. package/dist/esm/index.js +5 -0
  59. package/dist/esm/storage.d.ts +5 -0
  60. package/dist/esm/storage.js +5 -0
  61. package/dist/esm/types/enums.d.ts +5 -0
  62. package/dist/esm/types/enums.js +5 -0
  63. package/package.json +9 -1
  64. package/src/analytics.ts +35 -0
  65. package/src/bundle.ts +25 -0
  66. package/src/components/AnalyticsDetail.tsx +870 -0
  67. package/src/components/AnalyticsEventCard.tsx +442 -0
  68. package/src/components/AnalyticsGraph.tsx +583 -0
  69. package/src/components/AnimatedEntrance.tsx +64 -0
  70. package/src/components/AppHeaderLogo.tsx +109 -0
  71. package/src/components/BrandCircleIcon.tsx +144 -0
  72. package/src/components/BrandSquareIcon.tsx +144 -0
  73. package/src/components/CodeSnippet.tsx +725 -0
  74. package/src/components/ConsoleLogCard.tsx +628 -0
  75. package/src/components/CopyButton.tsx +87 -0
  76. package/src/components/DiffViewer.tsx +82 -0
  77. package/src/components/DomainHeader.tsx +237 -0
  78. package/src/components/EmptyState.tsx +73 -0
  79. package/src/components/EndOfListFooter.tsx +100 -0
  80. package/src/components/ErrorBoundary.tsx +688 -0
  81. package/src/components/HeadersSection.tsx +192 -0
  82. package/src/components/HighlightText.tsx +100 -0
  83. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  84. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  85. package/src/components/Inspector/BundleTab.tsx +4760 -0
  86. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  87. package/src/components/Inspector/CrashDetail.tsx +941 -0
  88. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  89. package/src/components/Inspector/CrashTab.tsx +871 -0
  90. package/src/components/Inspector/DeviceInfoTab.tsx +1204 -0
  91. package/src/components/Inspector/FabLauncher.tsx +80 -0
  92. package/src/components/Inspector/InspectorContext.tsx +28 -0
  93. package/src/components/Inspector/InspectorHeader.tsx +972 -0
  94. package/src/components/Inspector/LogDetail.tsx +1431 -0
  95. package/src/components/Inspector/MainScreen.tsx +262 -0
  96. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  97. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  98. package/src/components/Inspector/NetworkTab.tsx +1183 -0
  99. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  100. package/src/components/Inspector/PerformanceTab.tsx +1928 -0
  101. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  102. package/src/components/Inspector/ReduxTab.tsx +954 -0
  103. package/src/components/Inspector/SettingsPanel.tsx +3447 -0
  104. package/src/components/Inspector/StorageTab.tsx +1048 -0
  105. package/src/components/Inspector/TabBar.tsx +207 -0
  106. package/src/components/Inspector/TelemetryConsentModal.tsx +416 -0
  107. package/src/components/Inspector/UpdateAvailableModal.tsx +557 -0
  108. package/src/components/JsonViewer.tsx +486 -0
  109. package/src/components/LogCard.tsx +491 -0
  110. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  111. package/src/components/MetaAccordion.tsx +340 -0
  112. package/src/components/MiniBarChart.tsx +42 -0
  113. package/src/components/MiniLineChart.tsx +33 -0
  114. package/src/components/NetworkIcons.tsx +2221 -0
  115. package/src/components/SectionHeader.tsx +113 -0
  116. package/src/components/SegmentedTabs.tsx +83 -0
  117. package/src/components/Slider.tsx +300 -0
  118. package/src/components/SourcePageCard.tsx +148 -0
  119. package/src/components/Toast.tsx +131 -0
  120. package/src/components/TouchableScale.tsx +91 -0
  121. package/src/components/TreeNode.tsx +186 -0
  122. package/src/console.ts +24 -0
  123. package/src/constants/index.ts +38 -0
  124. package/src/constants/version.ts +3 -0
  125. package/src/crash.ts +32 -0
  126. package/src/customHooks/analyticsLogger.ts +336 -0
  127. package/src/customHooks/bundleAnalyzer.ts +1256 -0
  128. package/src/customHooks/consoleLogger.ts +497 -0
  129. package/src/customHooks/crashHandler.ts +944 -0
  130. package/src/customHooks/logFilters.ts +32 -0
  131. package/src/customHooks/networkLogger.ts +419 -0
  132. package/src/customHooks/performanceTracker.ts +1014 -0
  133. package/src/customHooks/reduxLogger.ts +406 -0
  134. package/src/customHooks/storageInspector.ts +509 -0
  135. package/src/customHooks/useAccordion.tsx +60 -0
  136. package/src/decorators/index.ts +184 -0
  137. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  138. package/src/helpers/index.ts +860 -0
  139. package/src/helpers/memoryManager.ts +138 -0
  140. package/src/helpers/searchQueryParser.ts +283 -0
  141. package/src/helpers/settingsStore.ts +171 -0
  142. package/src/helpers/telemetry.ts +505 -0
  143. package/src/helpers/toast.ts +17 -0
  144. package/src/i18n/index.ts +69 -0
  145. package/src/i18n/locales/en.json +1058 -0
  146. package/src/index.tsx +2356 -0
  147. package/src/native/NativeInspector.ts +397 -0
  148. package/src/native/NativeNetworkInspector.ts +24 -0
  149. package/src/network.ts +22 -0
  150. package/src/performance.ts +38 -0
  151. package/src/redux.ts +23 -0
  152. package/src/storage.ts +18 -0
  153. package/src/styles/AppColors.ts +408 -0
  154. package/src/styles/AppFonts.ts +8 -0
  155. package/src/styles/common.ts +209 -0
  156. package/src/styles/index.ts +1570 -0
  157. package/src/types/enums.ts +199 -0
  158. package/src/types/index.ts +34 -0
  159. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,871 @@
1
+ import React, {useCallback, useEffect, useMemo, useState} from 'react';
2
+ import {
3
+ Alert,
4
+ FlatList,
5
+ Modal,
6
+ Platform,
7
+ Pressable,
8
+ ScrollView,
9
+ StyleSheet,
10
+ Text,
11
+ TextInput,
12
+ TouchableOpacity,
13
+ View,
14
+ } from 'react-native';
15
+ import {useTranslation} from '../../i18n';
16
+ import {useInspector} from './InspectorContext';
17
+ import TouchableScale from '../TouchableScale';
18
+ import AnimatedEntrance from '../AnimatedEntrance';
19
+ import EmptyState from '../EmptyState';
20
+ import EndOfListFooter from '../EndOfListFooter';
21
+ import HighlightText from '../HighlightText';
22
+ import JsonViewer from '../JsonViewer';
23
+ import styles from '../../styles';
24
+ import {AppColors} from '../../styles/AppColors';
25
+ import {AppFonts} from '../../styles/AppFonts';
26
+ import {
27
+ CrashRecord,
28
+ ParsedStackFrame,
29
+ CrashBreadcrumb,
30
+ } from '../../types';
31
+ import {
32
+ CrashFilterType,
33
+ CrashType,
34
+ } from '../../types/enums';
35
+ import {
36
+ clearCrashRecords,
37
+ } from '../../customHooks/crashHandler';
38
+ import CrashFilterModal, {
39
+ CrashFilters,
40
+ DEFAULT_CRASH_FILTERS,
41
+ isCrashFiltersDefault,
42
+ } from './CrashFilterModal';
43
+ import {
44
+ CrashIcon,
45
+ SearchIcon,
46
+ ClearIcon,
47
+ TrashIcon,
48
+ WarningTriangleIcon,
49
+ TerminalIcon,
50
+ LayersIcon,
51
+ ClockIcon,
52
+ ShieldAlertIcon,
53
+ CircleAlertIcon,
54
+ CircleCheckIcon,
55
+ CloseWhite,
56
+ WhiteBackNavigation,
57
+ GlobeIcon,
58
+ FlameIcon,
59
+ JsIcon,
60
+ HourglassIcon,
61
+ SkullIcon,
62
+ LayoutIcon,
63
+ ChipIcon,
64
+ AppleIcon,
65
+ AndroidIcon,
66
+ RepeatIcon,
67
+ CodeBracketsIcon,
68
+ FilterIcon,
69
+ } from '../NetworkIcons';
70
+
71
+ const getRelativeTime = (timestamp: number): string => {
72
+ const now = Date.now();
73
+ const diff = Math.max(0, now - timestamp);
74
+ const seconds = Math.floor(diff / 1000);
75
+ const minutes = Math.floor(seconds / 60);
76
+ const hours = Math.floor(minutes / 60);
77
+ const days = Math.floor(hours / 24);
78
+
79
+ if (seconds < 60) return `${seconds}s ago`;
80
+ if (minutes < 60) return `${minutes}m ago`;
81
+ if (hours < 24) return `${hours}h ago`;
82
+ return `${days}d ago`;
83
+ };
84
+
85
+
86
+
87
+ const CrashTab = React.memo(() => {
88
+ const {t} = useTranslation();
89
+ const {crashRecords, selectedCrash, setSelectedCrash} = useInspector();
90
+
91
+ const [searchQuery, setSearchQuery] = useState('');
92
+ const [filterType, setFilterType] = useState<CrashFilterType>('all');
93
+ const [crashFilters, setCrashFilters] = useState<CrashFilters>(
94
+ DEFAULT_CRASH_FILTERS,
95
+ );
96
+ const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
97
+ const [detailSubTab, setDetailSubTab] = useState<
98
+ 'stack' | 'device' | 'breadcrumbs' | 'raw'
99
+ >('stack');
100
+
101
+ // Calculate statistics
102
+ const stats = useMemo(() => {
103
+ const total = crashRecords.length;
104
+ let fatalCount = 0;
105
+ let jsCount = 0;
106
+ let promiseCount = 0;
107
+ let renderCount = 0;
108
+ let nativeCount = 0;
109
+
110
+ crashRecords.forEach(c => {
111
+ if (c.isFatal) fatalCount++;
112
+ if (c.type === 'js') jsCount++;
113
+ else if (c.type === 'promise') promiseCount++;
114
+ else if (c.type === 'render') renderCount++;
115
+ else if (c.type === 'native') nativeCount++;
116
+ });
117
+
118
+ return {total, fatalCount, jsCount, promiseCount, renderCount, nativeCount};
119
+ }, [crashRecords]);
120
+
121
+ // Filtered crash list
122
+ const filteredList = useMemo(() => {
123
+ let list = crashRecords.filter(item => {
124
+ // Type filter
125
+ if (filterType === 'fatal' && !item.isFatal) return false;
126
+ if (filterType === 'js' && item.type !== 'js') return false;
127
+ if (filterType === 'promise' && item.type !== 'promise') return false;
128
+ if (filterType === 'render' && item.type !== 'render') return false;
129
+ if (filterType === 'native' && item.type !== 'native') return false;
130
+
131
+ // Modal filters: crash types
132
+ if (crashFilters.types.size > 0 && !crashFilters.types.has('all')) {
133
+ const matches =
134
+ (crashFilters.types.has('fatal') && item.isFatal) ||
135
+ (crashFilters.types.has('js') && item.type === 'js') ||
136
+ (crashFilters.types.has('promise') && item.type === 'promise') ||
137
+ (crashFilters.types.has('render') && item.type === 'render') ||
138
+ (crashFilters.types.has('native') && item.type === 'native');
139
+ if (!matches) return false;
140
+ }
141
+
142
+ // Modal filters: time window
143
+ if (crashFilters.timeWindow !== 'all') {
144
+ const now = Date.now();
145
+ const cutoff =
146
+ crashFilters.timeWindow === '15m'
147
+ ? now - 15 * 60 * 1000
148
+ : crashFilters.timeWindow === '1h'
149
+ ? now - 60 * 60 * 1000
150
+ : crashFilters.timeWindow === '24h'
151
+ ? now - 24 * 60 * 60 * 1000
152
+ : now - 7 * 24 * 60 * 60 * 1000;
153
+ if (item.timestamp < cutoff) return false;
154
+ }
155
+
156
+ // Modal filters: platform
157
+ if (crashFilters.platform !== 'all') {
158
+ const platform = (item.deviceInfo?.platform || '').toLowerCase();
159
+ if (platform !== crashFilters.platform) return false;
160
+ }
161
+
162
+ // Modal filters: engine
163
+ if (crashFilters.engine !== 'all') {
164
+ const isHermes = item.deviceInfo?.isHermes;
165
+ if (crashFilters.engine === 'hermes' && isHermes !== true) return false;
166
+ if (crashFilters.engine === 'jsc' && (isHermes === true)) return false;
167
+ }
168
+
169
+ // Search filter
170
+ if (searchQuery.trim().length > 0) {
171
+ const queryTokens = searchQuery.trim().toLowerCase().split(/\s+/).filter(Boolean);
172
+ const searchTarget = [
173
+ item.message || '',
174
+ item.name || '',
175
+ item.stack || '',
176
+ item.type || '',
177
+ item.deviceInfo?.platform || '',
178
+ ...(item.breadcrumbs || []).map(b => `${b.type || ''} ${b.message || ''}`),
179
+ ].join(' ').toLowerCase();
180
+
181
+ return queryTokens.every(token => searchTarget.includes(token));
182
+ }
183
+
184
+ return true;
185
+ });
186
+
187
+ // Modal filters: sort order
188
+ if (crashFilters.sortBy === 'time_asc') {
189
+ list = [...list].sort((a, b) => a.timestamp - b.timestamp);
190
+ }
191
+
192
+ return list;
193
+ }, [crashRecords, filterType, searchQuery, crashFilters]);
194
+
195
+ const handleClearAll = () => {
196
+ Alert.alert(
197
+ t('crash.clearTitle'),
198
+ t('crash.clearMessage'),
199
+ [
200
+ {text: t('crash.clearCancel'), style: 'cancel'},
201
+ {
202
+ text: t('crash.clearConfirm'),
203
+ style: 'destructive',
204
+ onPress: () => clearCrashRecords(),
205
+ },
206
+ ],
207
+ );
208
+ };
209
+
210
+ const renderCard = useCallback(
211
+ ({item, index}: {item: CrashRecord; index: number}) => {
212
+ const isFatal = item.isFatal;
213
+ const typeLabel = item.type.toUpperCase();
214
+ const topFrame = item.parsedStack && item.parsedStack.length > 0
215
+ ? item.parsedStack.find(f => f.isAppCode) || item.parsedStack[0]
216
+ : null;
217
+
218
+ const typeBadgeBg =
219
+ item.type === 'native'
220
+ ? `${AppColors.errorColor}22`
221
+ : item.type === 'render'
222
+ ? `${AppColors.purple}22`
223
+ : item.type === 'promise'
224
+ ? `${AppColors.darkOrange}22`
225
+ : `${AppColors.offerPurple}22`;
226
+
227
+ const typeBadgeColor =
228
+ item.type === 'native'
229
+ ? AppColors.errorColor
230
+ : item.type === 'render'
231
+ ? AppColors.purple
232
+ : item.type === 'promise'
233
+ ? AppColors.darkOrange
234
+ : AppColors.offerPurple;
235
+
236
+ return (
237
+ <AnimatedEntrance index={index} distance={8}>
238
+ <TouchableScale
239
+ onPress={() => setSelectedCrash(item)}
240
+ style={[
241
+ localStyles.cardContainer,
242
+ isFatal && localStyles.fatalCardBorder,
243
+ ]}>
244
+ {/* Header: #S.No, Badges & Timestamp */}
245
+ <View style={localStyles.cardHeader}>
246
+ <View style={localStyles.badgeRow}>
247
+ <View style={localStyles.sNoBadge}>
248
+ <Text style={localStyles.sNoBadgeText}>#{index + 1}</Text>
249
+ </View>
250
+
251
+ {isFatal ? (
252
+ <View style={localStyles.fatalBadge}>
253
+ <FlameIcon size={10} color={AppColors.white} />
254
+ <Text style={localStyles.fatalBadgeText}>{t('crash.fatalBadge')}</Text>
255
+ </View>
256
+ ) : (
257
+ <View style={localStyles.caughtBadge}>
258
+ <Text style={localStyles.caughtBadgeText}>{t('crash.handledBadge')}</Text>
259
+ </View>
260
+ )}
261
+
262
+ <View
263
+ style={[
264
+ localStyles.typeBadge,
265
+ {backgroundColor: typeBadgeBg},
266
+ ]}>
267
+ <Text
268
+ style={[
269
+ localStyles.typeBadgeText,
270
+ {color: typeBadgeColor},
271
+ ]}>
272
+ {typeLabel}
273
+ </Text>
274
+ </View>
275
+ </View>
276
+
277
+ <View style={localStyles.timeRow}>
278
+ <ClockIcon size={11} color={AppColors.grayTextWeak} />
279
+ <Text style={localStyles.timestampText}>
280
+ {item.timeStr || new Date(item.timestamp).toLocaleTimeString()}
281
+ </Text>
282
+ <Text style={localStyles.relativeTimeText}>
283
+ {getRelativeTime(item.timestamp)}
284
+ </Text>
285
+ </View>
286
+ </View>
287
+
288
+ {/* Error Message */}
289
+ <View style={{marginVertical: 6}}>
290
+ <HighlightText
291
+ text={item.message || t('crash.unknownException')}
292
+ highlight={searchQuery}
293
+ numberOfLines={3}
294
+ style={localStyles.errorMessageText}
295
+ />
296
+ </View>
297
+
298
+ {/* Top stack frame location if available */}
299
+ {topFrame && (
300
+ <View style={localStyles.locationRow}>
301
+ <TerminalIcon size={12} color={AppColors.grayTextWeak} />
302
+ <Text
303
+ numberOfLines={1}
304
+ ellipsizeMode="middle"
305
+ style={localStyles.locationText}>
306
+ {topFrame.method !== '<anonymous>' ? `${topFrame.method} ` : ''}
307
+ <Text style={{color: AppColors.purple, fontFamily: AppFonts.interBold}}>
308
+ {topFrame.file}:{topFrame.lineNumber}
309
+ </Text>
310
+ </Text>
311
+ </View>
312
+ )}
313
+
314
+ {/* Footer row: Actions */}
315
+ <View style={localStyles.cardFooter}>
316
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap'}}>
317
+ {item.deviceInfo && (
318
+ <View style={localStyles.metaPill}>
319
+ {item.deviceInfo.platform === 'ios' ? (
320
+ <AppleIcon size={10} color={AppColors.grayText} />
321
+ ) : item.deviceInfo.platform === 'android' ? (
322
+ <AndroidIcon size={10} color={AppColors.grayText} />
323
+ ) : null}
324
+ <Text style={localStyles.metaPillText}>
325
+ {item.deviceInfo.platform?.toUpperCase()}{' '}
326
+ {item.deviceInfo.osVersion || ''}
327
+ </Text>
328
+ </View>
329
+ )}
330
+ {item.deviceInfo && (
331
+ <View style={localStyles.metaPill}>
332
+ <Text style={localStyles.metaPillText}>
333
+ {item.deviceInfo.isHermes ? 'Hermes' : 'JSC'}
334
+ </Text>
335
+ </View>
336
+ )}
337
+ {item.parsedStack && item.parsedStack.length > 0 && (
338
+ <View style={localStyles.metaPill}>
339
+ <CodeBracketsIcon size={10} color={AppColors.grayText} />
340
+ <Text style={localStyles.metaPillText}>
341
+ {item.parsedStack.length}
342
+ </Text>
343
+ </View>
344
+ )}
345
+ {item.breadcrumbs && item.breadcrumbs.length > 0 && (
346
+ <View style={localStyles.metaPill}>
347
+ <RepeatIcon size={10} color={AppColors.grayText} />
348
+ <Text style={localStyles.metaPillText}>
349
+ {item.breadcrumbs.length} trail
350
+ </Text>
351
+ </View>
352
+ )}
353
+ {item.memoryInfo && (
354
+ <View style={localStyles.metaPill}>
355
+ <Text style={localStyles.metaPillText}>
356
+ {item.memoryInfo.usedJSHeapSize} MB
357
+ </Text>
358
+ </View>
359
+ )}
360
+ </View>
361
+ </View>
362
+ </TouchableScale>
363
+ </AnimatedEntrance>
364
+ );
365
+ },
366
+ [searchQuery, setSelectedCrash],
367
+ );
368
+
369
+ return (
370
+ <View style={localStyles.container}>
371
+ {crashRecords.length > 0 && (
372
+ <>
373
+ {/* ─── Top Stats Bar ─── */}
374
+ <View style={localStyles.statsBar}>
375
+ <ScrollView
376
+ horizontal
377
+ showsHorizontalScrollIndicator={false}
378
+ contentContainerStyle={localStyles.statsScrollContent}>
379
+ <View style={[localStyles.statChip, localStyles.statChipActive]}>
380
+ <Text style={localStyles.statChipValue}>{stats.total}</Text>
381
+ <Text style={localStyles.statChipLabel}>{t('crash.statCrashes')}</Text>
382
+ </View>
383
+
384
+ <View style={localStyles.statChip}>
385
+ <Text style={[localStyles.statChipValue, {color: AppColors.errorColor}]}>
386
+ {stats.fatalCount}
387
+ </Text>
388
+ <Text style={localStyles.statChipLabel}>{t('crash.statFatal')}</Text>
389
+ </View>
390
+
391
+ <View style={localStyles.statChip}>
392
+ <Text style={[localStyles.statChipValue, {color: AppColors.offerPurple}]}>
393
+ {stats.jsCount}
394
+ </Text>
395
+ <Text style={localStyles.statChipLabel}>{t('crash.statJsErrors')}</Text>
396
+ </View>
397
+
398
+ <View style={localStyles.statChip}>
399
+ <Text style={[localStyles.statChipValue, {color: AppColors.darkOrange}]}>
400
+ {stats.promiseCount}
401
+ </Text>
402
+ <Text style={localStyles.statChipLabel}>{t('crash.statPromises')}</Text>
403
+ </View>
404
+
405
+ <View style={localStyles.statChip}>
406
+ <Text style={[localStyles.statChipValue, {color: AppColors.purple}]}>
407
+ {stats.renderCount}
408
+ </Text>
409
+ <Text style={localStyles.statChipLabel}>{t('crash.statRender')}</Text>
410
+ </View>
411
+
412
+ <View style={localStyles.statChip}>
413
+ <Text style={[localStyles.statChipValue, {color: AppColors.skyBlue}]}>
414
+ {stats.nativeCount}
415
+ </Text>
416
+ <Text style={localStyles.statChipLabel}>{t('crash.statNative')}</Text>
417
+ </View>
418
+ </ScrollView>
419
+ </View>
420
+
421
+ {/* ─── Search & Controls ─── */}
422
+ <View style={localStyles.controlsContainer}>
423
+ <View style={localStyles.searchRow}>
424
+ <View style={localStyles.searchInputWrapper}>
425
+ <SearchIcon color={AppColors.grayTextWeak} size={14} />
426
+ <TextInput
427
+ value={searchQuery}
428
+ onChangeText={setSearchQuery}
429
+ placeholder={t('crash.searchPlaceholder')}
430
+ placeholderTextColor={AppColors.grayTextWeak}
431
+ style={localStyles.searchInput}
432
+ autoCapitalize="none"
433
+ autoCorrect={false}
434
+ />
435
+ {searchQuery.length > 0 && (
436
+ <TouchableOpacity onPress={() => setSearchQuery('')} hitSlop={8}>
437
+ <ClearIcon color={AppColors.grayTextWeak} size={14} />
438
+ </TouchableOpacity>
439
+ )}
440
+ </View>
441
+
442
+ {/* Crash Filters */}
443
+ <TouchableScale
444
+ onPress={() => setIsFilterModalOpen(true)}
445
+ style={[
446
+ localStyles.filterButton,
447
+ !isCrashFiltersDefault(crashFilters) && {
448
+ borderColor: `${AppColors.brandPurple}60`,
449
+ backgroundColor: `${AppColors.brandPurple}15`,
450
+ },
451
+ ]}>
452
+ <FilterIcon
453
+ size={14}
454
+ color={
455
+ isCrashFiltersDefault(crashFilters)
456
+ ? AppColors.grayTextStrong
457
+ : AppColors.brandPurple
458
+ }
459
+ />
460
+ {!isCrashFiltersDefault(crashFilters) && (
461
+ <View
462
+ style={{
463
+ position: 'absolute',
464
+ top: 2,
465
+ right: 2,
466
+ width: 7,
467
+ height: 7,
468
+ borderRadius: 3.5,
469
+ backgroundColor: AppColors.darkOrange,
470
+ borderWidth: 1,
471
+ borderColor: AppColors.white,
472
+ }}
473
+ />
474
+ )}
475
+ </TouchableScale>
476
+
477
+ {/* Clear Crashes */}
478
+ <TouchableScale
479
+ onPress={handleClearAll}
480
+ style={localStyles.clearButton}>
481
+ <TrashIcon size={14} color={AppColors.errorColor} />
482
+ </TouchableScale>
483
+ </View>
484
+
485
+ {/* ─── Filter Chips ─── */}
486
+ <ScrollView
487
+ horizontal
488
+ showsHorizontalScrollIndicator={false}
489
+ contentContainerStyle={localStyles.filterScrollContent}>
490
+ {(
491
+ [
492
+ {key: 'all', label: t('crash.filterAll'), Icon: LayersIcon},
493
+ {key: 'fatal', label: t('crash.filterFatal'), Icon: SkullIcon},
494
+ {key: 'js', label: t('crash.filterJsError'), Icon: JsIcon},
495
+ {key: 'promise', label: t('crash.filterPromise'), Icon: HourglassIcon},
496
+ {key: 'render', label: t('crash.filterRender'), Icon: LayoutIcon},
497
+ {key: 'native', label: t('crash.filterNative'), Icon: ChipIcon},
498
+ ] as const
499
+ ).map(chip => {
500
+ const isActive = filterType === chip.key;
501
+ return (
502
+ <TouchableOpacity
503
+ key={chip.key}
504
+ onPress={() => setFilterType(chip.key)}
505
+ style={[
506
+ localStyles.filterChip,
507
+ isActive && localStyles.filterChipActive,
508
+ ]}>
509
+ <chip.Icon
510
+ size={12}
511
+ color={isActive ? AppColors.white : AppColors.grayTextStrong}
512
+ />
513
+ <Text
514
+ style={[
515
+ localStyles.filterChipText,
516
+ isActive && localStyles.filterChipTextActive,
517
+ ]}>
518
+ {chip.label}
519
+ </Text>
520
+ </TouchableOpacity>
521
+ );
522
+ })}
523
+ </ScrollView>
524
+ </View>
525
+ </>
526
+ )}
527
+
528
+ {/* ─── Crash List or Empty State ─── */}
529
+ {filteredList.length === 0 ? (
530
+ <ScrollView
531
+ contentContainerStyle={localStyles.emptyContainer}
532
+ showsVerticalScrollIndicator={false}>
533
+ <View style={localStyles.emptyIconCircle}>
534
+ <ShieldAlertIcon size={38} color={AppColors.greenColor} />
535
+ </View>
536
+ <Text style={localStyles.emptyTitle}>{t('crash.emptyTitle')}</Text>
537
+ <Text style={localStyles.emptySub}>
538
+ {searchQuery
539
+ ? t('crash.emptySearchSubtitle')
540
+ : t('crash.emptySubtitle')}
541
+ </Text>
542
+ </ScrollView>
543
+ ) : (
544
+ <FlatList
545
+ data={filteredList}
546
+ keyExtractor={item => item.id}
547
+ renderItem={renderCard}
548
+ initialNumToRender={12}
549
+ maxToRenderPerBatch={8}
550
+ windowSize={5}
551
+ removeClippedSubviews={true}
552
+ renderToHardwareTextureAndroid={true}
553
+ contentContainerStyle={localStyles.listContent}
554
+ showsVerticalScrollIndicator={false}
555
+ ListFooterComponent={<EndOfListFooter />}
556
+ />
557
+ )}
558
+
559
+ {/* Filter Modal */}
560
+ <CrashFilterModal
561
+ visible={isFilterModalOpen}
562
+ onClose={() => setIsFilterModalOpen(false)}
563
+ filters={crashFilters}
564
+ onApply={setCrashFilters}
565
+ searchQuery={searchQuery}
566
+ />
567
+ </View>
568
+ );
569
+ });
570
+
571
+ export default CrashTab;
572
+
573
+ const localStyles = StyleSheet.create({
574
+ container: {
575
+ flex: 1,
576
+ backgroundColor: AppColors.grayBackground,
577
+ },
578
+ statsBar: {
579
+ backgroundColor: AppColors.primaryLight,
580
+ borderBottomWidth: 1,
581
+ borderBottomColor: AppColors.grayBorderSecondary,
582
+ paddingVertical: 10,
583
+ },
584
+ statsScrollContent: {
585
+ paddingHorizontal: 16,
586
+ gap: 8,
587
+ },
588
+ statChip: {
589
+ flexDirection: 'row',
590
+ alignItems: 'center',
591
+ gap: 6,
592
+ paddingHorizontal: 10,
593
+ paddingVertical: 5,
594
+ borderRadius: 8,
595
+ backgroundColor: AppColors.purpleShade50,
596
+ borderWidth: 1,
597
+ borderColor: `${AppColors.purple}14`,
598
+ },
599
+ statChipActive: {
600
+ backgroundColor: `${AppColors.purple}14`,
601
+ borderColor: `${AppColors.purple}33`,
602
+ },
603
+ statChipValue: {
604
+ fontFamily: AppFonts.interBold,
605
+ fontSize: 13,
606
+ color: AppColors.primaryBlack,
607
+ },
608
+ statChipLabel: {
609
+ fontFamily: AppFonts.interMedium,
610
+ fontSize: 11,
611
+ color: AppColors.grayText,
612
+ },
613
+ controlsContainer: {
614
+ paddingHorizontal: 16,
615
+ paddingTop: 12,
616
+ paddingBottom: 6,
617
+ backgroundColor: AppColors.grayBackground,
618
+ },
619
+ searchRow: {
620
+ flexDirection: 'row',
621
+ alignItems: 'center',
622
+ gap: 8,
623
+ },
624
+ searchInputWrapper: {
625
+ flex: 1,
626
+ flexDirection: 'row',
627
+ alignItems: 'center',
628
+ backgroundColor: AppColors.primaryLight,
629
+ borderWidth: 1,
630
+ borderColor: AppColors.grayBorderSecondary,
631
+ borderRadius: 10,
632
+ paddingHorizontal: 10,
633
+ height: 38,
634
+ gap: 8,
635
+ },
636
+ searchInput: {
637
+ flex: 1,
638
+ fontFamily: AppFonts.interRegular,
639
+ fontSize: 13,
640
+ color: AppColors.primaryBlack,
641
+ paddingVertical: 0,
642
+ },
643
+ clearButton: {
644
+ width: 38,
645
+ height: 38,
646
+ borderRadius: 10,
647
+ backgroundColor: `${AppColors.errorColor}14`,
648
+ borderWidth: 1,
649
+ borderColor: `${AppColors.errorColor}22`,
650
+ alignItems: 'center',
651
+ justifyContent: 'center',
652
+ },
653
+ filterButton: {
654
+ width: 38,
655
+ height: 38,
656
+ borderRadius: 10,
657
+ backgroundColor: `${AppColors.brandPurple}0F`,
658
+ borderWidth: 1,
659
+ borderColor: `${AppColors.brandPurple}30`,
660
+ alignItems: 'center',
661
+ justifyContent: 'center',
662
+ },
663
+ filterScrollContent: {
664
+ paddingVertical: 10,
665
+ gap: 6,
666
+ },
667
+ filterChip: {
668
+ flexDirection: 'row',
669
+ alignItems: 'center',
670
+ gap: 5,
671
+ paddingHorizontal: 12,
672
+ paddingVertical: 6,
673
+ borderRadius: 8,
674
+ backgroundColor: AppColors.primaryLight,
675
+ borderWidth: 1,
676
+ borderColor: AppColors.grayBorderSecondary,
677
+ },
678
+ filterChipActive: {
679
+ backgroundColor: AppColors.purple,
680
+ borderColor: AppColors.purple,
681
+ },
682
+ filterChipText: {
683
+ fontFamily: AppFonts.interMedium,
684
+ fontSize: 12,
685
+ color: AppColors.grayText,
686
+ },
687
+ filterChipTextActive: {
688
+ fontFamily: AppFonts.interBold,
689
+ color: AppColors.white,
690
+ },
691
+ listContent: {
692
+ paddingHorizontal: 16,
693
+ paddingTop: 4,
694
+ paddingBottom: 24,
695
+ gap: 10,
696
+ },
697
+ cardContainer: {
698
+ backgroundColor: AppColors.primaryLight,
699
+ borderRadius: 12,
700
+ borderWidth: 1,
701
+ borderColor: AppColors.grayBorderSecondary,
702
+ padding: 14,
703
+ shadowColor: AppColors.shadowColorString,
704
+ shadowOffset: {width: 0, height: 1},
705
+ shadowOpacity: 0.05,
706
+ shadowRadius: 3,
707
+ elevation: 2,
708
+ },
709
+ fatalCardBorder: {
710
+ borderColor: `${AppColors.errorColor}44`,
711
+ },
712
+ cardHeader: {
713
+ flexDirection: 'row',
714
+ alignItems: 'center',
715
+ justifyContent: 'space-between',
716
+ },
717
+ badgeRow: {
718
+ flexDirection: 'row',
719
+ alignItems: 'center',
720
+ gap: 6,
721
+ flexShrink: 1,
722
+ },
723
+ sNoBadge: {
724
+ backgroundColor: AppColors.grayBackground,
725
+ borderWidth: 1,
726
+ borderColor: AppColors.grayBorderSecondary,
727
+ paddingHorizontal: 6,
728
+ paddingVertical: 3,
729
+ borderRadius: 5,
730
+ },
731
+ sNoBadgeText: {
732
+ fontFamily: AppFonts.interBold,
733
+ fontSize: 9.5,
734
+ color: AppColors.grayText,
735
+ letterSpacing: 0.3,
736
+ },
737
+ fatalBadge: {
738
+ flexDirection: 'row',
739
+ alignItems: 'center',
740
+ gap: 4,
741
+ backgroundColor: AppColors.errorColor,
742
+ paddingHorizontal: 6,
743
+ paddingVertical: 3,
744
+ borderRadius: 5,
745
+ },
746
+ fatalBadgeText: {
747
+ fontFamily: AppFonts.interBold,
748
+ fontSize: 9.5,
749
+ color: AppColors.white,
750
+ letterSpacing: 0.5,
751
+ },
752
+ caughtBadge: {
753
+ backgroundColor: `${AppColors.greenColor}1F`,
754
+ paddingHorizontal: 6,
755
+ paddingVertical: 3,
756
+ borderRadius: 5,
757
+ },
758
+ caughtBadgeText: {
759
+ fontFamily: AppFonts.interBold,
760
+ fontSize: 9.5,
761
+ color: AppColors.greenColor,
762
+ letterSpacing: 0.5,
763
+ },
764
+ typeBadge: {
765
+ paddingHorizontal: 6,
766
+ paddingVertical: 3,
767
+ borderRadius: 5,
768
+ },
769
+ typeBadgeText: {
770
+ fontFamily: AppFonts.interBold,
771
+ fontSize: 9.5,
772
+ letterSpacing: 0.5,
773
+ },
774
+ timeRow: {
775
+ flexDirection: 'row',
776
+ alignItems: 'center',
777
+ gap: 4,
778
+ flexShrink: 1,
779
+ },
780
+ timestampText: {
781
+ fontFamily: AppFonts.interRegular,
782
+ fontSize: 11,
783
+ color: AppColors.grayTextWeak,
784
+ },
785
+ relativeTimeText: {
786
+ fontFamily: AppFonts.interBold,
787
+ fontSize: 10,
788
+ color: AppColors.grayTextWeak,
789
+ backgroundColor: AppColors.grayBackground,
790
+ paddingHorizontal: 5,
791
+ paddingVertical: 1,
792
+ borderRadius: 4,
793
+ overflow: 'hidden',
794
+ },
795
+ errorMessageText: {
796
+ fontFamily: AppFonts.interBold,
797
+ fontSize: 13.5,
798
+ color: AppColors.primaryBlack,
799
+ lineHeight: 18,
800
+ },
801
+ locationRow: {
802
+ flexDirection: 'row',
803
+ alignItems: 'center',
804
+ gap: 6,
805
+ marginTop: 2,
806
+ marginBottom: 6,
807
+ backgroundColor: AppColors.purpleShade50,
808
+ paddingHorizontal: 8,
809
+ paddingVertical: 4,
810
+ borderRadius: 6,
811
+ },
812
+ locationText: {
813
+ fontFamily: AppFonts.interRegular,
814
+ fontSize: 11,
815
+ color: AppColors.grayText,
816
+ flex: 1,
817
+ },
818
+ cardFooter: {
819
+ flexDirection: 'row',
820
+ alignItems: 'center',
821
+ justifyContent: 'space-between',
822
+ marginTop: 6,
823
+ paddingTop: 8,
824
+ borderTopWidth: 1,
825
+ borderTopColor: AppColors.dividerColor,
826
+ },
827
+ metaPill: {
828
+ flexDirection: 'row',
829
+ alignItems: 'center',
830
+ gap: 4,
831
+ backgroundColor: AppColors.purpleShade50,
832
+ paddingHorizontal: 6,
833
+ paddingVertical: 2,
834
+ borderRadius: 4,
835
+ },
836
+ metaPillText: {
837
+ fontFamily: AppFonts.interMedium,
838
+ fontSize: 10,
839
+ color: AppColors.grayTextWeak,
840
+ },
841
+ emptyContainer: {
842
+ flexGrow: 1,
843
+ alignItems: 'center',
844
+ justifyContent: 'center',
845
+ padding: 32,
846
+ paddingBottom: 80,
847
+ },
848
+ emptyIconCircle: {
849
+ width: 72,
850
+ height: 72,
851
+ borderRadius: 36,
852
+ backgroundColor: `${AppColors.greenColor}1F`,
853
+ alignItems: 'center',
854
+ justifyContent: 'center',
855
+ marginBottom: 16,
856
+ },
857
+ emptyTitle: {
858
+ fontFamily: AppFonts.interBold,
859
+ fontSize: 17,
860
+ color: AppColors.primaryBlack,
861
+ marginBottom: 6,
862
+ },
863
+ emptySub: {
864
+ fontFamily: AppFonts.interRegular,
865
+ fontSize: 13,
866
+ color: AppColors.grayText,
867
+ textAlign: 'center',
868
+ lineHeight: 18,
869
+ maxWidth: 280,
870
+ },
871
+ });