react-native-inapp-inspector 2.2.2 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/commonjs/constants/version.d.ts +1 -1
  2. package/dist/commonjs/constants/version.js +1 -1
  3. package/dist/esm/constants/version.d.ts +1 -1
  4. package/dist/esm/constants/version.js +1 -1
  5. package/package.json +2 -1
  6. package/src/analytics.ts +35 -0
  7. package/src/bundle.ts +25 -0
  8. package/src/components/AnalyticsDetail.tsx +865 -0
  9. package/src/components/AnalyticsEventCard.tsx +441 -0
  10. package/src/components/AnalyticsGraph.tsx +583 -0
  11. package/src/components/AnimatedEntrance.tsx +64 -0
  12. package/src/components/AppHeaderLogo.tsx +109 -0
  13. package/src/components/BrandCircleIcon.tsx +144 -0
  14. package/src/components/BrandSquareIcon.tsx +144 -0
  15. package/src/components/CodeSnippet.tsx +725 -0
  16. package/src/components/ConsoleLogCard.tsx +628 -0
  17. package/src/components/CopyButton.tsx +87 -0
  18. package/src/components/DiffViewer.tsx +82 -0
  19. package/src/components/DomainHeader.tsx +237 -0
  20. package/src/components/EmptyState.tsx +73 -0
  21. package/src/components/EndOfListFooter.tsx +100 -0
  22. package/src/components/ErrorBoundary.tsx +688 -0
  23. package/src/components/HeadersSection.tsx +192 -0
  24. package/src/components/HighlightText.tsx +100 -0
  25. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  26. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  27. package/src/components/Inspector/BundleTab.tsx +4731 -0
  28. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  29. package/src/components/Inspector/CrashDetail.tsx +941 -0
  30. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  31. package/src/components/Inspector/CrashTab.tsx +871 -0
  32. package/src/components/Inspector/FabLauncher.tsx +80 -0
  33. package/src/components/Inspector/InspectorContext.tsx +28 -0
  34. package/src/components/Inspector/InspectorHeader.tsx +973 -0
  35. package/src/components/Inspector/LogDetail.tsx +1427 -0
  36. package/src/components/Inspector/MainScreen.tsx +258 -0
  37. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  38. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  39. package/src/components/Inspector/NetworkTab.tsx +1095 -0
  40. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  41. package/src/components/Inspector/PerformanceTab.tsx +1894 -0
  42. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  43. package/src/components/Inspector/ReduxTab.tsx +954 -0
  44. package/src/components/Inspector/SettingsPanel.tsx +3181 -0
  45. package/src/components/Inspector/TabBar.tsx +187 -0
  46. package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
  47. package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
  48. package/src/components/JsonViewer.tsx +486 -0
  49. package/src/components/LogCard.tsx +491 -0
  50. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  51. package/src/components/MetaAccordion.tsx +340 -0
  52. package/src/components/MiniBarChart.tsx +42 -0
  53. package/src/components/MiniLineChart.tsx +33 -0
  54. package/src/components/NetworkIcons.tsx +2118 -0
  55. package/src/components/SectionHeader.tsx +113 -0
  56. package/src/components/SegmentedTabs.tsx +83 -0
  57. package/src/components/Slider.tsx +300 -0
  58. package/src/components/SourcePageCard.tsx +148 -0
  59. package/src/components/Toast.tsx +131 -0
  60. package/src/components/TouchableScale.tsx +91 -0
  61. package/src/components/TreeNode.tsx +186 -0
  62. package/src/console.ts +24 -0
  63. package/src/constants/index.ts +38 -0
  64. package/src/constants/version.ts +3 -0
  65. package/src/crash.ts +32 -0
  66. package/src/customHooks/analyticsLogger.ts +336 -0
  67. package/src/customHooks/bundleAnalyzer.ts +1231 -0
  68. package/src/customHooks/consoleLogger.ts +497 -0
  69. package/src/customHooks/crashHandler.ts +944 -0
  70. package/src/customHooks/logFilters.ts +32 -0
  71. package/src/customHooks/networkLogger.ts +419 -0
  72. package/src/customHooks/performanceTracker.ts +1014 -0
  73. package/src/customHooks/reduxLogger.ts +406 -0
  74. package/src/customHooks/useAccordion.tsx +60 -0
  75. package/src/decorators/index.ts +184 -0
  76. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  77. package/src/helpers/index.ts +860 -0
  78. package/src/helpers/memoryManager.ts +138 -0
  79. package/src/helpers/searchQueryParser.ts +283 -0
  80. package/src/helpers/settingsStore.ts +171 -0
  81. package/src/helpers/telemetry.ts +505 -0
  82. package/src/helpers/toast.ts +17 -0
  83. package/src/i18n/index.ts +69 -0
  84. package/src/i18n/locales/en.json +1052 -0
  85. package/src/index.tsx +2336 -0
  86. package/src/native/NativeInspector.ts +397 -0
  87. package/src/native/NativeNetworkInspector.ts +24 -0
  88. package/src/network.ts +22 -0
  89. package/src/performance.ts +38 -0
  90. package/src/redux.ts +23 -0
  91. package/src/styles/AppColors.ts +408 -0
  92. package/src/styles/AppFonts.ts +8 -0
  93. package/src/styles/common.ts +209 -0
  94. package/src/styles/index.ts +1570 -0
  95. package/src/types/enums.ts +194 -0
  96. package/src/types/index.ts +34 -0
  97. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,1336 @@
1
+ import React, {useCallback, useState, useMemo, useEffect} from 'react';
2
+ import {
3
+ FlatList,
4
+ Pressable,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ View,
9
+ } from 'react-native';
10
+ import {animateNextLayout, useInspector} from './InspectorContext';
11
+ import AnalyticsEventCard from '../AnalyticsEventCard';
12
+ import AnalyticsFilterModal from './AnalyticsFilterModal';
13
+ import EndOfListFooter from '../EndOfListFooter';
14
+ import styles from '../../styles';
15
+ import {AppColors} from '../../styles/AppColors';
16
+ import {AppFonts} from '../../styles/AppFonts';
17
+ import {useTranslation} from '../../i18n';
18
+ import Svg, {
19
+ Path,
20
+ Rect,
21
+ Line,
22
+ Circle,
23
+ G,
24
+ Defs,
25
+ LinearGradient as SvgLinearGradient,
26
+ Stop,
27
+ } from 'react-native-svg';
28
+ import TouchableScale from '../TouchableScale';
29
+ import {
30
+ getCurrentUserId,
31
+ getCurrentUserProperties,
32
+ getDefaultEventParameters,
33
+ getCollectionEnabled,
34
+ } from '../../customHooks/analyticsLogger';
35
+ import {
36
+ getEventCategory,
37
+ getCategoryColors,
38
+ } from '../../helpers';
39
+ import {
40
+ LayersIcon,
41
+ CheckIcon,
42
+ SearchIcon,
43
+ ClearIcon,
44
+ TrashIcon,
45
+ EmptyRadarIcon,
46
+ HeaderPauseIcon,
47
+ FilterIcon,
48
+ CartIcon,
49
+ GlobeIcon,
50
+ BoltIcon,
51
+ SparkleIcon,
52
+ ClockIcon,
53
+ TargetGoalIcon,
54
+ PinIcon,
55
+ MoneyIcon,
56
+ UserCheckIcon,
57
+ } from '../NetworkIcons';
58
+
59
+ const AnalyticsHeader = React.memo(() => {
60
+ const {t} = useTranslation();
61
+ const {
62
+ analyticsEvents,
63
+ filteredAnalyticsEvents,
64
+ analyticsFilters,
65
+ setAnalyticsFilters,
66
+ analyticsHeaderExpanded,
67
+ setAnalyticsHeaderExpanded,
68
+ } = useInspector();
69
+
70
+ const userId = getCurrentUserId();
71
+ const userProperties = getCurrentUserProperties();
72
+ const defaultParams = getDefaultEventParameters();
73
+ const isTrackingEnabled = getCollectionEnabled();
74
+
75
+ // If there are no analytics events in the session, hide the graph, category breakdown, and realtime card
76
+ if (analyticsEvents.length === 0) {
77
+ return null;
78
+ }
79
+
80
+ const hasUserProps = Object.keys(userProperties).length > 0;
81
+ const hasDefaultParams = Object.keys(defaultParams).length > 0;
82
+ const totalEvents = filteredAnalyticsEvents.length;
83
+
84
+ // Category counts & breakdown across all session events
85
+ const categoryStats = useMemo(() => {
86
+ let ecommerce = 0;
87
+ let page_view = 0;
88
+ let system = 0;
89
+ let custom = 0;
90
+ let totalRevenue = 0;
91
+
92
+ for (const e of analyticsEvents) {
93
+ const cat = getEventCategory(e.name);
94
+ const c = (e as any).count || 1;
95
+ if (cat === 'ecommerce') {
96
+ ecommerce += c;
97
+ const val = e.params?.value ?? e.params?.price;
98
+ if (val && !isNaN(Number(val))) {
99
+ totalRevenue += Number(val);
100
+ }
101
+ } else if (cat === 'page_view') {
102
+ page_view += c;
103
+ } else if (cat === 'system') {
104
+ system += c;
105
+ } else {
106
+ custom += c;
107
+ }
108
+ }
109
+
110
+ const total = ecommerce + page_view + system + custom || 1;
111
+ return {
112
+ ecommerce,
113
+ page_view,
114
+ system,
115
+ custom,
116
+ totalRevenue,
117
+ ecommercePct: Math.round((ecommerce / total) * 100),
118
+ page_viewPct: Math.round((page_view / total) * 100),
119
+ systemPct: Math.round((system / total) * 100),
120
+ customPct: Math.round((custom / total) * 100),
121
+ totalCount: total,
122
+ };
123
+ }, [analyticsEvents]);
124
+
125
+ // Top Screen Views Leaderboard (GA4 Realtime Card)
126
+ const topScreens = useMemo(() => {
127
+ const screenMap = new Map<string, number>();
128
+ for (const e of filteredAnalyticsEvents) {
129
+ const scr =
130
+ e.screenName ||
131
+ e.params?.firebase_screen ||
132
+ e.params?.screen_name ||
133
+ e.params?.firebase_screen_class ||
134
+ e.screenClass;
135
+ if (scr && typeof scr === 'string' && scr.trim() !== '') {
136
+ screenMap.set(scr, (screenMap.get(scr) || 0) + ((e as any).count || 1));
137
+ }
138
+ }
139
+ const arr = Array.from(screenMap.entries()).sort((a, b) => b[1] - a[1]);
140
+ const maxVal = arr.length > 0 ? arr[0][1] : 1;
141
+ return arr.slice(0, 3).map(([name, count]) => ({
142
+ name,
143
+ count,
144
+ pct: Math.round((count / (filteredAnalyticsEvents.length || 1)) * 100),
145
+ barWidthPct: Math.round((count / maxVal) * 100),
146
+ }));
147
+ }, [filteredAnalyticsEvents]);
148
+
149
+ // Multi-select category toggle from top button group
150
+ const toggleCategoryFilter = (cat: string) => {
151
+ animateNextLayout();
152
+ setAnalyticsFilters(prev => {
153
+ if (cat === 'all') {
154
+ return {...prev, categories: new Set(['all'])};
155
+ }
156
+
157
+ const nextCategories = new Set(prev.categories);
158
+ if (nextCategories.has('all')) {
159
+ nextCategories.delete('all');
160
+ nextCategories.add(cat);
161
+ } else {
162
+ if (nextCategories.has(cat)) {
163
+ nextCategories.delete(cat);
164
+ if (nextCategories.size === 0) {
165
+ nextCategories.add('all');
166
+ }
167
+ } else {
168
+ nextCategories.add(cat);
169
+ if (
170
+ nextCategories.has('ecommerce') &&
171
+ nextCategories.has('page_view') &&
172
+ nextCategories.has('system') &&
173
+ nextCategories.has('custom')
174
+ ) {
175
+ return {...prev, categories: new Set(['all'])};
176
+ }
177
+ }
178
+ }
179
+
180
+ return {...prev, categories: nextCategories};
181
+ });
182
+ };
183
+
184
+ // 12-Bucket GA4 Realtime Stacked Histogram & Spline Wave Graph (60s live rolling stream)
185
+ const BUCKET_COUNT = 12;
186
+ const chartHeight = 64;
187
+ const svgWidth = 320;
188
+ const [selectedBucketIdx, setSelectedBucketIdx] = useState<number | null>(null);
189
+ const [liveTick, setLiveTick] = useState<number>(0);
190
+
191
+ useEffect(() => {
192
+ if (!isTrackingEnabled) return;
193
+ const interval = setInterval(() => {
194
+ setLiveTick(t => (t + 1) % 100000);
195
+ }, 1000);
196
+ return () => clearInterval(interval);
197
+ }, [isTrackingEnabled]);
198
+
199
+ const histogramData = useMemo(() => {
200
+ const buckets: {
201
+ index: number;
202
+ ecommerce: number;
203
+ page_view: number;
204
+ system: number;
205
+ custom: number;
206
+ total: number;
207
+ x: number;
208
+ y: number;
209
+ isLatest: boolean;
210
+ ageLabel: string;
211
+ }[] = [];
212
+
213
+ const now = Date.now();
214
+ const windowMs = 60000; // 60s live rolling window
215
+ const bucketDuration = windowMs / BUCKET_COUNT; // 5s per bucket
216
+
217
+ for (let i = 0; i < BUCKET_COUNT; i++) {
218
+ const bucketAgeSec = Math.round(((BUCKET_COUNT - 1 - i) * bucketDuration) / 1000);
219
+ buckets.push({
220
+ index: i,
221
+ ecommerce: 0,
222
+ page_view: 0,
223
+ system: 0,
224
+ custom: 0,
225
+ total: 0,
226
+ x: (i / (BUCKET_COUNT - 1)) * (svgWidth - 24) + 12,
227
+ y: chartHeight - 12,
228
+ isLatest: i === BUCKET_COUNT - 1,
229
+ ageLabel: i === BUCKET_COUNT - 1 ? t('analytics.timeNow') : `-${bucketAgeSec}s`,
230
+ });
231
+ }
232
+
233
+ for (const e of filteredAnalyticsEvents) {
234
+ const age = now - e.timestamp;
235
+ if (age >= 0 && age < windowMs) {
236
+ const bucketIdx = Math.floor((windowMs - 1 - age) / bucketDuration);
237
+ if (bucketIdx >= 0 && bucketIdx < BUCKET_COUNT) {
238
+ const cat = getEventCategory(e.name);
239
+ const c = (e as any).count || 1;
240
+ buckets[bucketIdx].total += c;
241
+ if (cat === 'ecommerce') buckets[bucketIdx].ecommerce += c;
242
+ else if (cat === 'page_view') buckets[bucketIdx].page_view += c;
243
+ else if (cat === 'system') buckets[bucketIdx].system += c;
244
+ else buckets[bucketIdx].custom += c;
245
+ }
246
+ }
247
+ }
248
+
249
+ const maxVal = Math.max(...buckets.map(b => b.total), 4);
250
+ buckets.forEach(b => {
251
+ const normalizedH = (b.total / maxVal) * (chartHeight - 22);
252
+ b.y = chartHeight - 10 - normalizedH;
253
+ });
254
+
255
+ // Generate Cubic Bézier Spline paths
256
+ let linePath = `M ${buckets[0].x.toFixed(1)} ${buckets[0].y.toFixed(1)}`;
257
+ for (let i = 0; i < buckets.length - 1; i++) {
258
+ const p0 = buckets[Math.max(0, i - 1)];
259
+ const p1 = buckets[i];
260
+ const p2 = buckets[i + 1];
261
+ const p3 = buckets[Math.min(buckets.length - 1, i + 2)];
262
+
263
+ const cp1x = p1.x + (p2.x - p0.x) / 6;
264
+ const cp1y = p1.y + (p2.y - p0.y) / 6;
265
+ const cp2x = p2.x - (p3.x - p1.x) / 6;
266
+ const cp2y = p2.y - (p3.y - p1.y) / 6;
267
+
268
+ linePath += ` C ${cp1x.toFixed(1)} ${cp1y.toFixed(1)}, ${cp2x.toFixed(1)} ${cp2y.toFixed(1)}, ${p2.x.toFixed(1)} ${p2.y.toFixed(1)}`;
269
+ }
270
+
271
+ const lastB = buckets[buckets.length - 1];
272
+ const firstB = buckets[0];
273
+ const areaPath = `${linePath} L ${lastB.x.toFixed(1)} ${chartHeight - 4} L ${firstB.x.toFixed(1)} ${chartHeight - 4} Z`;
274
+
275
+ const peakVal = Math.max(...buckets.map(b => b.total), 0);
276
+ const eventRate = Math.round(buckets.reduce((a, b) => a + b.total, 0));
277
+
278
+ return {
279
+ buckets,
280
+ linePath,
281
+ areaPath,
282
+ peakVal,
283
+ eventRate,
284
+ };
285
+ }, [filteredAnalyticsEvents, chartHeight, svgWidth, liveTick, t]);
286
+
287
+ const selectedBucket = selectedBucketIdx != null ? histogramData.buckets[selectedBucketIdx] : null;
288
+
289
+ return (
290
+ <View style={styles.analyticsHeaderCard}>
291
+ {/* ── 1. Top Header Row (GA4 Style) ─────────────────────────────────── */}
292
+ <View style={styles.analyticsHeaderTop}>
293
+ <View style={{flex: 1}}>
294
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
295
+ <Text style={styles.analyticsHeaderTitle}>{t('analytics.realtimeStream')}</Text>
296
+ <View
297
+ style={[
298
+ styles.statusDot,
299
+ {
300
+ backgroundColor: isTrackingEnabled
301
+ ? AppColors.greenColor
302
+ : AppColors.errorColor,
303
+ },
304
+ ]}
305
+ />
306
+ <Text
307
+ style={[
308
+ styles.statusText,
309
+ {
310
+ color: isTrackingEnabled
311
+ ? AppColors.greenColor
312
+ : AppColors.errorColor,
313
+ fontFamily: AppFonts.interBold,
314
+ },
315
+ ]}>
316
+ {isTrackingEnabled ? t('performance.live') : t('common.pause')}
317
+ </Text>
318
+ </View>
319
+ <Text style={styles.analyticsHeaderSubtitle} numberOfLines={1}>
320
+ {userId ? `${t('analytics.userId')}: ${userId}` : t('analytics.emptySubtitle')}
321
+ </Text>
322
+ </View>
323
+
324
+ <TouchableOpacity
325
+ style={styles.analyticsHeaderToggle}
326
+ activeOpacity={0.7}
327
+ onPress={() => {
328
+ animateNextLayout();
329
+ setAnalyticsHeaderExpanded(!analyticsHeaderExpanded);
330
+ }}>
331
+ <Text style={styles.analyticsHeaderToggleText}>
332
+ {analyticsHeaderExpanded ? t('common.close') : t('analytics.eventDetails')}
333
+ </Text>
334
+ </TouchableOpacity>
335
+ </View>
336
+
337
+ {/* ── 2. Multi-Select Category Filter Buttons (AT THE TOP) ──────────── */}
338
+ <View style={{marginTop: 10, marginBottom: 8}}>
339
+ {/* Multi-color Proportional Segmented Bar */}
340
+ <View
341
+ style={{
342
+ height: 4.5,
343
+ borderRadius: 2.5,
344
+ backgroundColor: `${AppColors.grayBorderSecondary}40`,
345
+ flexDirection: 'row',
346
+ overflow: 'hidden',
347
+ marginBottom: 8,
348
+ }}>
349
+ {categoryStats.ecommerce > 0 && (
350
+ <View
351
+ style={{
352
+ flex: categoryStats.ecommerce,
353
+ backgroundColor: AppColors.amber500,
354
+ }}
355
+ />
356
+ )}
357
+ {categoryStats.page_view > 0 && (
358
+ <View
359
+ style={{
360
+ flex: categoryStats.page_view,
361
+ backgroundColor: AppColors.sky500,
362
+ }}
363
+ />
364
+ )}
365
+ {categoryStats.system > 0 && (
366
+ <View
367
+ style={{
368
+ flex: categoryStats.system,
369
+ backgroundColor: AppColors.purple,
370
+ }}
371
+ />
372
+ )}
373
+ {categoryStats.custom > 0 && (
374
+ <View
375
+ style={{
376
+ flex: categoryStats.custom,
377
+ backgroundColor: AppColors.brandPurple,
378
+ }}
379
+ />
380
+ )}
381
+ </View>
382
+
383
+ {/* Multi-Select Category Buttons */}
384
+ <View
385
+ style={{
386
+ flexDirection: 'row',
387
+ flexWrap: 'wrap',
388
+ gap: 6,
389
+ }}>
390
+ {/* All Button */}
391
+ {(() => {
392
+ const isAllSelected = analyticsFilters.categories.has('all');
393
+ return (
394
+ <TouchableScale
395
+ onPress={() => toggleCategoryFilter('all')}
396
+ style={[
397
+ {
398
+ flexDirection: 'row',
399
+ alignItems: 'center',
400
+ gap: 5,
401
+ backgroundColor: isAllSelected ? AppColors.indigo600Alt : AppColors.indigo50,
402
+ paddingHorizontal: 8,
403
+ paddingVertical: 5,
404
+ borderRadius: 7,
405
+ borderWidth: 1,
406
+ borderColor: isAllSelected ? AppColors.indigo600 : AppColors.indigo400,
407
+ },
408
+ isAllSelected && {
409
+ shadowColor: AppColors.indigo600Alt,
410
+ shadowOffset: {width: 0, height: 1},
411
+ shadowOpacity: 0.28,
412
+ shadowRadius: 2.5,
413
+ elevation: 2,
414
+ },
415
+ ]}>
416
+ <LayersIcon
417
+ color={isAllSelected ? AppColors.white : AppColors.indigo600Alt}
418
+ size={11}
419
+ />
420
+ <Text
421
+ style={{
422
+ fontFamily: AppFonts.interBold,
423
+ fontSize: 10,
424
+ color: isAllSelected ? AppColors.white : AppColors.indigo600Alt,
425
+ }}>
426
+ {t('analytics.allCategory')}: {categoryStats.totalCount}
427
+ </Text>
428
+ </TouchableScale>
429
+ );
430
+ })()}
431
+
432
+ {/* Ecommerce Button */}
433
+ {(() => {
434
+ const isSelected =
435
+ !analyticsFilters.categories.has('all') &&
436
+ analyticsFilters.categories.has('ecommerce');
437
+ return (
438
+ <TouchableScale
439
+ onPress={() => toggleCategoryFilter('ecommerce')}
440
+ style={[
441
+ {
442
+ flexDirection: 'row',
443
+ alignItems: 'center',
444
+ gap: 5,
445
+ backgroundColor: isSelected ? AppColors.amber600 : AppColors.amber100,
446
+ paddingHorizontal: 8,
447
+ paddingVertical: 5,
448
+ borderRadius: 7,
449
+ borderWidth: 1,
450
+ borderColor: isSelected ? AppColors.amber700 : AppColors.amber200,
451
+ },
452
+ isSelected && {
453
+ shadowColor: AppColors.amber600,
454
+ shadowOffset: {width: 0, height: 1},
455
+ shadowOpacity: 0.28,
456
+ shadowRadius: 2.5,
457
+ elevation: 2,
458
+ },
459
+ ]}>
460
+ <CartIcon
461
+ color={isSelected ? AppColors.white : AppColors.amber700}
462
+ size={11}
463
+ />
464
+ <Text
465
+ style={{
466
+ fontFamily: AppFonts.interBold,
467
+ fontSize: 10,
468
+ color: isSelected ? AppColors.white : AppColors.amber800Warm,
469
+ }}>
470
+ {t('analytics.ecommerceCategory')}: {categoryStats.ecommerce} ({categoryStats.ecommercePct}%)
471
+ </Text>
472
+ </TouchableScale>
473
+ );
474
+ })()}
475
+
476
+ {/* Screens Button */}
477
+ {(() => {
478
+ const isSelected =
479
+ !analyticsFilters.categories.has('all') &&
480
+ analyticsFilters.categories.has('page_view');
481
+ return (
482
+ <TouchableScale
483
+ onPress={() => toggleCategoryFilter('page_view')}
484
+ style={[
485
+ {
486
+ flexDirection: 'row',
487
+ alignItems: 'center',
488
+ gap: 5,
489
+ backgroundColor: isSelected ? AppColors.sky600 : AppColors.sky100,
490
+ paddingHorizontal: 8,
491
+ paddingVertical: 5,
492
+ borderRadius: 7,
493
+ borderWidth: 1,
494
+ borderColor: isSelected ? AppColors.blue700 : AppColors.sky400,
495
+ },
496
+ isSelected && {
497
+ shadowColor: AppColors.sky600,
498
+ shadowOffset: {width: 0, height: 1},
499
+ shadowOpacity: 0.28,
500
+ shadowRadius: 2.5,
501
+ elevation: 2,
502
+ },
503
+ ]}>
504
+ <GlobeIcon
505
+ color={isSelected ? AppColors.white : AppColors.sky600}
506
+ size={11}
507
+ />
508
+ <Text
509
+ style={{
510
+ fontFamily: AppFonts.interBold,
511
+ fontSize: 10,
512
+ color: isSelected ? AppColors.white : AppColors.sky600,
513
+ }}>
514
+ {t('analytics.screensCategory')}: {categoryStats.page_view} ({categoryStats.page_viewPct}%)
515
+ </Text>
516
+ </TouchableScale>
517
+ );
518
+ })()}
519
+
520
+ {/* System Button */}
521
+ {(() => {
522
+ const isSelected =
523
+ !analyticsFilters.categories.has('all') &&
524
+ analyticsFilters.categories.has('system');
525
+ return (
526
+ <TouchableScale
527
+ onPress={() => toggleCategoryFilter('system')}
528
+ style={[
529
+ {
530
+ flexDirection: 'row',
531
+ alignItems: 'center',
532
+ gap: 5,
533
+ backgroundColor: isSelected ? AppColors.violet600 : AppColors.purple100,
534
+ paddingHorizontal: 8,
535
+ paddingVertical: 5,
536
+ borderRadius: 7,
537
+ borderWidth: 1,
538
+ borderColor: isSelected ? AppColors.purpleShade700 : AppColors.purple200,
539
+ },
540
+ isSelected && {
541
+ shadowColor: AppColors.violet600,
542
+ shadowOffset: {width: 0, height: 1},
543
+ shadowOpacity: 0.28,
544
+ shadowRadius: 2.5,
545
+ elevation: 2,
546
+ },
547
+ ]}>
548
+ <BoltIcon
549
+ color={isSelected ? AppColors.white : AppColors.purple}
550
+ size={11}
551
+ />
552
+ <Text
553
+ style={{
554
+ fontFamily: AppFonts.interBold,
555
+ fontSize: 10,
556
+ color: isSelected ? AppColors.white : AppColors.brandPurple,
557
+ }}>
558
+ {t('analytics.systemCategory')}: {categoryStats.system} ({categoryStats.systemPct}%)
559
+ </Text>
560
+ </TouchableScale>
561
+ );
562
+ })()}
563
+
564
+ {/* Custom Button */}
565
+ {(() => {
566
+ const isSelected =
567
+ !analyticsFilters.categories.has('all') &&
568
+ analyticsFilters.categories.has('custom');
569
+ return (
570
+ <TouchableScale
571
+ onPress={() => toggleCategoryFilter('custom')}
572
+ style={[
573
+ {
574
+ flexDirection: 'row',
575
+ alignItems: 'center',
576
+ gap: 5,
577
+ backgroundColor: isSelected ? AppColors.pink600 : AppColors.pink100,
578
+ paddingHorizontal: 8,
579
+ paddingVertical: 5,
580
+ borderRadius: 7,
581
+ borderWidth: 1,
582
+ borderColor: isSelected ? AppColors.rose700 : AppColors.roseBorder,
583
+ },
584
+ isSelected && {
585
+ shadowColor: AppColors.pink600,
586
+ shadowOffset: {width: 0, height: 1},
587
+ shadowOpacity: 0.28,
588
+ shadowRadius: 2.5,
589
+ elevation: 2,
590
+ },
591
+ ]}>
592
+ <SparkleIcon
593
+ color={isSelected ? AppColors.white : AppColors.brandPurple}
594
+ size={11}
595
+ />
596
+ <Text
597
+ style={{
598
+ fontFamily: AppFonts.interBold,
599
+ fontSize: 10,
600
+ color: isSelected ? AppColors.white : AppColors.pink600,
601
+ }}>
602
+ {t('analytics.customCategory')}: {categoryStats.custom} ({categoryStats.customPct}%)
603
+ </Text>
604
+ </TouchableScale>
605
+ );
606
+ })()}
607
+ </View>
608
+ </View>
609
+
610
+ {/* ── 3. GA4 Realtime Metric Hero & Activity Chart (UNDER CATEGORIES) ─ */}
611
+ <View
612
+ style={{
613
+ marginTop: 2,
614
+ marginBottom: 8,
615
+ backgroundColor: AppColors.grayBackground,
616
+ borderRadius: 10,
617
+ borderWidth: 1,
618
+ borderColor: AppColors.dividerColor,
619
+ padding: 10,
620
+ }}>
621
+ {/* Hero Metric Row */}
622
+ <View
623
+ style={{
624
+ flexDirection: 'row',
625
+ justifyContent: 'space-between',
626
+ alignItems: 'flex-start',
627
+ marginBottom: 6,
628
+ }}>
629
+ <View>
630
+ <View style={{flexDirection: 'row', alignItems: 'baseline', gap: 6}}>
631
+ <Text
632
+ style={{
633
+ fontFamily: AppFonts.interBold,
634
+ fontSize: 22,
635
+ color: AppColors.primaryBlack,
636
+ lineHeight: 26,
637
+ }}>
638
+ {totalEvents}
639
+ </Text>
640
+ <Text
641
+ style={{
642
+ fontFamily: AppFonts.interMedium,
643
+ fontSize: 10.5,
644
+ color: AppColors.grayTextWeak,
645
+ }}>
646
+ {t('analytics.eventsInWindow')}
647
+ </Text>
648
+ </View>
649
+ </View>
650
+
651
+ {/* Realtime Rate Badges */}
652
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 5}}>
653
+ <View
654
+ style={{
655
+ flexDirection: 'row',
656
+ alignItems: 'center',
657
+ gap: 3,
658
+ backgroundColor: `${AppColors.purple}14`,
659
+ paddingHorizontal: 6,
660
+ paddingVertical: 2.5,
661
+ borderRadius: 4,
662
+ borderWidth: 1,
663
+ borderColor: `${AppColors.purple}2E`,
664
+ }}>
665
+ <Text
666
+ style={{
667
+ fontFamily: AppFonts.interBold,
668
+ fontSize: 9.5,
669
+ color: AppColors.purple,
670
+ }}>
671
+ {t('analytics.eventVelocity', {rate: histogramData.eventRate})}
672
+ </Text>
673
+ </View>
674
+
675
+ {categoryStats.totalRevenue > 0 && (
676
+ <View
677
+ style={{
678
+ flexDirection: 'row',
679
+ alignItems: 'center',
680
+ gap: 3,
681
+ backgroundColor: `${AppColors.emerald600}15`,
682
+ paddingHorizontal: 6,
683
+ paddingVertical: 2.5,
684
+ borderRadius: 4,
685
+ borderWidth: 1,
686
+ borderColor: `${AppColors.emerald600}30`,
687
+ }}>
688
+ <MoneyIcon color={AppColors.emerald600} size={10} />
689
+ <Text
690
+ style={{
691
+ fontFamily: AppFonts.interBold,
692
+ fontSize: 9.5,
693
+ color: AppColors.emerald600,
694
+ }}>
695
+ ${categoryStats.totalRevenue.toFixed(2)}
696
+ </Text>
697
+ </View>
698
+ )}
699
+ </View>
700
+ </View>
701
+
702
+ {/* ── Crashlytics-Style Multi-Bar Histogram with Trendline Overlay ── */}
703
+ <View style={{height: chartHeight, position: 'relative', overflow: 'hidden', borderRadius: 8}}>
704
+ <Svg width="100%" height={chartHeight} viewBox={`0 0 ${svgWidth} ${chartHeight}`} preserveAspectRatio="none">
705
+ <Defs>
706
+ <SvgLinearGradient id="crashlyticsBarGrad" x1="0" y1="0" x2="0" y2="1">
707
+ <Stop offset="0%" stopColor="#8B5CF6" stopOpacity="0.9" />
708
+ <Stop offset="100%" stopColor="#6366F1" stopOpacity="0.6" />
709
+ </SvgLinearGradient>
710
+ <SvgLinearGradient id="crashlyticsAreaGrad" x1="0" y1="0" x2="0" y2="1">
711
+ <Stop offset="0%" stopColor="#8B5CF6" stopOpacity="0.25" />
712
+ <Stop offset="80%" stopColor="#8B5CF6" stopOpacity="0.04" />
713
+ <Stop offset="100%" stopColor="#8B5CF6" stopOpacity="0.0" />
714
+ </SvgLinearGradient>
715
+ </Defs>
716
+
717
+ {/* Subtle Grid Guidelines */}
718
+ <Line
719
+ x1="6"
720
+ y1={chartHeight - 6}
721
+ x2={svgWidth - 6}
722
+ y2={chartHeight - 6}
723
+ stroke="#E2E8F0"
724
+ strokeWidth="1"
725
+ />
726
+ <Line
727
+ x1="6"
728
+ y1={chartHeight / 2}
729
+ x2={svgWidth - 6}
730
+ y2={chartHeight / 2}
731
+ stroke="#F1F5F9"
732
+ strokeWidth="1"
733
+ strokeDasharray="4,4"
734
+ />
735
+
736
+ {/* Glowing Area Underlay */}
737
+ <Path d={histogramData.areaPath} fill="url(#crashlyticsAreaGrad)" />
738
+
739
+ {/* Vertical Crashlytics Histogram Bars */}
740
+ {histogramData.buckets.map((b, idx) => {
741
+ const isSelected = selectedBucketIdx === idx;
742
+ const barWidth = 10;
743
+ const barX = b.x - barWidth / 2;
744
+ const barHeight = Math.max(3, chartHeight - 8 - b.y);
745
+ const barY = chartHeight - 8 - barHeight;
746
+
747
+ return (
748
+ <G key={`bar-${b.index}`}>
749
+ {/* Background slot */}
750
+ <Rect
751
+ x={barX}
752
+ y={6}
753
+ width={barWidth}
754
+ height={chartHeight - 14}
755
+ rx={3}
756
+ fill={isSelected ? '#EDE9FE' : '#F8FAFC'}
757
+ />
758
+
759
+ {/* Active Event Bar */}
760
+ {b.total > 0 && (
761
+ <Rect
762
+ x={barX}
763
+ y={barY}
764
+ width={barWidth}
765
+ height={barHeight}
766
+ rx={3}
767
+ fill="url(#crashlyticsBarGrad)"
768
+ />
769
+ )}
770
+
771
+ {/* Selected Cursor Line */}
772
+ {isSelected && (
773
+ <Line
774
+ x1={b.x}
775
+ y1={4}
776
+ x2={b.x}
777
+ y2={chartHeight - 6}
778
+ stroke="#8B5CF6"
779
+ strokeWidth="1.5"
780
+ strokeDasharray="2,2"
781
+ />
782
+ )}
783
+ </G>
784
+ );
785
+ })}
786
+
787
+ {/* Crashlytics Apex Trendline */}
788
+ <Path
789
+ d={histogramData.linePath}
790
+ fill="none"
791
+ stroke="#8B5CF6"
792
+ strokeWidth="2"
793
+ strokeLinecap="round"
794
+ strokeLinejoin="round"
795
+ />
796
+
797
+ {/* Pulsing Dot on Current Active Time Bucket */}
798
+ {histogramData.buckets.length > 0 && (
799
+ <G>
800
+ <Circle
801
+ cx={histogramData.buckets[histogramData.buckets.length - 1].x}
802
+ cy={histogramData.buckets[histogramData.buckets.length - 1].y}
803
+ r="5.5"
804
+ fill="#8B5CF6"
805
+ opacity="0.25"
806
+ />
807
+ <Circle
808
+ cx={histogramData.buckets[histogramData.buckets.length - 1].x}
809
+ cy={histogramData.buckets[histogramData.buckets.length - 1].y}
810
+ r="3"
811
+ fill="#7C3AED"
812
+ />
813
+ </G>
814
+ )}
815
+ </Svg>
816
+
817
+ {/* Invisible Touch Columns for Interactive Bucket Selection */}
818
+ <View
819
+ style={{
820
+ position: 'absolute',
821
+ top: 0,
822
+ left: 0,
823
+ right: 0,
824
+ bottom: 0,
825
+ flexDirection: 'row',
826
+ }}>
827
+ {histogramData.buckets.map((b, idx) => (
828
+ <TouchableOpacity
829
+ key={b.index}
830
+ style={{flex: 1, height: '100%'}}
831
+ activeOpacity={0.8}
832
+ onPress={() => setSelectedBucketIdx(selectedBucketIdx === idx ? null : idx)}
833
+ />
834
+ ))}
835
+ </View>
836
+ </View>
837
+
838
+ {/* Selected Bucket Quick Breakdown Overlay */}
839
+ {selectedBucket && (
840
+ <View
841
+ style={{
842
+ flexDirection: 'row',
843
+ alignItems: 'center',
844
+ justifyContent: 'space-between',
845
+ backgroundColor: AppColors.white,
846
+ borderRadius: 6,
847
+ paddingHorizontal: 8,
848
+ paddingVertical: 4,
849
+ marginTop: 4,
850
+ borderWidth: 1,
851
+ borderColor: AppColors.brandPurple,
852
+ }}>
853
+ <Text
854
+ style={{
855
+ fontFamily: AppFonts.interBold,
856
+ fontSize: 10,
857
+ color: AppColors.primaryBlack,
858
+ }}>
859
+ {t('analytics.selectedBucket', {
860
+ time: selectedBucket.ageLabel,
861
+ count: selectedBucket.total,
862
+ })}
863
+ </Text>
864
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
865
+ {selectedBucket.ecommerce > 0 && (
866
+ <Text style={{fontSize: 9, fontFamily: AppFonts.interBold, color: AppColors.amber700}}>
867
+ Ecom: {selectedBucket.ecommerce}
868
+ </Text>
869
+ )}
870
+ {selectedBucket.page_view > 0 && (
871
+ <Text style={{fontSize: 9, fontFamily: AppFonts.interBold, color: AppColors.sky500}}>
872
+ Screen: {selectedBucket.page_view}
873
+ </Text>
874
+ )}
875
+ {selectedBucket.custom > 0 && (
876
+ <Text style={{fontSize: 9, fontFamily: AppFonts.interBold, color: AppColors.brandPurple}}>
877
+ Custom: {selectedBucket.custom}
878
+ </Text>
879
+ )}
880
+ </View>
881
+ </View>
882
+ )}
883
+
884
+ {/* Timeline Axis Labels (60s Realtime Rolling Stream) */}
885
+ <View
886
+ style={{
887
+ flexDirection: 'row',
888
+ justifyContent: 'space-between',
889
+ marginTop: 4,
890
+ paddingHorizontal: 4,
891
+ }}>
892
+ <Text
893
+ style={{
894
+ fontFamily: AppFonts.interMedium,
895
+ fontSize: 8.5,
896
+ color: AppColors.grayTextWeak,
897
+ }}>
898
+ -60s
899
+ </Text>
900
+ <Text
901
+ style={{
902
+ fontFamily: AppFonts.interMedium,
903
+ fontSize: 8.5,
904
+ color: AppColors.grayTextWeak,
905
+ }}>
906
+ -40s
907
+ </Text>
908
+ <Text
909
+ style={{
910
+ fontFamily: AppFonts.interMedium,
911
+ fontSize: 8.5,
912
+ color: AppColors.grayTextWeak,
913
+ }}>
914
+ -20s
915
+ </Text>
916
+ <Text
917
+ style={{
918
+ fontFamily: AppFonts.interBold,
919
+ fontSize: 8.5,
920
+ color: AppColors.brandPurple,
921
+ }}>
922
+ {t('analytics.timeNow')}
923
+ </Text>
924
+ </View>
925
+ </View>
926
+
927
+ {/* ── Top Screens / Views Mini Leaderboard (GA4 Realtime Overview) ──── */}
928
+ {topScreens.length > 0 && (
929
+ <View
930
+ style={{
931
+ marginTop: 8,
932
+ backgroundColor: `${AppColors.sky500}0A`,
933
+ borderRadius: 8,
934
+ borderWidth: 1,
935
+ borderColor: `${AppColors.sky500}22`,
936
+ padding: 8,
937
+ }}>
938
+ <View
939
+ style={{
940
+ flexDirection: 'row',
941
+ justifyContent: 'space-between',
942
+ alignItems: 'center',
943
+ marginBottom: 6,
944
+ }}>
945
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
946
+ <PinIcon color={AppColors.sky600} size={11} />
947
+ <Text
948
+ style={{
949
+ fontFamily: AppFonts.interBold,
950
+ fontSize: 9.5,
951
+ color: AppColors.sky600,
952
+ letterSpacing: 0.3,
953
+ }}>
954
+ TOP SCREENS (REALTIME)
955
+ </Text>
956
+ </View>
957
+ <Text
958
+ style={{
959
+ fontFamily: AppFonts.interMedium,
960
+ fontSize: 9,
961
+ color: AppColors.grayTextWeak,
962
+ }}>
963
+ Views
964
+ </Text>
965
+ </View>
966
+
967
+ <View style={{gap: 4}}>
968
+ {topScreens.map(scr => (
969
+ <View key={scr.name}>
970
+ <View
971
+ style={{
972
+ flexDirection: 'row',
973
+ justifyContent: 'space-between',
974
+ alignItems: 'center',
975
+ marginBottom: 2,
976
+ }}>
977
+ <Text
978
+ style={{
979
+ fontFamily: AppFonts.interMedium,
980
+ fontSize: 10,
981
+ color: AppColors.primaryBlack,
982
+ }}
983
+ numberOfLines={1}>
984
+ {scr.name}
985
+ </Text>
986
+ <Text
987
+ style={{
988
+ fontFamily: AppFonts.interBold,
989
+ fontSize: 9.5,
990
+ color: AppColors.sky600,
991
+ }}>
992
+ {scr.count} ({scr.pct}%)
993
+ </Text>
994
+ </View>
995
+ {/* Mini Horizontal Progress Meter */}
996
+ <View
997
+ style={{
998
+ height: 3,
999
+ borderRadius: 1.5,
1000
+ backgroundColor: `${AppColors.sky500}22`,
1001
+ overflow: 'hidden',
1002
+ }}>
1003
+ <View
1004
+ style={{
1005
+ height: '100%',
1006
+ width: `${scr.barWidthPct}%`,
1007
+ backgroundColor: AppColors.sky500,
1008
+ }}
1009
+ />
1010
+ </View>
1011
+ </View>
1012
+ ))}
1013
+ </View>
1014
+ </View>
1015
+ )}
1016
+
1017
+ {/* ── Expandable User Properties & Default Parameters ───────────────── */}
1018
+ {analyticsHeaderExpanded && (
1019
+ <>
1020
+ {/* Stats Row */}
1021
+ <View style={styles.analyticsStatsRow}>
1022
+ <View style={styles.analyticsStatBox}>
1023
+ <Text style={styles.analyticsStatValue}>{totalEvents}</Text>
1024
+ <Text style={styles.analyticsStatLabel}>{t('analytics.eventCount', {count: totalEvents})}</Text>
1025
+ </View>
1026
+ <View style={styles.analyticsStatBox}>
1027
+ <Text style={styles.analyticsStatValue}>
1028
+ {Object.keys(userProperties).length}
1029
+ </Text>
1030
+ <Text style={styles.analyticsStatLabel}>{t('analytics.props')}</Text>
1031
+ </View>
1032
+ <View style={styles.analyticsStatBox}>
1033
+ <Text style={styles.analyticsStatValue}>
1034
+ {Object.keys(defaultParams).length}
1035
+ </Text>
1036
+ <Text style={styles.analyticsStatLabel}>{t('analytics.defaultParameters')}</Text>
1037
+ </View>
1038
+ </View>
1039
+
1040
+ {/* Details Section */}
1041
+ {(hasUserProps || hasDefaultParams) && (
1042
+ <View style={styles.analyticsHeaderDetails}>
1043
+ {hasUserProps && (
1044
+ <View style={{marginBottom: 10}}>
1045
+ <Text style={styles.detailsGroupTitle}>{t('analytics.userProperties')}</Text>
1046
+ {Object.entries(userProperties).map(([k, v]) => (
1047
+ <View key={k} style={styles.detailsRow}>
1048
+ <Text style={styles.detailsKey} selectable={true}>{k}</Text>
1049
+ <Text style={styles.detailsValue} selectable={true}>{String(v)}</Text>
1050
+ </View>
1051
+ ))}
1052
+ </View>
1053
+ )}
1054
+
1055
+ {hasDefaultParams && (
1056
+ <View>
1057
+ <Text style={styles.detailsGroupTitle}>{t('analytics.defaultParameters')}</Text>
1058
+ {Object.entries(defaultParams).map(([k, v]) => (
1059
+ <View key={k} style={styles.detailsRow}>
1060
+ <Text style={styles.detailsKey} selectable={true}>{k}</Text>
1061
+ <Text style={styles.detailsValue} selectable={true}>{String(v)}</Text>
1062
+ </View>
1063
+ ))}
1064
+ </View>
1065
+ )}
1066
+ </View>
1067
+ )}
1068
+ </>
1069
+ )}
1070
+ </View>
1071
+ );
1072
+ });
1073
+
1074
+ const AnalyticsTab = React.memo(() => {
1075
+ const {t} = useTranslation();
1076
+ const {
1077
+ filteredAnalyticsEvents,
1078
+ analyticsSearch,
1079
+ setAnalyticsSearch,
1080
+ handleDelete,
1081
+ selectedEvent,
1082
+ setSelectedEvent,
1083
+ newEventIds,
1084
+ logRouteMapRef,
1085
+ isAnalyticsLayoutReady,
1086
+ setIsAnalyticsLayoutReady,
1087
+ isAnalyticsPaused,
1088
+ setIsAnalyticsPaused,
1089
+ isAnalyticsFilterApplied,
1090
+ } = useInspector();
1091
+
1092
+ const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
1093
+
1094
+ const keyExtractor = useCallback(
1095
+ (item: any, index: number) => item?.id?.toString() ?? index.toString(),
1096
+ [],
1097
+ );
1098
+
1099
+ const renderItem = useCallback(
1100
+ ({item, index}: {item: any; index: number}) => {
1101
+ const prev = filteredAnalyticsEvents[index + 1];
1102
+ const next = filteredAnalyticsEvents[index - 1];
1103
+ const msSincePrev = prev
1104
+ ? item.timestamp - prev.timestamp
1105
+ : undefined;
1106
+ const thisMin = Math.floor(item.timestamp / 60000);
1107
+ const nextMin = next
1108
+ ? Math.floor(next.timestamp / 60000)
1109
+ : -1;
1110
+ const showTimestamp =
1111
+ index === 0 || thisMin !== nextMin;
1112
+ return (
1113
+ <AnalyticsEventCard
1114
+ event={item}
1115
+ onPress={() => {
1116
+ animateNextLayout();
1117
+ setSelectedEvent(item);
1118
+ }}
1119
+ isNew={newEventIds.has(item.id)}
1120
+ searchStr={analyticsSearch}
1121
+ isFirst={index === 0}
1122
+ isLast={
1123
+ index === filteredAnalyticsEvents.length - 1
1124
+ }
1125
+ msSincePrev={msSincePrev}
1126
+ showTimestamp={showTimestamp}
1127
+ computedScreenName={(() => {
1128
+ if (!item) return '';
1129
+ const rawScreenName =
1130
+ item.screenName ||
1131
+ item.screenClass ||
1132
+ item.pageTitle ||
1133
+ item.pageLocation ||
1134
+ item.params?.firebase_screen ||
1135
+ item.params?.screen_name ||
1136
+ item.params?.firebase_screen_class ||
1137
+ item.params?.screen_class;
1138
+ let screenName = typeof rawScreenName === 'string'
1139
+ ? rawScreenName
1140
+ : (rawScreenName ? JSON.stringify(rawScreenName) : '');
1141
+ const routeInfo = logRouteMapRef.current.get(
1142
+ item.id + 1000000,
1143
+ );
1144
+ if (!screenName) {
1145
+ if (
1146
+ routeInfo &&
1147
+ routeInfo.path !== 'Navigators'
1148
+ ) {
1149
+ const parts = routeInfo.path.split(' ➔ ');
1150
+ screenName = parts[parts.length - 1];
1151
+ }
1152
+ }
1153
+ return screenName;
1154
+ })()}
1155
+ />
1156
+ );
1157
+ },
1158
+ [filteredAnalyticsEvents, analyticsSearch, newEventIds, setSelectedEvent, logRouteMapRef],
1159
+ );
1160
+
1161
+
1162
+ return (
1163
+ <>
1164
+ {/* ─── Search + Shared Toolbar for Analytics ──────────────────────── */}
1165
+ {selectedEvent == null && (
1166
+ <View
1167
+ style={[
1168
+ styles.toolbarRow,
1169
+ {marginTop: 12, marginBottom: 8},
1170
+ ]}>
1171
+ <View style={styles.searchContainer}>
1172
+ <SearchIcon
1173
+ color={AppColors.grayTextWeak}
1174
+ size={16}
1175
+ />
1176
+ <TextInput
1177
+ placeholder="Search events..."
1178
+ placeholderTextColor={AppColors.grayTextWeak}
1179
+ value={analyticsSearch}
1180
+ onChangeText={setAnalyticsSearch}
1181
+ style={styles.searchInput}
1182
+ autoCorrect={false}
1183
+ autoCapitalize="none"
1184
+ />
1185
+ {analyticsSearch.length > 0 && (
1186
+ <Pressable
1187
+ onPress={() => setAnalyticsSearch('')}
1188
+ hitSlop={10}
1189
+ style={styles.clearBtn}>
1190
+ <ClearIcon
1191
+ color={AppColors.grayTextWeak}
1192
+ size={14}
1193
+ />
1194
+ </Pressable>
1195
+ )}
1196
+ </View>
1197
+ <View style={styles.toolbarRight}>
1198
+ <TouchableOpacity
1199
+ style={[
1200
+ styles.toolbarBtn,
1201
+ isAnalyticsPaused && {
1202
+ borderColor: `${AppColors.darkOrange}50`,
1203
+ backgroundColor: `${AppColors.darkOrange}18`,
1204
+ },
1205
+ ]}
1206
+ onPress={() => setIsAnalyticsPaused(p => !p)}
1207
+ hitSlop={6}>
1208
+ <HeaderPauseIcon
1209
+ isPaused={isAnalyticsPaused}
1210
+ color={
1211
+ isAnalyticsPaused
1212
+ ? AppColors.darkOrange
1213
+ : AppColors.grayTextStrong
1214
+ }
1215
+ size={16}
1216
+ />
1217
+ </TouchableOpacity>
1218
+
1219
+ {/* GA Dashboard-style Filter Button */}
1220
+ <TouchableOpacity
1221
+ style={[
1222
+ styles.toolbarBtn,
1223
+ isAnalyticsFilterApplied && {
1224
+ borderColor: `${AppColors.brandPurple}60`,
1225
+ backgroundColor: `${AppColors.brandPurple}15`,
1226
+ },
1227
+ ]}
1228
+ onPress={() => setIsFilterModalOpen(true)}
1229
+ hitSlop={6}>
1230
+ <FilterIcon
1231
+ color={
1232
+ isAnalyticsFilterApplied
1233
+ ? AppColors.brandPurple
1234
+ : AppColors.grayTextStrong
1235
+ }
1236
+ size={15}
1237
+ />
1238
+ {isAnalyticsFilterApplied && (
1239
+ <View
1240
+ style={{
1241
+ position: 'absolute',
1242
+ top: 2,
1243
+ right: 2,
1244
+ width: 7,
1245
+ height: 7,
1246
+ borderRadius: 3.5,
1247
+ backgroundColor: AppColors.darkOrange,
1248
+ borderWidth: 1,
1249
+ borderColor: AppColors.white,
1250
+ }}
1251
+ />
1252
+ )}
1253
+ </TouchableOpacity>
1254
+
1255
+ <TouchableOpacity
1256
+ style={[
1257
+ styles.toolbarBtn,
1258
+ {
1259
+ borderColor: `${AppColors.errorColor}38`,
1260
+ backgroundColor: `${AppColors.errorColor}0F`,
1261
+ },
1262
+ ]}
1263
+ onPress={handleDelete}
1264
+ hitSlop={6}>
1265
+ <TrashIcon
1266
+ color={AppColors.errorColor}
1267
+ size={15}
1268
+ />
1269
+ </TouchableOpacity>
1270
+ </View>
1271
+ </View>
1272
+ )}
1273
+
1274
+ {/* Filter Modal */}
1275
+ <AnalyticsFilterModal
1276
+ visible={isFilterModalOpen}
1277
+ onClose={() => setIsFilterModalOpen(false)}
1278
+ />
1279
+
1280
+ <View
1281
+ style={{flex: 1}}
1282
+ onLayout={() => setIsAnalyticsLayoutReady(true)}>
1283
+ {isAnalyticsLayoutReady ? (
1284
+ <FlatList
1285
+ data={filteredAnalyticsEvents}
1286
+ keyExtractor={keyExtractor}
1287
+ ListHeaderComponent={AnalyticsHeader}
1288
+ renderItem={renderItem}
1289
+ initialNumToRender={12}
1290
+ maxToRenderPerBatch={8}
1291
+ windowSize={5}
1292
+ removeClippedSubviews={true}
1293
+ renderToHardwareTextureAndroid={true}
1294
+ ListEmptyComponent={
1295
+ <View style={styles.emptyContainer}>
1296
+ <View style={styles.emptyIconWrap}>
1297
+ <EmptyRadarIcon
1298
+ color={AppColors.purple}
1299
+ size={32}
1300
+ />
1301
+ </View>
1302
+ <Text style={styles.emptyTitle}>
1303
+ {analyticsSearch.length > 0
1304
+ ? 'No matching events'
1305
+ : 'No analytics events yet'}
1306
+ </Text>
1307
+ <Text style={styles.emptySub}>
1308
+ {analyticsSearch.length > 0
1309
+ ? 'Try adjusting your search.'
1310
+ : 'Call setupAnalyticsLogger(analytics()) at app start.'}
1311
+ </Text>
1312
+ </View>
1313
+ }
1314
+ ListFooterComponent={
1315
+ filteredAnalyticsEvents.length > 0 ? (
1316
+ <EndOfListFooter
1317
+ count={filteredAnalyticsEvents.length}
1318
+ label="events"
1319
+ />
1320
+ ) : null
1321
+ }
1322
+ contentContainerStyle={[
1323
+ styles.listContent,
1324
+ filteredAnalyticsEvents.length === 0 && {
1325
+ flexGrow: 1,
1326
+ },
1327
+ ]}
1328
+ keyboardShouldPersistTaps="handled"
1329
+ />
1330
+ ) : null}
1331
+ </View>
1332
+ </>
1333
+ );
1334
+ });
1335
+
1336
+ export default AnalyticsTab;