react-native-inapp-inspector 1.1.26 → 1.1.28

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 (67) hide show
  1. package/android/build.gradle +1 -0
  2. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +34 -0
  3. package/dist/commonjs/components/ErrorBoundary.d.ts +1 -1
  4. package/dist/commonjs/components/ErrorBoundary.js +530 -100
  5. package/dist/commonjs/components/Inspector/AnalyticsTab.js +176 -126
  6. package/dist/commonjs/components/Inspector/BundleTab.d.ts +1 -0
  7. package/dist/commonjs/components/Inspector/BundleTab.js +145 -21
  8. package/dist/commonjs/components/Inspector/CrashTab.js +0 -8
  9. package/dist/commonjs/components/Inspector/NetworkDetail.js +41 -0
  10. package/dist/commonjs/components/Inspector/PerformanceTab.d.ts +5 -0
  11. package/dist/commonjs/components/Inspector/PerformanceTab.js +588 -155
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +57 -22
  13. package/dist/commonjs/components/Inspector/TabBar.js +13 -1
  14. package/dist/commonjs/components/LogCard.js +75 -34
  15. package/dist/commonjs/components/NetworkIcons.d.ts +112 -102
  16. package/dist/commonjs/constants/version.d.ts +1 -1
  17. package/dist/commonjs/constants/version.js +1 -1
  18. package/dist/commonjs/customHooks/analyticsLogger.d.ts +1 -11
  19. package/dist/commonjs/customHooks/analyticsLogger.js +8 -1
  20. package/dist/commonjs/customHooks/bundleAnalyzer.js +121 -124
  21. package/dist/commonjs/customHooks/networkLogger.d.ts +1 -0
  22. package/dist/commonjs/customHooks/networkLogger.js +16 -0
  23. package/dist/commonjs/customHooks/performanceTracker.d.ts +74 -0
  24. package/dist/commonjs/customHooks/performanceTracker.js +546 -392
  25. package/dist/commonjs/customHooks/reduxLogger.d.ts +1 -0
  26. package/dist/commonjs/customHooks/reduxLogger.js +3 -1
  27. package/dist/commonjs/helpers/index.js +27 -3
  28. package/dist/commonjs/i18n/locales/en.json +225 -4
  29. package/dist/commonjs/index.d.ts +1 -1
  30. package/dist/commonjs/index.js +36 -2
  31. package/dist/commonjs/styles/index.d.ts +3 -1
  32. package/dist/commonjs/styles/index.js +8 -6
  33. package/dist/commonjs/types/interfaces.d.ts +3 -0
  34. package/dist/esm/components/ErrorBoundary.d.ts +1 -1
  35. package/dist/esm/components/ErrorBoundary.js +498 -101
  36. package/dist/esm/components/Inspector/AnalyticsTab.js +177 -127
  37. package/dist/esm/components/Inspector/BundleTab.d.ts +1 -0
  38. package/dist/esm/components/Inspector/BundleTab.js +145 -23
  39. package/dist/esm/components/Inspector/CrashTab.js +0 -8
  40. package/dist/esm/components/Inspector/NetworkDetail.js +41 -0
  41. package/dist/esm/components/Inspector/PerformanceTab.d.ts +5 -0
  42. package/dist/esm/components/Inspector/PerformanceTab.js +591 -158
  43. package/dist/esm/components/Inspector/SettingsPanel.js +57 -22
  44. package/dist/esm/components/Inspector/TabBar.js +13 -1
  45. package/dist/esm/components/LogCard.js +75 -34
  46. package/dist/esm/components/NetworkIcons.d.ts +112 -102
  47. package/dist/esm/constants/version.d.ts +1 -1
  48. package/dist/esm/constants/version.js +1 -1
  49. package/dist/esm/customHooks/analyticsLogger.d.ts +1 -11
  50. package/dist/esm/customHooks/analyticsLogger.js +6 -0
  51. package/dist/esm/customHooks/bundleAnalyzer.js +121 -124
  52. package/dist/esm/customHooks/networkLogger.d.ts +1 -0
  53. package/dist/esm/customHooks/networkLogger.js +16 -0
  54. package/dist/esm/customHooks/performanceTracker.d.ts +74 -0
  55. package/dist/esm/customHooks/performanceTracker.js +531 -391
  56. package/dist/esm/customHooks/reduxLogger.d.ts +1 -0
  57. package/dist/esm/customHooks/reduxLogger.js +1 -0
  58. package/dist/esm/helpers/index.js +27 -3
  59. package/dist/esm/i18n/locales/en.json +225 -4
  60. package/dist/esm/index.d.ts +1 -1
  61. package/dist/esm/index.js +24 -3
  62. package/dist/esm/styles/index.d.ts +3 -1
  63. package/dist/esm/styles/index.js +8 -6
  64. package/dist/esm/types/interfaces.d.ts +3 -0
  65. package/ios/NetworkInspectorModule.h +2 -0
  66. package/ios/NetworkInspectorModule.m +10 -0
  67. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect, useMemo, useRef } from 'react';
2
- import { Platform, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native';
2
+ import { ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
3
+ import Svg, { Path, Defs, LinearGradient, Stop, Line, Circle, G, } from 'react-native-svg';
3
4
  import TouchableScale from '../TouchableScale';
4
5
  import CopyButton from '../CopyButton';
5
6
  import HighlightText from '../HighlightText';
@@ -8,8 +9,8 @@ import { AppColors } from '../../styles/AppColors';
8
9
  import { AppFonts } from '../../styles/AppFonts';
9
10
  import { useTranslation } from '../../i18n';
10
11
  import { getRuntimeDiagnostics } from '../../helpers';
11
- import { SearchIcon, CircleXIcon, PerformanceIcon, BoltIcon, LightbulbIcon, WarningTriangleIcon, MapIcon, AtomIcon, BrainIcon, GlobeIcon, } from '../NetworkIcons';
12
- import { INITIAL_EVENTS, subscribePerformanceEvents, logPerformanceEvent, clearPerformanceEvents, } from '../../customHooks/performanceTracker';
12
+ import { SearchIcon, CircleXIcon, PerformanceIcon, BoltIcon, LightbulbIcon, } from '../NetworkIcons';
13
+ import { getPerformanceEvents, subscribePerformanceEvents, logPerformanceEvent, clearPerformanceEvents, } from '../../customHooks/performanceTracker';
13
14
  const PerformanceTab = React.memo(() => {
14
15
  const { t } = useTranslation();
15
16
  // ─── Real-Time FPS Tracking ────────────────────────────────────────────────
@@ -35,8 +36,8 @@ const PerformanceTab = React.memo(() => {
35
36
  const [fpsHistory, setFpsHistory] = useState([
36
37
  60, 59, 60, 60, 58, 60, 59, 60, 60, 60, 57, 60, 59, 60, 60, 58, 60, 60, 59, 60,
37
38
  ]);
38
- // Performance Log Events (Initialized with benchmark & diagnostics events)
39
- const [events, setEvents] = useState(INITIAL_EVENTS);
39
+ // Performance Log Events (Dynamic live subscription)
40
+ const [events, setEvents] = useState(() => getPerformanceEvents());
40
41
  const [filterCategory, setFilterCategory] = useState('ALL');
41
42
  const [search, setSearch] = useState('');
42
43
  useEffect(() => {
@@ -211,9 +212,88 @@ const PerformanceTab = React.memo(() => {
211
212
  headroomPercent,
212
213
  };
213
214
  }, [avgFps, jsLagMs]);
215
+ // ─── Dynamic SVG FPS Area & Trend Curve Data ────────────────────────────
216
+ const [selectedFpsPointIdx, setSelectedFpsPointIdx] = useState(null);
217
+ const svgGraphWidth = 320;
218
+ const svgGraphHeight = 64;
219
+ const fpsGraphData = useMemo(() => {
220
+ const points = fpsHistory.length > 0 ? fpsHistory : [60, 60];
221
+ const stepX = svgGraphWidth / Math.max(1, points.length - 1);
222
+ const coords = points.map((val, idx) => {
223
+ const clamped = Math.max(0, Math.min(60, val));
224
+ const x = Number((idx * stepX).toFixed(1));
225
+ const y = Number((svgGraphHeight - (clamped / 60) * (svgGraphHeight - 14) - 6).toFixed(1));
226
+ return { x, y, val };
227
+ });
228
+ let linePath = `M ${coords[0].x} ${coords[0].y}`;
229
+ for (let i = 0; i < coords.length - 1; i++) {
230
+ const p0 = coords[i];
231
+ const p1 = coords[i + 1];
232
+ const midX = Number(((p0.x + p1.x) / 2).toFixed(1));
233
+ linePath += ` C ${midX} ${p0.y}, ${midX} ${p1.y}, ${p1.x} ${p1.y}`;
234
+ }
235
+ const last = coords[coords.length - 1];
236
+ const first = coords[0];
237
+ const areaPath = `${linePath} L ${last.x} ${svgGraphHeight} L ${first.x} ${svgGraphHeight} Z`;
238
+ return { linePath, areaPath, coords };
239
+ }, [fpsHistory]);
240
+ const latencyBuckets = useMemo(() => {
241
+ let optimal = 0;
242
+ let minorJank = 0;
243
+ let noticeableJank = 0;
244
+ let severeFreeze = 0;
245
+ fpsHistory.forEach(fps => {
246
+ if (fps >= 55)
247
+ optimal++;
248
+ else if (fps >= 30)
249
+ minorJank++;
250
+ else if (fps >= 20)
251
+ noticeableJank++;
252
+ else
253
+ severeFreeze++;
254
+ });
255
+ const total = Math.max(1, fpsHistory.length);
256
+ return [
257
+ {
258
+ key: 'optimal',
259
+ label: t('performance.optimalBucket'),
260
+ count: optimal,
261
+ percent: Number(((optimal / total) * 100).toFixed(0)),
262
+ color: AppColors.emerald500,
263
+ bgColor: AppColors.emerald100,
264
+ },
265
+ {
266
+ key: 'minorJank',
267
+ label: t('performance.minorJankBucket'),
268
+ count: minorJank,
269
+ percent: Number(((minorJank / total) * 100).toFixed(0)),
270
+ color: AppColors.amber500,
271
+ bgColor: AppColors.amber100,
272
+ },
273
+ {
274
+ key: 'noticeableJank',
275
+ label: t('performance.noticeableJankBucket'),
276
+ count: noticeableJank,
277
+ percent: Number(((noticeableJank / total) * 100).toFixed(0)),
278
+ color: AppColors.firebaseOrange,
279
+ bgColor: AppColors.lightOrange,
280
+ },
281
+ {
282
+ key: 'severeFreeze',
283
+ label: t('performance.severeFreezeBucket'),
284
+ count: severeFreeze,
285
+ percent: Number(((severeFreeze / total) * 100).toFixed(0)),
286
+ color: AppColors.errorColor,
287
+ bgColor: AppColors.errorCardBg,
288
+ },
289
+ ];
290
+ }, [fpsHistory, t]);
214
291
  const clearEvents = () => {
215
292
  clearPerformanceEvents();
216
293
  };
294
+ const activeScrubPoint = selectedFpsPointIdx !== null && fpsGraphData.coords[selectedFpsPointIdx]
295
+ ? fpsGraphData.coords[selectedFpsPointIdx]
296
+ : null;
217
297
  return (<View style={perfStyles.container}>
218
298
  {/* Search & Live Control Header */}
219
299
  <View style={perfStyles.headerBar}>
@@ -225,99 +305,81 @@ const PerformanceTab = React.memo(() => {
225
305
  </TouchableScale>) : null}
226
306
  </View>
227
307
 
228
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
229
- <TouchableScale onPress={() => setIsRecording(prev => !prev)} style={[
230
- perfStyles.recordBtn,
231
- isRecording ? perfStyles.recordBtnActive : perfStyles.recordBtnPaused,
308
+ {/* Live FPS Telemetry Recorder Toggle */}
309
+ <TouchableScale onPress={() => setIsRecording(prev => !prev)} style={[
310
+ perfStyles.recordToggleBtn,
311
+ isRecording
312
+ ? perfStyles.recordToggleActive
313
+ : perfStyles.recordToggleInactive,
232
314
  ]}>
233
- <View style={[
234
- perfStyles.recordDot,
235
- { backgroundColor: isRecording ? AppColors.emerald500 : AppColors.amber500 },
315
+ <View style={[
316
+ perfStyles.recordingDot,
317
+ {
318
+ backgroundColor: isRecording
319
+ ? AppColors.errorColor
320
+ : AppColors.grayTextWeak,
321
+ },
236
322
  ]}/>
237
- <Text style={[
238
- perfStyles.recordBtnText,
239
- { color: isRecording ? AppColors.emerald600 : AppColors.amber700 },
323
+ <Text style={[
324
+ perfStyles.recordToggleText,
325
+ {
326
+ color: isRecording
327
+ ? AppColors.errorColor
328
+ : AppColors.grayTextWeak,
329
+ },
240
330
  ]}>
241
- {isRecording ? t('performance.live') : t('redux.paused')}
242
- </Text>
243
- </TouchableScale>
244
-
245
- <CopyButton value={() => ({
246
- currentFps,
247
- minFps,
248
- maxFps,
249
- avgFps,
250
- jankyFrameCount,
251
- jsLagMs,
252
- platform: Platform.OS,
253
- engine: runtimeDiagnostics.jsEngineName,
254
- totalEvents: events.length,
255
- events,
256
- })} label={t('common.copyJson')}/>
257
- </View>
331
+ {isRecording
332
+ ? t('performance.recording')
333
+ : t('performance.paused')}
334
+ </Text>
335
+ </TouchableScale>
258
336
  </View>
259
337
 
260
- {/* ─── Frozen Interactive Filter Tabs Under Search Component ─── */}
261
- <View style={perfStyles.filterRow}>
262
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={perfStyles.filterScrollContent}>
338
+ {/* Filter Category Tabs Carousel */}
339
+ <View style={perfStyles.categoryScrollWrapper}>
340
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={perfStyles.categoryScrollView} contentContainerStyle={perfStyles.categoryScroll}>
263
341
  {[
264
- { key: 'ALL', label: t('performance.allLogs'), Icon: BoltIcon, count: events.length },
265
- {
266
- key: 'JANKY',
267
- label: t('performance.jankySlow'),
268
- Icon: WarningTriangleIcon,
269
- count: events.filter(e => e.severity === 'warning' || e.severity === 'critical').length,
270
- },
271
- {
272
- key: 'NAVIGATION',
273
- label: t('performance.navigation'),
274
- Icon: MapIcon,
275
- count: events.filter(e => e.category === 'navigation').length,
276
- },
277
- {
278
- key: 'RENDER',
279
- label: t('performance.components'),
280
- Icon: AtomIcon,
281
- count: events.filter(e => e.category === 'render').length,
282
- },
283
- {
284
- key: 'MEMORY',
285
- label: t('performance.memoryGc'),
286
- Icon: BrainIcon,
287
- count: events.filter(e => e.category === 'memory').length,
288
- },
289
- {
290
- key: 'IO',
291
- label: t('performance.networkIo'),
292
- Icon: GlobeIcon,
293
- count: events.filter(e => e.category === 'io' || e.category === 'bridge').length,
294
- },
295
- ].map(tab => {
296
- const isActive = filterCategory === tab.key;
297
- return (<TouchableScale key={tab.key} onPress={() => setFilterCategory(tab.key)} style={[
298
- perfStyles.filterPill,
299
- isActive && perfStyles.filterPillActive,
342
+ { key: 'ALL', label: t('performance.catAll') },
343
+ { key: 'JANKY', label: t('performance.catJanky') },
344
+ { key: 'NAVIGATION', label: t('performance.catNavigation') },
345
+ { key: 'RENDER', label: t('performance.catRender') },
346
+ { key: 'MEMORY', label: t('performance.catMemory') },
347
+ { key: 'IO', label: t('performance.catIo') },
348
+ ].map(cat => {
349
+ const isSelected = filterCategory === cat.key;
350
+ return (<TouchableScale key={cat.key} onPress={() => setFilterCategory(cat.key)} style={[
351
+ perfStyles.categoryChip,
352
+ isSelected && perfStyles.categoryChipActive,
300
353
  ]}>
301
- <tab.Icon color={isActive ? AppColors.white : AppColors.grayTextStrong} size={12}/>
302
354
  <Text style={[
303
- perfStyles.filterPillText,
304
- isActive && perfStyles.filterPillTextActive,
355
+ perfStyles.categoryChipText,
356
+ isSelected && perfStyles.categoryChipTextActive,
305
357
  ]}>
306
- {tab.label} ({tab.count})
358
+ {cat.label}
307
359
  </Text>
308
360
  </TouchableScale>);
309
361
  })}
310
362
  </ScrollView>
311
363
  </View>
312
364
 
313
- <ScrollView style={{ flex: 1 }} contentContainerStyle={perfStyles.scrollContent} keyboardShouldPersistTaps="handled">
314
- {/* ── 1. REALTIME FPS METRIC DASHBOARD ── */}
315
- <View style={perfStyles.metricCard}>
316
- <View style={perfStyles.metricCardHeader}>
365
+ {/* Main Content Scroll View */}
366
+ <ScrollView style={perfStyles.scrollBody} contentContainerStyle={perfStyles.scrollContent} showsVerticalScrollIndicator={false}>
367
+ {/* ── 1. REAL-TIME FPS SCORECARD & SMOOTH AREA GRAPH ── */}
368
+ <View style={perfStyles.heroCard}>
369
+ <View style={perfStyles.heroHeaderRow}>
317
370
  <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
318
- <PerformanceIcon color={AppColors.purple} size={18}/>
319
- <Text style={perfStyles.metricCardTitle}>{t('performance.liveFps')}</Text>
371
+ <PerformanceIcon color={AppColors.purple} size={17}/>
372
+ <View>
373
+ <Text style={perfStyles.heroTitle}>
374
+ {t('performance.fpsAreaChartTitle')}
375
+ </Text>
376
+ <Text style={perfStyles.heroSubTitle}>
377
+ {t('performance.fpsAreaChartSub')}
378
+ </Text>
379
+ </View>
320
380
  </View>
381
+
382
+ {/* Health Grade Score Pill */}
321
383
  <View style={[
322
384
  perfStyles.scorePill,
323
385
  { backgroundColor: performanceScore.bg },
@@ -351,55 +413,169 @@ const PerformanceTab = React.memo(() => {
351
413
 
352
414
  <View style={perfStyles.fpsStatsGrid}>
353
415
  <View style={perfStyles.statItem}>
354
- <Text style={perfStyles.statLabel}>{t('performance.avgFrameTime')}</Text>
416
+ <Text style={perfStyles.statLabel}>
417
+ {t('performance.avgFrameTime')}
418
+ </Text>
355
419
  <Text style={perfStyles.statValue}>{avgFps}</Text>
356
420
  </View>
357
421
  <View style={perfStyles.statItem}>
358
- <Text style={perfStyles.statLabel}>{t('performance.peakTime')}</Text>
359
- <Text style={[perfStyles.statValue, minFps < 50 && { color: AppColors.firebaseOrange }]}>
422
+ <Text style={perfStyles.statLabel}>
423
+ {t('performance.peakTime')}
424
+ </Text>
425
+ <Text style={[
426
+ perfStyles.statValue,
427
+ minFps < 50 && { color: AppColors.firebaseOrange },
428
+ ]}>
360
429
  {minFps}
361
430
  </Text>
362
431
  </View>
363
432
  <View style={perfStyles.statItem}>
364
- <Text style={perfStyles.statLabel}>{t('performance.jsLag')}</Text>
365
- <Text style={[perfStyles.statValue, jsLagMs > 5 && { color: AppColors.firebaseOrange }]}>
433
+ <Text style={perfStyles.statLabel}>
434
+ {t('performance.jsLag')}
435
+ </Text>
436
+ <Text style={[
437
+ perfStyles.statValue,
438
+ jsLagMs > 5 && { color: AppColors.firebaseOrange },
439
+ ]}>
366
440
  {jsLagMs}ms
367
441
  </Text>
368
442
  </View>
369
443
  <View style={perfStyles.statItem}>
370
- <Text style={perfStyles.statLabel}>{t('performance.jankRate')}</Text>
444
+ <Text style={perfStyles.statLabel}>
445
+ {t('performance.jankRate')}
446
+ </Text>
371
447
  <Text style={perfStyles.statValue}>
372
448
  {totalFrames > 0
373
- ? `${((jankyFrameCount / Math.max(1, totalFrames / 60)) * 100).toFixed(0)}%`
449
+ ? `${((jankyFrameCount / Math.max(1, totalFrames / 60)) *
450
+ 100).toFixed(0)}%`
374
451
  : '0%'}
375
452
  </Text>
376
453
  </View>
377
454
  </View>
378
455
  </View>
379
456
 
380
- {/* Sparkline Bar Graph */}
381
- <View style={perfStyles.sparklineTrack}>
382
- {fpsHistory.map((fpsVal, idx) => {
383
- const barHeight = Math.max(6, (fpsVal / 60) * 36);
384
- const isLow = fpsVal < 50;
385
- return (<View key={idx} style={perfStyles.sparklineCol}>
386
- <View style={[
387
- perfStyles.sparklineBar,
388
- {
389
- height: barHeight,
390
- backgroundColor: isLow ? AppColors.errorColor : AppColors.emerald500,
391
- },
392
- ]}/>
393
- </View>);
457
+ {/* Interactive Scrub Tooltip */}
458
+ {activeScrubPoint && (<View style={perfStyles.graphScrubTooltip}>
459
+ <Text style={perfStyles.graphScrubTooltipText}>
460
+ {activeScrubPoint.val} FPS •{' '}
461
+ {Number((1000 / Math.max(1, activeScrubPoint.val)).toFixed(1))}
462
+ ms
463
+ </Text>
464
+ </View>)}
465
+
466
+ {/* Real-Time SVG Smooth Area & Trend Wave Graph */}
467
+ <View style={perfStyles.svgGraphContainer}>
468
+ <Svg width="100%" height={svgGraphHeight} viewBox={`0 0 ${svgGraphWidth} ${svgGraphHeight}`}>
469
+ <Defs>
470
+ <LinearGradient id="fpsAreaGradient" x1="0" y1="0" x2="0" y2="1">
471
+ <Stop offset="0%" stopColor={avgFps >= 55
472
+ ? AppColors.emerald500
473
+ : avgFps >= 40
474
+ ? AppColors.amber500
475
+ : AppColors.errorColor} stopOpacity="0.32"/>
476
+ <Stop offset="100%" stopColor={avgFps >= 55
477
+ ? AppColors.emerald500
478
+ : avgFps >= 40
479
+ ? AppColors.amber500
480
+ : AppColors.errorColor} stopOpacity="0.02"/>
481
+ </LinearGradient>
482
+ </Defs>
483
+
484
+ {/* 60 FPS Target Baseline */}
485
+ <Line x1="0" y1="7" x2={svgGraphWidth} y2="7" stroke={AppColors.emerald500} strokeWidth="1" strokeDasharray="4,4" opacity="0.4"/>
486
+
487
+ {/* 30 FPS Warning Baseline */}
488
+ <Line x1="0" y1={svgGraphHeight / 2} x2={svgGraphWidth} y2={svgGraphHeight / 2} stroke={AppColors.amber500} strokeWidth="0.8" strokeDasharray="3,3" opacity="0.3"/>
489
+
490
+ {/* Shaded Area Under Curve */}
491
+ {fpsGraphData.areaPath ? (<Path d={fpsGraphData.areaPath} fill="url(#fpsAreaGradient)"/>) : null}
492
+
493
+ {/* Bezier Trend Line */}
494
+ {fpsGraphData.linePath ? (<Path d={fpsGraphData.linePath} fill="none" stroke={avgFps >= 55
495
+ ? AppColors.emerald600
496
+ : avgFps >= 40
497
+ ? AppColors.amber600
498
+ : AppColors.errorColor} strokeWidth="2.2"/>) : null}
499
+
500
+ {/* Sample Nodes */}
501
+ {fpsGraphData.coords.map((c, idx) => {
502
+ const isSelected = selectedFpsPointIdx === idx;
503
+ const isDip = c.val < 50;
504
+ return (<G key={idx}>
505
+ {isDip && (<Circle cx={c.x} cy={c.y} r="4" fill={AppColors.errorColor} opacity="0.25"/>)}
506
+ <Circle cx={c.x} cy={c.y} r={isSelected ? 4 : isDip ? 2.5 : 1.8} fill={isSelected
507
+ ? AppColors.brandPurple
508
+ : isDip
509
+ ? AppColors.errorColor
510
+ : AppColors.emerald600} stroke={AppColors.white} strokeWidth={isSelected ? 1.5 : 0.8}/>
511
+ </G>);
394
512
  })}
513
+ </Svg>
514
+
515
+ {/* Invisible Touch Column Targets for Interactive Inspection */}
516
+ <View style={perfStyles.svgTouchOverlay}>
517
+ {fpsGraphData.coords.map((_, idx) => (<TouchableOpacity key={idx} style={perfStyles.svgTouchCol} activeOpacity={0.7} onPress={() => setSelectedFpsPointIdx(selectedFpsPointIdx === idx ? null : idx)}/>))}
518
+ </View>
395
519
  </View>
520
+
396
521
  <View style={perfStyles.sparklineFooter}>
397
- <Text style={perfStyles.sparklineFooterText}>{t('performance.realtimeWindow')}</Text>
398
- <Text style={perfStyles.sparklineFooterTarget}>{t('performance.fpsTarget')}</Text>
522
+ <Text style={perfStyles.sparklineFooterText}>
523
+ {t('performance.realtimeWindow')}
524
+ </Text>
525
+ <Text style={perfStyles.sparklineFooterTarget}>
526
+ {t('performance.target60Fps', { fps: avgFps })}
527
+ </Text>
399
528
  </View>
400
529
  </View>
401
530
 
402
- {/* ── 2. FRAME BUDGET BREAKDOWN CARD ── */}
531
+ {/* ── 2. FRAME LATENCY DISTRIBUTION HISTOGRAM CARD ── */}
532
+ <View style={perfStyles.budgetCard}>
533
+ <View style={perfStyles.budgetHeader}>
534
+ <View>
535
+ <Text style={perfStyles.budgetTitle}>
536
+ {t('performance.frameLatencyDistTitle')}
537
+ </Text>
538
+ <Text style={perfStyles.heroSubTitle}>
539
+ {t('performance.frameLatencyDistSub')}
540
+ </Text>
541
+ </View>
542
+ </View>
543
+
544
+ <View style={perfStyles.histogramContainer}>
545
+ {latencyBuckets.map(b => (<View key={b.key} style={perfStyles.histogramRow}>
546
+ <View style={perfStyles.histogramLabelCol}>
547
+ <View style={[perfStyles.legendDot, { backgroundColor: b.color }]}/>
548
+ <Text style={perfStyles.histogramLabelText}>{b.label}</Text>
549
+ </View>
550
+
551
+ <View style={perfStyles.histogramBarCol}>
552
+ <View style={perfStyles.histogramTrack}>
553
+ <View style={[
554
+ perfStyles.histogramBarFill,
555
+ {
556
+ width: `${Math.max(b.percent > 0 ? 4 : 0, b.percent)}%`,
557
+ backgroundColor: b.color,
558
+ },
559
+ ]}/>
560
+ </View>
561
+ </View>
562
+
563
+ <View style={perfStyles.histogramStatsCol}>
564
+ <Text style={[
565
+ perfStyles.histogramPercentText,
566
+ { color: b.color },
567
+ ]}>
568
+ {b.percent}%
569
+ </Text>
570
+ <Text style={perfStyles.histogramCountText}>
571
+ ({b.count})
572
+ </Text>
573
+ </View>
574
+ </View>))}
575
+ </View>
576
+ </View>
577
+
578
+ {/* ── 3. FRAME BUDGET BREAKDOWN CARD ── */}
403
579
  <View style={perfStyles.budgetCard}>
404
580
  <View style={perfStyles.budgetHeader}>
405
581
  <Text style={perfStyles.budgetTitle}>{t('performance.budgetUsage')}</Text>
@@ -509,20 +685,45 @@ const PerformanceTab = React.memo(() => {
509
685
  isCritical && { borderColor: AppColors.errorBorder, backgroundColor: AppColors.errorCardBg },
510
686
  ]}>
511
687
  <View style={perfStyles.eventTopRow}>
512
- <View style={[
688
+ <View style={perfStyles.fpsBadgeGroup}>
689
+ <View style={[
513
690
  perfStyles.fpsBadge,
514
691
  { backgroundColor: badgeBg, borderColor: badgeBorder },
515
692
  ]}>
516
- <Text style={[perfStyles.fpsBadgeText, { color: badgeColor }]}>
517
- {event.fps} FPS
518
- </Text>
519
- </View>
693
+ <Text style={[perfStyles.fpsBadgeText, { color: badgeColor }]}>
694
+ {event.fps} FPS
695
+ </Text>
696
+ </View>
520
697
 
521
- <View style={{ flex: 1, paddingHorizontal: 6 }}>
522
- <HighlightText text={event.label} search={search} style={perfStyles.eventLabel} highlightStyle={perfStyles.searchHighlight}/>
523
- <Text style={perfStyles.eventMeta}>
524
- {t('performance.msTotal', { duration: event.durationMs, time: new Date(event.timestamp).toLocaleTimeString() })}
525
- </Text>
698
+ {/* Dropped Frames Badge */}
699
+ <View style={[
700
+ perfStyles.droppedBadge,
701
+ (event.droppedFrames ?? Math.max(0, 60 - event.fps)) > 0
702
+ ? perfStyles.droppedBadgeWarning
703
+ : perfStyles.droppedBadgeOptimal,
704
+ ]}>
705
+ <Text style={[
706
+ perfStyles.droppedBadgeText,
707
+ (event.droppedFrames ?? Math.max(0, 60 - event.fps)) > 0
708
+ ? { color: AppColors.red600 }
709
+ : { color: AppColors.emerald600 },
710
+ ]}>
711
+ {(event.droppedFrames ?? Math.max(0, 60 - event.fps)) > 0
712
+ ? t('performance.droppedFrames', { count: event.droppedFrames ?? Math.max(0, 60 - event.fps) })
713
+ : t('performance.zeroDropped')}
714
+ </Text>
715
+ </View>
716
+
717
+ {/* Bottleneck Thread Tag */}
718
+ {event.bottleneckThread && (<View style={perfStyles.bottleneckTag}>
719
+ <Text style={perfStyles.bottleneckTagText}>
720
+ {event.bottleneckThread === 'JS Thread'
721
+ ? `⚡ ${t('performance.jsBound')}`
722
+ : event.bottleneckThread === 'UI Thread'
723
+ ? `🎨 ${t('performance.uiBound')}`
724
+ : t('performance.balanced')}
725
+ </Text>
726
+ </View>)}
526
727
  </View>
527
728
 
528
729
  <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
@@ -533,18 +734,85 @@ const PerformanceTab = React.memo(() => {
533
734
  </View>
534
735
  </View>
535
736
 
737
+ {/* Event Label & Meta */}
738
+ <View style={{ marginTop: 4 }}>
739
+ <HighlightText text={event.label} search={search} style={perfStyles.eventLabel} highlightStyle={perfStyles.searchHighlight}/>
740
+ <View style={perfStyles.eventMetaRow}>
741
+ <Text style={perfStyles.eventMeta}>
742
+ {t('performance.msTotal', {
743
+ duration: event.durationMs,
744
+ time: new Date(event.timestamp).toLocaleTimeString(),
745
+ })}
746
+ </Text>
747
+ <Text style={perfStyles.eventMetaDot}>•</Text>
748
+ <Text style={[
749
+ perfStyles.frameBudgetHint,
750
+ event.durationMs > 16.67
751
+ ? { color: AppColors.amber800Warm }
752
+ : { color: AppColors.emerald600 },
753
+ ]}>
754
+ {t('performance.frameBudgetMs', {
755
+ time: event.durationMs,
756
+ budget: Number(((event.durationMs / 16.67) * 100).toFixed(0)),
757
+ })}
758
+ </Text>
759
+ </View>
760
+ </View>
761
+
762
+ {/* Visual Frame Time Budget Bar */}
763
+ <View style={perfStyles.frameBarTrack}>
764
+ <View style={[
765
+ perfStyles.frameBarFill,
766
+ {
767
+ width: `${Math.min(100, Math.max(10, (event.durationMs / 33.33) * 100))}%`,
768
+ backgroundColor: badgeColor,
769
+ },
770
+ ]}/>
771
+ </View>
772
+
536
773
  <HighlightText text={event.detail} search={search} style={perfStyles.eventDetail} highlightStyle={perfStyles.searchHighlight}/>
537
774
 
538
775
  {/* ── EXPANDABLE IN-DEPTH METRICS ACCORDION ── */}
539
776
  {isExpanded && (<View style={perfStyles.expandedDetailBox}>
540
- {/* Source file / hook tag */}
541
- {event.source && (<View style={perfStyles.sourceRow}>
542
- <Text style={perfStyles.sourceTag}>{t('common.source')}</Text>
777
+ {/* Screen / Component Context */}
778
+ {(event.screenName || event.source) && (<View style={perfStyles.sourceRow}>
779
+ <Text style={perfStyles.sourceTag}>{t('performance.screenContext')}</Text>
543
780
  <Text style={perfStyles.sourceText} numberOfLines={1}>
544
- {event.source}
781
+ {event.screenName || event.source}
545
782
  </Text>
546
783
  </View>)}
547
784
 
785
+ {/* Frame Metrics Grid */}
786
+ <View style={perfStyles.fpsMetricsGrid}>
787
+ <View style={perfStyles.fpsMetricItem}>
788
+ <Text style={perfStyles.fpsMetricLabel}>{t('performance.target60Fps', { fps: event.fps })}</Text>
789
+ <Text style={[perfStyles.fpsMetricVal, { color: badgeColor }]}>
790
+ {event.fps} FPS
791
+ </Text>
792
+ </View>
793
+ <View style={perfStyles.fpsMetricItem}>
794
+ <Text style={perfStyles.fpsMetricLabel}>{t('performance.frameDuration')}</Text>
795
+ <Text style={perfStyles.fpsMetricVal}>
796
+ {event.durationMs} ms
797
+ </Text>
798
+ </View>
799
+ <View style={perfStyles.fpsMetricItem}>
800
+ <Text style={perfStyles.fpsMetricLabel}>{t('performance.jankRate')}</Text>
801
+ <Text style={[
802
+ perfStyles.fpsMetricVal,
803
+ (event.droppedFrames ?? 0) > 0 ? { color: AppColors.red600 } : { color: AppColors.emerald600 },
804
+ ]}>
805
+ {event.droppedFrames ?? Math.max(0, 60 - event.fps)} frames
806
+ </Text>
807
+ </View>
808
+ <View style={perfStyles.fpsMetricItem}>
809
+ <Text style={perfStyles.fpsMetricLabel}>{t('performance.bottleneck')}</Text>
810
+ <Text style={perfStyles.fpsMetricVal}>
811
+ {event.bottleneckThread || t('performance.balanced')}
812
+ </Text>
813
+ </View>
814
+ </View>
815
+
548
816
  {/* Thread Timing Breakdown */}
549
817
  {event.breakdown && (<View style={perfStyles.breakdownGrid}>
550
818
  <View style={perfStyles.breakdownCol}>
@@ -626,7 +894,7 @@ const perfStyles = StyleSheet.create({
626
894
  color: AppColors.primaryBlack,
627
895
  paddingVertical: 0,
628
896
  },
629
- recordBtn: {
897
+ recordToggleBtn: {
630
898
  flexDirection: 'row',
631
899
  alignItems: 'center',
632
900
  gap: 5,
@@ -635,63 +903,72 @@ const perfStyles = StyleSheet.create({
635
903
  borderRadius: 7,
636
904
  borderWidth: 1,
637
905
  },
638
- recordBtnActive: {
639
- backgroundColor: AppColors.greenBg,
640
- borderColor: AppColors.greenBorder,
906
+ recordToggleActive: {
907
+ backgroundColor: `${AppColors.errorColor}14`,
908
+ borderColor: `${AppColors.errorColor}30`,
641
909
  },
642
- recordBtnPaused: {
643
- backgroundColor: AppColors.amberBg,
644
- borderColor: AppColors.lightOrange,
910
+ recordToggleInactive: {
911
+ backgroundColor: AppColors.graySurface,
912
+ borderColor: AppColors.dividerColor,
645
913
  },
646
- recordDot: {
914
+ recordingDot: {
647
915
  width: 6,
648
916
  height: 6,
649
917
  borderRadius: 3,
650
918
  },
651
- recordBtnText: {
919
+ recordToggleText: {
652
920
  fontFamily: AppFonts.interBold,
653
921
  fontSize: 11,
654
922
  },
655
- filterRow: {
923
+ categoryScrollWrapper: {
656
924
  backgroundColor: AppColors.white,
657
925
  borderBottomWidth: 1,
658
926
  borderBottomColor: AppColors.dividerColor,
659
- paddingVertical: 5,
927
+ height: 42,
928
+ },
929
+ categoryScrollView: {
930
+ flexGrow: 0,
931
+ height: 42,
660
932
  },
661
- filterScrollContent: {
933
+ categoryScroll: {
934
+ flexDirection: 'row',
935
+ alignItems: 'center',
662
936
  gap: 6,
663
937
  paddingHorizontal: 12,
664
- paddingVertical: 2,
938
+ height: 42,
665
939
  },
666
- filterPill: {
940
+ categoryChip: {
667
941
  flexDirection: 'row',
668
942
  alignItems: 'center',
669
- gap: 4,
670
- paddingHorizontal: 10,
671
- paddingVertical: 5,
672
- borderRadius: 16,
943
+ justifyContent: 'center',
944
+ paddingHorizontal: 12,
945
+ height: 28,
946
+ borderRadius: 14,
673
947
  backgroundColor: AppColors.grayBackground,
674
948
  borderWidth: 1,
675
949
  borderColor: AppColors.dividerColor,
676
950
  },
677
- filterPillActive: {
951
+ categoryChipActive: {
678
952
  backgroundColor: AppColors.brandPurple,
679
953
  borderColor: AppColors.brandPurple,
680
954
  },
681
- filterPillText: {
955
+ categoryChipText: {
682
956
  fontFamily: AppFonts.interMedium,
683
957
  fontSize: 11,
684
958
  color: AppColors.grayTextStrong,
685
959
  },
686
- filterPillTextActive: {
960
+ categoryChipTextActive: {
687
961
  fontFamily: AppFonts.interBold,
688
962
  color: AppColors.white,
689
963
  },
964
+ scrollBody: {
965
+ flex: 1,
966
+ },
690
967
  scrollContent: {
691
968
  padding: 12,
692
969
  gap: 10,
693
970
  },
694
- metricCard: {
971
+ heroCard: {
695
972
  backgroundColor: AppColors.white,
696
973
  borderRadius: 12,
697
974
  padding: 14,
@@ -703,17 +980,23 @@ const perfStyles = StyleSheet.create({
703
980
  shadowRadius: 3,
704
981
  elevation: 1,
705
982
  },
706
- metricCardHeader: {
983
+ heroHeaderRow: {
707
984
  flexDirection: 'row',
708
985
  alignItems: 'center',
709
986
  justifyContent: 'space-between',
710
987
  marginBottom: 10,
711
988
  },
712
- metricCardTitle: {
989
+ heroTitle: {
713
990
  fontFamily: AppFonts.interBold,
714
- fontSize: 13.5,
991
+ fontSize: 13,
715
992
  color: AppColors.primaryBlack,
716
993
  },
994
+ heroSubTitle: {
995
+ fontFamily: AppFonts.interRegular,
996
+ fontSize: 9.5,
997
+ color: AppColors.grayTextWeak,
998
+ marginTop: 1,
999
+ },
717
1000
  scorePill: {
718
1001
  paddingHorizontal: 8,
719
1002
  paddingVertical: 3,
@@ -768,26 +1051,85 @@ const perfStyles = StyleSheet.create({
768
1051
  marginTop: 2,
769
1052
  textAlign: 'center',
770
1053
  },
771
- sparklineTrack: {
1054
+ graphScrubTooltip: {
1055
+ alignSelf: 'center',
1056
+ backgroundColor: AppColors.primaryBlack,
1057
+ paddingHorizontal: 8,
1058
+ paddingVertical: 3,
1059
+ borderRadius: 6,
1060
+ marginBottom: 4,
1061
+ },
1062
+ graphScrubTooltipText: {
1063
+ fontFamily: AppFonts.interBold,
1064
+ fontSize: 10,
1065
+ color: AppColors.white,
1066
+ },
1067
+ svgGraphContainer: {
1068
+ width: '100%',
1069
+ height: 64,
1070
+ marginTop: 6,
1071
+ position: 'relative',
1072
+ },
1073
+ svgTouchOverlay: {
1074
+ position: 'absolute',
1075
+ top: 0,
1076
+ left: 0,
1077
+ right: 0,
1078
+ bottom: 0,
772
1079
  flexDirection: 'row',
773
- alignItems: 'flex-end',
774
- justifyContent: 'space-between',
775
- height: 38,
776
- marginTop: 10,
777
- paddingTop: 4,
778
- borderBottomWidth: 1,
779
- borderBottomColor: AppColors.dividerColor,
780
1080
  },
781
- sparklineCol: {
1081
+ svgTouchCol: {
782
1082
  flex: 1,
1083
+ height: '100%',
1084
+ },
1085
+ histogramContainer: {
1086
+ gap: 8,
1087
+ marginTop: 4,
1088
+ },
1089
+ histogramRow: {
1090
+ flexDirection: 'row',
783
1091
  alignItems: 'center',
784
- justifyContent: 'flex-end',
1092
+ gap: 8,
1093
+ },
1094
+ histogramLabelCol: {
1095
+ flexDirection: 'row',
1096
+ alignItems: 'center',
1097
+ gap: 6,
1098
+ width: 140,
1099
+ },
1100
+ histogramLabelText: {
1101
+ fontFamily: AppFonts.interMedium,
1102
+ fontSize: 10,
1103
+ color: AppColors.grayTextStrong,
1104
+ },
1105
+ histogramBarCol: {
1106
+ flex: 1,
1107
+ },
1108
+ histogramTrack: {
1109
+ height: 8,
1110
+ backgroundColor: AppColors.grayBackground,
1111
+ borderRadius: 4,
1112
+ overflow: 'hidden',
1113
+ },
1114
+ histogramBarFill: {
785
1115
  height: '100%',
786
- paddingHorizontal: 1,
1116
+ borderRadius: 4,
787
1117
  },
788
- sparklineBar: {
789
- width: '80%',
790
- borderRadius: 2,
1118
+ histogramStatsCol: {
1119
+ flexDirection: 'row',
1120
+ alignItems: 'center',
1121
+ gap: 4,
1122
+ minWidth: 52,
1123
+ justifyContent: 'flex-end',
1124
+ },
1125
+ histogramPercentText: {
1126
+ fontFamily: AppFonts.interBold,
1127
+ fontSize: 10.5,
1128
+ },
1129
+ histogramCountText: {
1130
+ fontFamily: AppFonts.interRegular,
1131
+ fontSize: 9.5,
1132
+ color: AppColors.grayTextWeak,
791
1133
  },
792
1134
  sparklineFooter: {
793
1135
  flexDirection: 'row',
@@ -920,6 +1262,12 @@ const perfStyles = StyleSheet.create({
920
1262
  alignItems: 'center',
921
1263
  justifyContent: 'space-between',
922
1264
  },
1265
+ fpsBadgeGroup: {
1266
+ flexDirection: 'row',
1267
+ alignItems: 'center',
1268
+ gap: 6,
1269
+ flexWrap: 'wrap',
1270
+ },
923
1271
  fpsBadge: {
924
1272
  paddingHorizontal: 6,
925
1273
  paddingVertical: 2,
@@ -930,6 +1278,91 @@ const perfStyles = StyleSheet.create({
930
1278
  fontFamily: AppFonts.interBold,
931
1279
  fontSize: 10,
932
1280
  },
1281
+ droppedBadge: {
1282
+ paddingHorizontal: 6,
1283
+ paddingVertical: 2,
1284
+ borderRadius: 5,
1285
+ borderWidth: 1,
1286
+ },
1287
+ droppedBadgeWarning: {
1288
+ backgroundColor: AppColors.red100,
1289
+ borderColor: AppColors.errorBorder,
1290
+ },
1291
+ droppedBadgeOptimal: {
1292
+ backgroundColor: AppColors.green100,
1293
+ borderColor: AppColors.greenBorder,
1294
+ },
1295
+ droppedBadgeText: {
1296
+ fontFamily: AppFonts.interBold,
1297
+ fontSize: 9.5,
1298
+ },
1299
+ bottleneckTag: {
1300
+ backgroundColor: AppColors.purpleShade50,
1301
+ paddingHorizontal: 5,
1302
+ paddingVertical: 2,
1303
+ borderRadius: 4,
1304
+ borderWidth: 1,
1305
+ borderColor: AppColors.purpleTint2,
1306
+ },
1307
+ bottleneckTagText: {
1308
+ fontFamily: AppFonts.interMedium,
1309
+ fontSize: 9.5,
1310
+ color: AppColors.purple,
1311
+ },
1312
+ eventMetaRow: {
1313
+ flexDirection: 'row',
1314
+ alignItems: 'center',
1315
+ gap: 6,
1316
+ marginTop: 2,
1317
+ flexWrap: 'wrap',
1318
+ },
1319
+ eventMetaDot: {
1320
+ fontSize: 9,
1321
+ color: AppColors.grayTextWeak,
1322
+ },
1323
+ frameBudgetHint: {
1324
+ fontFamily: AppFonts.interMedium,
1325
+ fontSize: 10,
1326
+ },
1327
+ frameBarTrack: {
1328
+ height: 4,
1329
+ borderRadius: 2,
1330
+ backgroundColor: AppColors.graySurface,
1331
+ overflow: 'hidden',
1332
+ marginVertical: 4,
1333
+ },
1334
+ frameBarFill: {
1335
+ height: '100%',
1336
+ borderRadius: 2,
1337
+ },
1338
+ fpsMetricsGrid: {
1339
+ flexDirection: 'row',
1340
+ alignItems: 'center',
1341
+ backgroundColor: AppColors.white,
1342
+ borderRadius: 6,
1343
+ padding: 6,
1344
+ borderWidth: 1,
1345
+ borderColor: AppColors.dividerColor,
1346
+ justifyContent: 'space-between',
1347
+ gap: 4,
1348
+ },
1349
+ fpsMetricItem: {
1350
+ alignItems: 'center',
1351
+ flex: 1,
1352
+ },
1353
+ fpsMetricLabel: {
1354
+ fontFamily: AppFonts.interRegular,
1355
+ fontSize: 8.5,
1356
+ color: AppColors.grayTextWeak,
1357
+ textAlign: 'center',
1358
+ },
1359
+ fpsMetricVal: {
1360
+ fontFamily: AppFonts.interBold,
1361
+ fontSize: 10.5,
1362
+ color: AppColors.primaryBlack,
1363
+ marginTop: 1,
1364
+ textAlign: 'center',
1365
+ },
933
1366
  eventLabel: {
934
1367
  fontFamily: AppFonts.interBold,
935
1368
  fontSize: 12,