react-native-nitro-tracing 0.6.0

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 (233) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/NitroTracing.podspec +32 -0
  4. package/README.md +75 -0
  5. package/android/CMakeLists.txt +49 -0
  6. package/android/build.gradle +133 -0
  7. package/android/fix-prefab.gradle +51 -0
  8. package/android/gradle.properties +5 -0
  9. package/android/proguard-rules.pro +5 -0
  10. package/android/src/main/AndroidManifest.xml +2 -0
  11. package/android/src/main/cpp/cpp-adapter.cpp +47 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrotracing/HermesSamplingGuard.kt +43 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingFrameSource.kt +36 -0
  14. package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingPackage.kt +31 -0
  15. package/client.ts +15 -0
  16. package/cpp/HermesSampling.cpp +26 -0
  17. package/cpp/HermesSampling.hpp +10 -0
  18. package/cpp/HybridRecording.cpp +66 -0
  19. package/cpp/HybridRecording.hpp +30 -0
  20. package/cpp/HybridTraceSpan.cpp +10 -0
  21. package/cpp/HybridTraceSpan.hpp +17 -0
  22. package/cpp/HybridTracing.cpp +10 -0
  23. package/cpp/HybridTracing.hpp +10 -0
  24. package/cpp/NativeConversions.cpp +77 -0
  25. package/cpp/NativeConversions.hpp +15 -0
  26. package/cpp/core/NativeSampler.cpp +152 -0
  27. package/cpp/core/NativeSampler.hpp +56 -0
  28. package/cpp/core/Recorder.cpp +218 -0
  29. package/cpp/core/Recorder.hpp +60 -0
  30. package/cpp/core/Retention.cpp +26 -0
  31. package/cpp/core/Retention.hpp +7 -0
  32. package/cpp/core/TraceJson.cpp +214 -0
  33. package/cpp/core/TraceJson.hpp +7 -0
  34. package/cpp/core/TraceTypes.hpp +62 -0
  35. package/expo.ts +2 -0
  36. package/ios/NitroTracingFrameSource.mm +35 -0
  37. package/lib/commonjs/client.cjs +767 -0
  38. package/lib/commonjs/client.cjs.map +7 -0
  39. package/lib/commonjs/expo.cjs +88 -0
  40. package/lib/commonjs/expo.cjs.map +7 -0
  41. package/lib/commonjs/index.cjs +28 -0
  42. package/lib/commonjs/index.cjs.map +7 -0
  43. package/lib/commonjs/performance.cjs +112 -0
  44. package/lib/commonjs/performance.cjs.map +7 -0
  45. package/lib/commonjs/plugins.cjs +477 -0
  46. package/lib/commonjs/plugins.cjs.map +7 -0
  47. package/lib/commonjs/react.cjs +3833 -0
  48. package/lib/commonjs/react.cjs.map +7 -0
  49. package/lib/commonjs/release-profiler.cjs +142 -0
  50. package/lib/commonjs/release-profiler.cjs.map +7 -0
  51. package/lib/commonjs/sentry.cjs +188 -0
  52. package/lib/commonjs/sentry.cjs.map +7 -0
  53. package/lib/module/client.mjs +768 -0
  54. package/lib/module/client.mjs.map +7 -0
  55. package/lib/module/expo.mjs +62 -0
  56. package/lib/module/expo.mjs.map +7 -0
  57. package/lib/module/index.mjs +7 -0
  58. package/lib/module/index.mjs.map +7 -0
  59. package/lib/module/performance.mjs +89 -0
  60. package/lib/module/performance.mjs.map +7 -0
  61. package/lib/module/plugins.mjs +461 -0
  62. package/lib/module/plugins.mjs.map +7 -0
  63. package/lib/module/react.mjs +3851 -0
  64. package/lib/module/react.mjs.map +7 -0
  65. package/lib/module/release-profiler.mjs +137 -0
  66. package/lib/module/release-profiler.mjs.map +7 -0
  67. package/lib/module/sentry.mjs +165 -0
  68. package/lib/module/sentry.mjs.map +7 -0
  69. package/lib/typescript/client.d.ts +4 -0
  70. package/lib/typescript/expo.d.ts +2 -0
  71. package/lib/typescript/performance.d.ts +2 -0
  72. package/lib/typescript/plugins.d.ts +12 -0
  73. package/lib/typescript/react.d.ts +5 -0
  74. package/lib/typescript/release-profiler.d.ts +2 -0
  75. package/lib/typescript/sentry.d.ts +2 -0
  76. package/lib/typescript/src/client/TracingSink.d.ts +10 -0
  77. package/lib/typescript/src/client/createTraceClient.d.ts +82 -0
  78. package/lib/typescript/src/client/playground.d.ts +12 -0
  79. package/lib/typescript/src/expo/ExpoAdapter.d.ts +15 -0
  80. package/lib/typescript/src/expo/index.d.ts +2 -0
  81. package/lib/typescript/src/index.d.ts +6 -0
  82. package/lib/typescript/src/plugins/TracePlugin.d.ts +22 -0
  83. package/lib/typescript/src/plugins/errors.d.ts +15 -0
  84. package/lib/typescript/src/plugins/nativeMetrics.d.ts +13 -0
  85. package/lib/typescript/src/plugins/navigation.d.ts +29 -0
  86. package/lib/typescript/src/plugins/network.d.ts +23 -0
  87. package/lib/typescript/src/plugins/orderSpans.d.ts +3 -0
  88. package/lib/typescript/src/plugins/performance.d.ts +13 -0
  89. package/lib/typescript/src/plugins/releaseProfiler.d.ts +25 -0
  90. package/lib/typescript/src/plugins/runtimeMetrics.d.ts +17 -0
  91. package/lib/typescript/src/plugins/sentry.d.ts +15 -0
  92. package/lib/typescript/src/plugins/startPlugins.d.ts +4 -0
  93. package/lib/typescript/src/react/DetailView.d.ts +11 -0
  94. package/lib/typescript/src/react/ExplorerFilters.d.ts +9 -0
  95. package/lib/typescript/src/react/ExplorerView.d.ts +9 -0
  96. package/lib/typescript/src/react/InspectorControls.d.ts +229 -0
  97. package/lib/typescript/src/react/InspectorHeader.d.ts +8 -0
  98. package/lib/typescript/src/react/InspectorSheet.d.ts +16 -0
  99. package/lib/typescript/src/react/MetricsView.d.ts +7 -0
  100. package/lib/typescript/src/react/SummaryView.d.ts +11 -0
  101. package/lib/typescript/src/react/TimelineView.d.ts +8 -0
  102. package/lib/typescript/src/react/TraceInspector.d.ts +11 -0
  103. package/lib/typescript/src/react/TraceOverlay.d.ts +17 -0
  104. package/lib/typescript/src/react/explorerModel.d.ts +38 -0
  105. package/lib/typescript/src/react/labels.d.ts +203 -0
  106. package/lib/typescript/src/react/native.d.ts +42 -0
  107. package/lib/typescript/src/react/readSnapshot.d.ts +4 -0
  108. package/lib/typescript/src/react/store.d.ts +9 -0
  109. package/lib/typescript/src/react/topics.d.ts +80 -0
  110. package/lib/typescript/src/react/useAppReadyMetric.d.ts +3 -0
  111. package/lib/typescript/src/react/useLiveRecording.d.ts +4 -0
  112. package/lib/typescript/src/react/useTraceViewer.d.ts +140 -0
  113. package/lib/typescript/src/react/viewerModel.d.ts +59 -0
  114. package/lib/typescript/src/specs/Recording.nitro.d.ts +31 -0
  115. package/lib/typescript/src/specs/TraceSpan.nitro.d.ts +14 -0
  116. package/lib/typescript/src/specs/Tracing.nitro.d.ts +17 -0
  117. package/lib/typescript/src/types/CompletedSpanOptions.d.ts +15 -0
  118. package/lib/typescript/src/types/EventContext.d.ts +8 -0
  119. package/lib/typescript/src/types/MarkEvent.d.ts +4 -0
  120. package/lib/typescript/src/types/MarkOptions.d.ts +6 -0
  121. package/lib/typescript/src/types/MetricEvent.d.ts +8 -0
  122. package/lib/typescript/src/types/MetricOptions.d.ts +10 -0
  123. package/lib/typescript/src/types/NativeSamplingOptions.d.ts +7 -0
  124. package/lib/typescript/src/types/ReadOptions.d.ts +7 -0
  125. package/lib/typescript/src/types/RecordingOptions.d.ts +11 -0
  126. package/lib/typescript/src/types/RecordingStats.d.ts +25 -0
  127. package/lib/typescript/src/types/SpanEvent.d.ts +13 -0
  128. package/lib/typescript/src/types/SpanOptions.d.ts +6 -0
  129. package/lib/typescript/src/types/SpanOutcome.d.ts +2 -0
  130. package/lib/typescript/src/types/TraceAttribute.d.ts +7 -0
  131. package/lib/typescript/src/types/TraceAttributes.d.ts +3 -0
  132. package/lib/typescript/src/types/TraceContext.d.ts +10 -0
  133. package/lib/typescript/src/types/TracePage.d.ts +18 -0
  134. package/lib/typescript/src/types.d.ts +17 -0
  135. package/nitro.json +20 -0
  136. package/nitrogen/generated/.gitattributes +1 -0
  137. package/nitrogen/generated/android/NitroTracing+autolinking.cmake +83 -0
  138. package/nitrogen/generated/android/NitroTracing+autolinking.gradle +27 -0
  139. package/nitrogen/generated/android/NitroTracingOnLoad.cpp +49 -0
  140. package/nitrogen/generated/android/NitroTracingOnLoad.hpp +34 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrotracing/NitroTracingOnLoad.kt +35 -0
  142. package/nitrogen/generated/ios/NitroTracing+autolinking.rb +62 -0
  143. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.cpp +17 -0
  144. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.hpp +27 -0
  145. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Umbrella.hpp +38 -0
  146. package/nitrogen/generated/ios/NitroTracingAutolinking.mm +35 -0
  147. package/nitrogen/generated/ios/NitroTracingAutolinking.swift +16 -0
  148. package/nitrogen/generated/shared/c++/CompletedSpanOptions.hpp +122 -0
  149. package/nitrogen/generated/shared/c++/HybridRecordingSpec.cpp +31 -0
  150. package/nitrogen/generated/shared/c++/HybridRecordingSpec.hpp +100 -0
  151. package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.cpp +23 -0
  152. package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.hpp +65 -0
  153. package/nitrogen/generated/shared/c++/HybridTracingSpec.cpp +22 -0
  154. package/nitrogen/generated/shared/c++/HybridTracingSpec.hpp +68 -0
  155. package/nitrogen/generated/shared/c++/MarkEvent.hpp +102 -0
  156. package/nitrogen/generated/shared/c++/MarkOptions.hpp +99 -0
  157. package/nitrogen/generated/shared/c++/MetricEvent.hpp +110 -0
  158. package/nitrogen/generated/shared/c++/MetricOptions.hpp +107 -0
  159. package/nitrogen/generated/shared/c++/NativeSamplingOptions.hpp +87 -0
  160. package/nitrogen/generated/shared/c++/ReadOptions.hpp +87 -0
  161. package/nitrogen/generated/shared/c++/RecordingOptions.hpp +95 -0
  162. package/nitrogen/generated/shared/c++/RecordingStats.hpp +123 -0
  163. package/nitrogen/generated/shared/c++/SpanEvent.hpp +121 -0
  164. package/nitrogen/generated/shared/c++/SpanOptions.hpp +99 -0
  165. package/nitrogen/generated/shared/c++/SpanOutcome.hpp +84 -0
  166. package/nitrogen/generated/shared/c++/TraceAttribute.hpp +87 -0
  167. package/nitrogen/generated/shared/c++/TracePage.hpp +111 -0
  168. package/package.json +236 -0
  169. package/performance.ts +2 -0
  170. package/plugins.ts +24 -0
  171. package/react-native.config.js +16 -0
  172. package/react.ts +10 -0
  173. package/release-profiler.ts +6 -0
  174. package/sentry.ts +2 -0
  175. package/src/client/TracingSink.ts +25 -0
  176. package/src/client/createTraceClient.ts +439 -0
  177. package/src/client/playground.ts +143 -0
  178. package/src/expo/ExpoAdapter.ts +71 -0
  179. package/src/expo/index.ts +2 -0
  180. package/src/index.ts +8 -0
  181. package/src/plugins/TracePlugin.ts +22 -0
  182. package/src/plugins/errors.ts +69 -0
  183. package/src/plugins/nativeMetrics.ts +26 -0
  184. package/src/plugins/navigation.ts +111 -0
  185. package/src/plugins/network.ts +178 -0
  186. package/src/plugins/orderSpans.ts +22 -0
  187. package/src/plugins/performance.ts +120 -0
  188. package/src/plugins/releaseProfiler.ts +123 -0
  189. package/src/plugins/runtimeMetrics.ts +131 -0
  190. package/src/plugins/sentry.ts +176 -0
  191. package/src/plugins/startPlugins.ts +58 -0
  192. package/src/react/DetailView.tsx +514 -0
  193. package/src/react/ExplorerFilters.tsx +151 -0
  194. package/src/react/ExplorerView.tsx +243 -0
  195. package/src/react/InspectorControls.tsx +651 -0
  196. package/src/react/InspectorHeader.tsx +171 -0
  197. package/src/react/InspectorSheet.tsx +89 -0
  198. package/src/react/MetricsView.tsx +182 -0
  199. package/src/react/SummaryView.tsx +121 -0
  200. package/src/react/TimelineView.tsx +87 -0
  201. package/src/react/TraceInspector.tsx +196 -0
  202. package/src/react/TraceOverlay.tsx +585 -0
  203. package/src/react/explorerModel.ts +285 -0
  204. package/src/react/labels.ts +234 -0
  205. package/src/react/native.tsx +159 -0
  206. package/src/react/readSnapshot.ts +58 -0
  207. package/src/react/store.ts +31 -0
  208. package/src/react/topics.ts +427 -0
  209. package/src/react/useAppReadyMetric.ts +10 -0
  210. package/src/react/useLiveRecording.ts +43 -0
  211. package/src/react/useTraceViewer.ts +333 -0
  212. package/src/react/viewerModel.ts +172 -0
  213. package/src/specs/Recording.nitro.ts +40 -0
  214. package/src/specs/TraceSpan.nitro.ts +14 -0
  215. package/src/specs/Tracing.nitro.ts +14 -0
  216. package/src/types/CompletedSpanOptions.ts +15 -0
  217. package/src/types/EventContext.ts +8 -0
  218. package/src/types/MarkEvent.ts +3 -0
  219. package/src/types/MarkOptions.ts +6 -0
  220. package/src/types/MetricEvent.ts +8 -0
  221. package/src/types/MetricOptions.ts +10 -0
  222. package/src/types/NativeSamplingOptions.ts +7 -0
  223. package/src/types/ReadOptions.ts +7 -0
  224. package/src/types/RecordingOptions.ts +11 -0
  225. package/src/types/RecordingStats.ts +25 -0
  226. package/src/types/SpanEvent.ts +13 -0
  227. package/src/types/SpanOptions.ts +6 -0
  228. package/src/types/SpanOutcome.ts +2 -0
  229. package/src/types/TraceAttribute.ts +7 -0
  230. package/src/types/TraceAttributes.ts +4 -0
  231. package/src/types/TraceContext.ts +10 -0
  232. package/src/types/TracePage.ts +18 -0
  233. package/src/types.ts +17 -0
@@ -0,0 +1,585 @@
1
+ import React, {
2
+ useContext,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ useSyncExternalStore,
7
+ } from 'react'
8
+ import {
9
+ Animated,
10
+ PanResponder,
11
+ Pressable,
12
+ ScrollView,
13
+ StyleSheet,
14
+ Text,
15
+ View,
16
+ useWindowDimensions,
17
+ } from 'react-native'
18
+ import {
19
+ initialWindowMetrics,
20
+ SafeAreaInsetsContext,
21
+ } from 'react-native-safe-area-context'
22
+ import type { TraceClient } from '../client/createTraceClient'
23
+ import { palette } from './InspectorControls'
24
+ import {
25
+ createTranslator,
26
+ type InspectorLabels,
27
+ type InspectorTranslator,
28
+ } from './labels'
29
+ import {
30
+ buildTimeline,
31
+ currentVisit,
32
+ defaultBudgets,
33
+ latestMetric,
34
+ summarizeTopics,
35
+ topicGlyph,
36
+ type Budgets,
37
+ type VisitSummary,
38
+ } from './topics'
39
+ import { useLiveRecording } from './useLiveRecording'
40
+ import { Glass } from './native'
41
+
42
+ type Detent = 'closed' | 'peek' | 'half'
43
+ const BUBBLE = 56
44
+ const PEEK = 150
45
+ const noInsets = { top: 0, right: 0, bottom: 0, left: 0 }
46
+ /** The app's safe-area provider when present, else launch metrics; never adds a native view. */
47
+ const useInsets = () =>
48
+ useContext(SafeAreaInsetsContext) ?? initialWindowMetrics?.insets ?? noInsets
49
+ const round = (value?: number) =>
50
+ value === undefined ? '—' : Math.round(value)
51
+
52
+ /**
53
+ * Floating entry point and live, non-modal sheet for the current screen. The app stays
54
+ * usable above the sheet; mount it last at the app root. It sits below the app's own
55
+ * native modals. Rendering it is the app's decision (e.g. internal or UAT builds only).
56
+ */
57
+ export function TraceOverlay({
58
+ client,
59
+ labels,
60
+ translate,
61
+ budgets,
62
+ bottomOffset = 0,
63
+ }: {
64
+ client: TraceClient
65
+ labels?: Partial<InspectorLabels>
66
+ translate?: InspectorTranslator
67
+ budgets?: Partial<Budgets>
68
+ /** Dock the sheet this far above the bottom edge, e.g. the app's tab bar height, so it stays usable. */
69
+ bottomOffset?: number
70
+ }) {
71
+ return (
72
+ // Plain Views only: a native provider here would swallow touches on Android.
73
+ <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
74
+ <Overlay
75
+ client={client}
76
+ t={translate ?? createTranslator(labels)}
77
+ budgets={{ ...defaultBudgets, ...budgets }}
78
+ bottomOffset={bottomOffset}
79
+ />
80
+ </View>
81
+ )
82
+ }
83
+
84
+ function Overlay({
85
+ client,
86
+ t,
87
+ budgets,
88
+ bottomOffset,
89
+ }: {
90
+ client: TraceClient
91
+ t: InspectorTranslator
92
+ budgets: Budgets
93
+ bottomOffset: number
94
+ }) {
95
+ const snapshot = useSyncExternalStore(
96
+ client.subscribe,
97
+ client.getSnapshot,
98
+ client.getSnapshot
99
+ )
100
+ const [detent, setDetent] = useState<Detent>('closed')
101
+ // The full inspector owns the screen while it is open; pause live polling meanwhile.
102
+ // The closed bubble only needs fps and an issue count: poll half as often.
103
+ const page = useLiveRecording(
104
+ client,
105
+ !snapshot.visible && snapshot.recording,
106
+ detent === 'closed' ? 2000 : 1000
107
+ )
108
+ const { appReadyMs, screenMs, requestMs, stallMs } = budgets
109
+ const visit = useMemo(() => {
110
+ if (!page) return undefined
111
+ const { issues } = summarizeTopics(page, snapshot.collectors ?? [], {
112
+ appReadyMs,
113
+ screenMs,
114
+ requestMs,
115
+ stallMs,
116
+ })
117
+ return currentVisit(buildTimeline(page, issues, 300))
118
+ }, [page, snapshot.collectors, appReadyMs, screenMs, requestMs, stallMs])
119
+ const vitals = useMemo(
120
+ () => ({
121
+ uiFps: page && latestMetric(page, 'ui.fps'),
122
+ jsFps: page && latestMetric(page, 'js.frame_callback.rate'),
123
+ cpu: page && latestMetric(page, 'process.cpu'),
124
+ memory: page && latestMetric(page, 'process.memory'),
125
+ }),
126
+ [page]
127
+ )
128
+ if (snapshot.visible || snapshot.disposed) return null
129
+ return detent === 'closed' ? (
130
+ <Bubble
131
+ fps={vitals.uiFps ?? vitals.jsFps}
132
+ native={vitals.uiFps !== undefined}
133
+ issues={visit?.issues ?? 0}
134
+ onPress={() => setDetent('half')}
135
+ onLongPress={client.open}
136
+ />
137
+ ) : (
138
+ <Sheet
139
+ bottomOffset={bottomOffset}
140
+ detent={detent}
141
+ setDetent={setDetent}
142
+ openInspector={() => {
143
+ setDetent('closed')
144
+ client.open()
145
+ }}
146
+ nowMs={client.getRecording()?.getStats().nowMs}
147
+ visit={visit}
148
+ vitals={vitals}
149
+ recording={snapshot.recording}
150
+ flag={() => client.flag()}
151
+ share={client.canShare ? () => client.shareTrace('perfetto') : undefined}
152
+ t={t}
153
+ />
154
+ )
155
+ }
156
+
157
+ function Bubble({
158
+ fps,
159
+ native,
160
+ issues,
161
+ onPress,
162
+ onLongPress,
163
+ }: {
164
+ fps?: number
165
+ native: boolean
166
+ issues: number
167
+ onPress: () => void
168
+ onLongPress: () => void
169
+ }) {
170
+ const { width, height } = useWindowDimensions()
171
+ const insets = useInsets()
172
+ const base = useRef({ x: width - BUBBLE - 12, y: height * 0.55 })
173
+ const position = useRef(new Animated.ValueXY(base.current)).current
174
+ const handlers = useRef({ onPress, onLongPress })
175
+ handlers.current = { onPress, onLongPress }
176
+ const responder = useMemo(() => {
177
+ let moved = false
178
+ let longPressed = false
179
+ let timer: ReturnType<typeof setTimeout> | undefined
180
+ return PanResponder.create({
181
+ onStartShouldSetPanResponder: () => true,
182
+ onPanResponderGrant: () => {
183
+ moved = false
184
+ longPressed = false
185
+ timer = setTimeout(() => {
186
+ longPressed = true
187
+ handlers.current.onLongPress()
188
+ }, 500)
189
+ },
190
+ onPanResponderMove: (_, g) => {
191
+ if (Math.abs(g.dx) + Math.abs(g.dy) > 6) {
192
+ moved = true
193
+ clearTimeout(timer)
194
+ }
195
+ position.setValue({
196
+ x: base.current.x + g.dx,
197
+ y: base.current.y + g.dy,
198
+ })
199
+ },
200
+ onPanResponderRelease: (_, g) => {
201
+ clearTimeout(timer)
202
+ if (!moved) {
203
+ if (!longPressed) handlers.current.onPress()
204
+ return
205
+ }
206
+ // Snap to the nearest side edge, clamped inside the safe area.
207
+ const x = base.current.x + g.dx
208
+ const y = base.current.y + g.dy
209
+ base.current = {
210
+ x: x + BUBBLE / 2 < width / 2 ? 12 : width - BUBBLE - 12,
211
+ y: Math.min(
212
+ Math.max(y, insets.top + 8),
213
+ height - BUBBLE - insets.bottom - 8
214
+ ),
215
+ }
216
+ Animated.spring(position, {
217
+ toValue: base.current,
218
+ useNativeDriver: false,
219
+ }).start()
220
+ },
221
+ onPanResponderTerminate: () => {
222
+ clearTimeout(timer)
223
+ position.setValue(base.current)
224
+ },
225
+ })
226
+ }, [position, width, height, insets.top, insets.bottom])
227
+ const alert = issues > 0
228
+ return (
229
+ <Animated.View
230
+ {...responder.panHandlers}
231
+ accessibilityRole="button"
232
+ accessibilityLabel="Performance overlay"
233
+ accessibilityHint="Tap for live activity, long press for the full inspector"
234
+ style={[
235
+ styles.bubble,
236
+ alert && styles.bubbleAlert,
237
+ { transform: position.getTranslateTransform() },
238
+ ]}
239
+ >
240
+ <Glass
241
+ style={[StyleSheet.absoluteFill, styles.bubbleGlass]}
242
+ fallbackColor={palette.bg}
243
+ interactive
244
+ />
245
+ <Text style={[styles.bubbleValue, alert && { color: palette.error }]}>
246
+ {fps === undefined ? '—' : Math.round(fps)}
247
+ </Text>
248
+ <Text style={styles.bubbleUnit}>
249
+ {alert ? `${issues} ⚠` : native ? 'UI fps' : 'JS fps'}
250
+ </Text>
251
+ </Animated.View>
252
+ )
253
+ }
254
+
255
+ function Sheet({
256
+ detent,
257
+ setDetent,
258
+ openInspector,
259
+ nowMs,
260
+ visit,
261
+ vitals,
262
+ recording,
263
+ flag,
264
+ share,
265
+ t,
266
+ bottomOffset,
267
+ }: {
268
+ bottomOffset: number
269
+ detent: Exclude<Detent, 'closed'>
270
+ setDetent: (detent: Detent) => void
271
+ openInspector: () => void
272
+ nowMs?: number
273
+ visit?: VisitSummary
274
+ vitals: Record<'uiFps' | 'jsFps' | 'cpu' | 'memory', number | undefined>
275
+ recording: boolean
276
+ flag: () => void
277
+ share?: () => Promise<void>
278
+ t: InspectorTranslator
279
+ }) {
280
+ const { height } = useWindowDimensions()
281
+ const insets = useInsets()
282
+ const [flagged, setFlagged] = useState(false)
283
+ const inset = bottomOffset > 0 ? 0 : insets.bottom
284
+ const half = Math.round(height * 0.45) + inset
285
+ const peek = PEEK + inset
286
+ // The sheet is always `half` tall; detents translate it down so only `peek` shows.
287
+ const offsetFor = (value: Detent) =>
288
+ value === 'half' ? 0 : value === 'peek' ? half - peek : half + bottomOffset
289
+ const translate = useRef(new Animated.Value(half + bottomOffset)).current
290
+ const settle = (value: Detent) =>
291
+ Animated.spring(translate, {
292
+ toValue: offsetFor(value),
293
+ useNativeDriver: true,
294
+ bounciness: 0,
295
+ }).start(() => value === 'closed' && setDetent('closed'))
296
+ const current = useRef(detent)
297
+ current.current = detent
298
+ React.useEffect(() => {
299
+ settle(detent)
300
+ // eslint-disable-next-line react-hooks/exhaustive-deps
301
+ }, [detent, half])
302
+ const responder = useMemo(
303
+ () =>
304
+ PanResponder.create({
305
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dy) > 4,
306
+ onPanResponderMove: (_, g) =>
307
+ translate.setValue(Math.max(-80, offsetFor(current.current) + g.dy)),
308
+ onPanResponderRelease: (_, g) => {
309
+ const at = offsetFor(current.current) + g.dy
310
+ if (at < -60) {
311
+ settle(current.current)
312
+ openInspector()
313
+ } else if (at > half - peek / 2 || g.vy > 1.5) settle('closed')
314
+ else {
315
+ const next = at < (half - peek) / 2 ? 'half' : 'peek'
316
+ if (next === current.current) settle(next)
317
+ else setDetent(next)
318
+ }
319
+ },
320
+ }),
321
+ // eslint-disable-next-line react-hooks/exhaustive-deps
322
+ [half, peek]
323
+ )
324
+ const elapsed =
325
+ nowMs !== undefined && visit?.sinceMs !== undefined
326
+ ? (nowMs - visit.sinceMs) / 1000
327
+ : undefined
328
+ const latest = visit?.events[0]
329
+ return (
330
+ <Animated.View
331
+ style={[
332
+ styles.sheet,
333
+ { height: half, paddingBottom: inset, bottom: bottomOffset },
334
+ { transform: [{ translateY: translate }] },
335
+ ]}
336
+ >
337
+ <Glass
338
+ style={[StyleSheet.absoluteFill, styles.sheetGlass]}
339
+ fallbackColor={palette.bg}
340
+ />
341
+ <View {...responder.panHandlers} style={styles.grabArea}>
342
+ <View style={styles.grabber} />
343
+ <View style={styles.row}>
344
+ <Text style={styles.screen} numberOfLines={1}>
345
+ {recording ? '●' : '○'} {visit?.screen ?? t('liveAllActivity')}
346
+ {elapsed !== undefined && (
347
+ <Text style={styles.muted}> · {elapsed.toFixed(0)} s</Text>
348
+ )}
349
+ </Text>
350
+ <Pressable
351
+ accessibilityRole="button"
352
+ onPress={() => setDetent(detent === 'half' ? 'peek' : 'half')}
353
+ style={styles.chip}
354
+ >
355
+ <Text style={styles.chipText}>{detent === 'half' ? '▾' : '▴'}</Text>
356
+ </Pressable>
357
+ <Pressable
358
+ accessibilityRole="button"
359
+ onPress={openInspector}
360
+ style={styles.chip}
361
+ >
362
+ <Text style={styles.chipText}>{t('liveInspector')}</Text>
363
+ </Pressable>
364
+ <Pressable
365
+ accessibilityRole="button"
366
+ accessibilityLabel={t('close')}
367
+ onPress={() => settle('closed')}
368
+ style={styles.chip}
369
+ >
370
+ <Text style={styles.chipText}>✕</Text>
371
+ </Pressable>
372
+ </View>
373
+ <View style={styles.stats}>
374
+ <Stat label="UI fps" value={round(vitals.uiFps)} />
375
+ <Stat label="JS fps" value={round(vitals.jsFps)} />
376
+ <Stat label="CPU %" value={round(vitals.cpu)} />
377
+ <Stat label="MB" value={round(vitals.memory)} />
378
+ </View>
379
+ <View style={styles.stats}>
380
+ <Stat label={t('liveRequests')} value={visit?.requests ?? 0} />
381
+ <Stat label={t('liveStalls')} value={visit?.stalls ?? 0} alert />
382
+ <Stat label={t('liveErrors')} value={visit?.errors ?? 0} alert />
383
+ <Pressable
384
+ accessibilityRole="button"
385
+ onPress={() => {
386
+ flag()
387
+ setFlagged(true)
388
+ setTimeout(() => setFlagged(false), 1200)
389
+ }}
390
+ style={styles.action}
391
+ >
392
+ <Text style={styles.actionText}>
393
+ ⚑ {t(flagged ? 'liveFlagged' : 'liveFlag')}
394
+ </Text>
395
+ </Pressable>
396
+ {share && (
397
+ <Pressable
398
+ accessibilityRole="button"
399
+ onPress={() => void share()}
400
+ style={styles.action}
401
+ >
402
+ <Text style={styles.actionText}>⇪ {t('liveShare')}</Text>
403
+ </Pressable>
404
+ )}
405
+ </View>
406
+ </View>
407
+ {detent === 'peek' ? (
408
+ latest && (
409
+ <Text style={styles.muted} numberOfLines={1}>
410
+ {topicGlyph[latest.topic].icon} {latest.event.name}
411
+ </Text>
412
+ )
413
+ ) : (
414
+ <ScrollView style={styles.list}>
415
+ {!visit?.events.length && (
416
+ <Text style={[styles.muted, styles.empty]}>{t('liveEmpty')}</Text>
417
+ )}
418
+ {visit?.events.slice(0, 100).map((item) => (
419
+ <View key={item.key} style={styles.event}>
420
+ <Text
421
+ style={[styles.glyph, { color: topicGlyph[item.topic].color }]}
422
+ >
423
+ {topicGlyph[item.topic].icon}
424
+ </Text>
425
+ <Text
426
+ style={[styles.eventName, item.issue && styles.alertText]}
427
+ numberOfLines={1}
428
+ >
429
+ {item.event.name}
430
+ </Text>
431
+ <Text style={[styles.value, item.issue && styles.alertText]}>
432
+ {'durationMs' in item.event
433
+ ? `${item.event.durationMs.toFixed(0)} ms`
434
+ : 'value' in item.event
435
+ ? `${item.event.value.toFixed(0)} ${item.event.unit}`
436
+ : ''}
437
+ </Text>
438
+ <Text style={styles.time}>
439
+ +
440
+ {(
441
+ (item.event.timestampMs - (visit.sinceMs ?? 0)) /
442
+ 1000
443
+ ).toFixed(1)}
444
+ s
445
+ </Text>
446
+ </View>
447
+ ))}
448
+ </ScrollView>
449
+ )}
450
+ </Animated.View>
451
+ )
452
+ }
453
+
454
+ function Stat({
455
+ label,
456
+ value,
457
+ alert,
458
+ }: {
459
+ label: string
460
+ value: number | string
461
+ alert?: boolean
462
+ }) {
463
+ const hot = alert && Number(value) > 0
464
+ return (
465
+ <View style={styles.stat}>
466
+ <Text style={[styles.statValue, hot && styles.alertText]}>{value}</Text>
467
+ <Text style={styles.statLabel}>{label}</Text>
468
+ </View>
469
+ )
470
+ }
471
+
472
+ const styles = StyleSheet.create({
473
+ bubble: {
474
+ position: 'absolute',
475
+ width: BUBBLE,
476
+ height: BUBBLE,
477
+ borderRadius: BUBBLE / 2,
478
+ overflow: 'hidden',
479
+ borderWidth: StyleSheet.hairlineWidth * 2,
480
+ borderColor: palette.line,
481
+ alignItems: 'center',
482
+ justifyContent: 'center',
483
+ shadowColor: '#000',
484
+ shadowOpacity: 0.15,
485
+ shadowRadius: 10,
486
+ shadowOffset: { width: 0, height: 4 },
487
+ elevation: 6,
488
+ },
489
+ bubbleGlass: { borderRadius: BUBBLE / 2 },
490
+ sheetGlass: { borderTopLeftRadius: 22, borderTopRightRadius: 22 },
491
+ bubbleAlert: { borderColor: palette.error, borderWidth: 2 },
492
+ bubbleValue: {
493
+ color: palette.text,
494
+ fontSize: 17,
495
+ fontWeight: '600',
496
+ fontVariant: ['tabular-nums'],
497
+ },
498
+ bubbleUnit: { color: palette.muted, fontSize: 10, marginTop: -2 },
499
+ sheet: {
500
+ position: 'absolute',
501
+ left: 0,
502
+ right: 0,
503
+ bottom: 0,
504
+ borderTopLeftRadius: 22,
505
+ borderTopRightRadius: 22,
506
+ overflow: 'hidden',
507
+ borderTopWidth: StyleSheet.hairlineWidth,
508
+ borderColor: palette.line,
509
+ paddingHorizontal: 14,
510
+ shadowColor: '#000',
511
+ shadowOpacity: 0.12,
512
+ shadowRadius: 16,
513
+ shadowOffset: { width: 0, height: -4 },
514
+ elevation: 12,
515
+ },
516
+ grabArea: { paddingBottom: 8 },
517
+ grabber: {
518
+ alignSelf: 'center',
519
+ width: 36,
520
+ height: 5,
521
+ borderRadius: 3,
522
+ backgroundColor: palette.line,
523
+ marginTop: 6,
524
+ marginBottom: 8,
525
+ },
526
+ row: { flexDirection: 'row', alignItems: 'center', gap: 6 },
527
+ screen: { flex: 1, color: palette.text, fontSize: 15, fontWeight: '600' },
528
+ muted: { color: palette.muted, fontSize: 13, fontWeight: '400' },
529
+ chip: {
530
+ paddingHorizontal: 10,
531
+ paddingVertical: 5,
532
+ borderRadius: 6,
533
+ backgroundColor: palette.panel,
534
+ },
535
+ chipText: { color: palette.accent, fontSize: 13, fontWeight: '500' },
536
+ stats: {
537
+ flexDirection: 'row',
538
+ marginTop: 10,
539
+ borderTopWidth: StyleSheet.hairlineWidth,
540
+ borderColor: palette.line,
541
+ paddingTop: 8,
542
+ },
543
+ stat: { flex: 1, alignItems: 'center' },
544
+ action: {
545
+ paddingHorizontal: 10,
546
+ paddingVertical: 6,
547
+ borderRadius: 7,
548
+ backgroundColor: palette.panel,
549
+ marginLeft: 6,
550
+ alignSelf: 'center',
551
+ },
552
+ actionText: { color: palette.accent, fontSize: 13, fontWeight: '500' },
553
+ statValue: {
554
+ color: palette.text,
555
+ fontSize: 17,
556
+ fontWeight: '600',
557
+ fontVariant: ['tabular-nums'],
558
+ },
559
+ statLabel: { color: palette.muted, fontSize: 11 },
560
+ list: { flex: 1 },
561
+ empty: { paddingVertical: 12 },
562
+ event: {
563
+ flexDirection: 'row',
564
+ alignItems: 'center',
565
+ gap: 8,
566
+ paddingVertical: 7,
567
+ borderBottomWidth: StyleSheet.hairlineWidth,
568
+ borderColor: palette.line,
569
+ },
570
+ glyph: { width: 18, textAlign: 'center', color: palette.muted },
571
+ eventName: { flex: 1, color: palette.text, fontSize: 13 },
572
+ value: {
573
+ color: palette.text,
574
+ fontSize: 13,
575
+ fontVariant: ['tabular-nums'],
576
+ },
577
+ time: {
578
+ width: 52,
579
+ textAlign: 'right',
580
+ color: palette.muted,
581
+ fontSize: 12,
582
+ fontVariant: ['tabular-nums'],
583
+ },
584
+ alertText: { color: palette.error },
585
+ })