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,171 @@
1
+ import React from 'react'
2
+ import { Alert, StyleSheet, Text, View } from 'react-native'
3
+ import { useStore } from './store'
4
+ import { useMenu } from './native'
5
+ import type { Viewer } from './useTraceViewer'
6
+ import type { InspectorTranslator } from './labels'
7
+ import { apple, Banner, NavBar, palette, TextButton } from './InspectorControls'
8
+
9
+ const titleKey = {
10
+ trace: 'traces',
11
+ span: 'spans',
12
+ metric: 'metrics',
13
+ mark: 'marks',
14
+ } as const
15
+ const tabKey = {
16
+ overview: 'overview',
17
+ timeline: 'timeline',
18
+ explore: 'explore',
19
+ metrics: 'metrics',
20
+ tools: 'tools',
21
+ } as const
22
+
23
+ /** Navigation bar, status line and live-update banner. Only this subscribes to the 1 s telemetry tick. */
24
+ export function InspectorHeader({
25
+ v: viewer,
26
+ t,
27
+ }: {
28
+ v: Viewer
29
+ t: InspectorTranslator
30
+ }) {
31
+ const live = useStore(viewer.telemetry, (s) => s)
32
+ const v = { ...viewer, ...live }
33
+ const client = v.client
34
+ const menu = useMenu()
35
+ const canShare = client.canShare && !!v.stats && !v.busy
36
+ const startNew = () =>
37
+ Alert.alert(t('replaceTitle'), t('replaceDescription'), [
38
+ { text: t('cancel'), style: 'cancel' },
39
+ { text: t('start'), style: 'destructive', onPress: v.start },
40
+ ])
41
+ const seconds = Math.round((v.stats?.nowMs ?? 0) / 1000)
42
+ // Say what is missing, not only how much: lost spans matter more than samples.
43
+ const lost = (
44
+ [
45
+ ['lostSpans', v.stats?.droppedSpans],
46
+ ['lostMarks', v.stats?.droppedMarks],
47
+ ['lostMetrics', v.stats?.droppedMetrics],
48
+ ] as const
49
+ )
50
+ .filter(([, count]) => (count ?? 0) > 0)
51
+ .map(([key, count]) => t(key, { count }))
52
+ const newEvents = v.pending
53
+ ? Math.max(0, v.pending.nextSequence - v.page.nextSequence)
54
+ : 0
55
+ return (
56
+ <View style={styles.root}>
57
+ <NavBar
58
+ left={
59
+ v.detail ? (
60
+ <TextButton label={`‹ ${t('back')}`} onPress={v.back} />
61
+ ) : (
62
+ <TextButton label={t('done')} onPress={client.close} bold />
63
+ )
64
+ }
65
+ title={t(v.detail ? titleKey[v.detail.kind] : tabKey[v.tab])}
66
+ right={
67
+ <>
68
+ <TextButton
69
+ icon="⇪"
70
+ accessibilityLabel={t('share')}
71
+ onPress={() =>
72
+ menu.open({
73
+ title: t('shareTitle'),
74
+ cancelLabel: t('cancel'),
75
+ options: [
76
+ {
77
+ label: t('sharePerfetto'),
78
+ onPress: v.sharePerfetto,
79
+ disabled: !canShare,
80
+ },
81
+ {
82
+ label: t('exportAll'),
83
+ onPress: v.export,
84
+ disabled: !canShare,
85
+ },
86
+ ...(v.profilePath && client.canShareProfile
87
+ ? [{ label: t('shareProfile'), onPress: v.shareProfile }]
88
+ : []),
89
+ ],
90
+ })
91
+ }
92
+ disabled={!canShare && !v.profilePath}
93
+ />
94
+ <TextButton
95
+ icon="⋯"
96
+ accessibilityLabel={t('more')}
97
+ onPress={() =>
98
+ menu.open({
99
+ cancelLabel: t('cancel'),
100
+ options: [
101
+ v.recording
102
+ ? { label: t('stop'), onPress: v.stop, destructive: true }
103
+ : { label: t('start'), onPress: v.start },
104
+ ...(v.recording
105
+ ? [{ label: t('startNew'), onPress: startNew }]
106
+ : []),
107
+ {
108
+ label: t(v.paused ? 'resumeUpdates' : 'pauseUpdates'),
109
+ onPress: v.togglePause,
110
+ },
111
+ { label: t('tools'), onPress: () => v.setTab('tools') },
112
+ ],
113
+ })
114
+ }
115
+ />
116
+ </>
117
+ }
118
+ />
119
+ <Text style={styles.status} numberOfLines={1}>
120
+ <Text style={{ color: v.recording ? palette.error : apple.secondary }}>
121
+ {v.recording ? '●' : '○'}
122
+ </Text>{' '}
123
+ {t(v.recording ? 'recording' : 'stopped')} · {Math.floor(seconds / 60)}:
124
+ {String(seconds % 60).padStart(2, '0')} · {v.stats?.eventCount ?? 0}{' '}
125
+ {t('events')}
126
+ {v.screen ? ` · ${v.screen}` : ''}
127
+ {v.paused ? ` · ${t('frozen')}` : ''}
128
+ </Text>
129
+ {lost.length > 0 && (
130
+ <Text style={styles.warning}>
131
+ {t('lostHistory', { what: lost.join(', ') })}
132
+ </Text>
133
+ )}
134
+ {v.error && (
135
+ <Text selectable style={styles.warning} numberOfLines={3}>
136
+ {v.error}
137
+ </Text>
138
+ )}
139
+ {!v.detail && newEvents > 0 && !v.paused && (
140
+ <Banner
141
+ label={t('newEvents', { count: newEvents })}
142
+ onPress={v.apply}
143
+ />
144
+ )}
145
+ {menu.element}
146
+ </View>
147
+ )
148
+ }
149
+
150
+ const styles = StyleSheet.create({
151
+ root: {
152
+ backgroundColor: apple.grouped,
153
+ borderBottomWidth: StyleSheet.hairlineWidth,
154
+ borderColor: apple.separator,
155
+ paddingBottom: 8,
156
+ },
157
+ status: {
158
+ fontSize: 13,
159
+ color: apple.secondary,
160
+ textAlign: 'center',
161
+ paddingHorizontal: 16,
162
+ fontVariant: ['tabular-nums'],
163
+ },
164
+ warning: {
165
+ fontSize: 13,
166
+ color: palette.error,
167
+ textAlign: 'center',
168
+ paddingHorizontal: 16,
169
+ marginTop: 4,
170
+ },
171
+ })
@@ -0,0 +1,89 @@
1
+ import React, { type ReactNode } from 'react'
2
+ import {
3
+ KeyboardAvoidingView,
4
+ Modal,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ View,
9
+ } from 'react-native'
10
+ import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'
11
+ import { apple, NavBar, palette, TextButton } from './InspectorControls'
12
+ /**
13
+ * iOS page sheet: nav bar with Cancel and an optional primary action, grouped body.
14
+ * Mount only while open; a pre-mounted Modal inside the inspector's Modal presents unreliably.
15
+ */
16
+ export function InspectorSheet({
17
+ title,
18
+ close,
19
+ closeLabel,
20
+ primary,
21
+ children,
22
+ }: {
23
+ title: string
24
+ close: () => void
25
+ closeLabel: string
26
+ primary?: { label: string; onPress: () => void; disabled?: boolean }
27
+ children: ReactNode
28
+ }) {
29
+ return (
30
+ <Modal visible transparent animationType="slide" onRequestClose={close}>
31
+ <SafeAreaProvider>
32
+ <KeyboardAvoidingView
33
+ style={styles.scrim}
34
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
35
+ >
36
+ <Pressable
37
+ accessibilityRole="button"
38
+ accessibilityLabel={closeLabel}
39
+ onPress={close}
40
+ style={StyleSheet.absoluteFill}
41
+ />
42
+ <SafeAreaView
43
+ edges={['bottom', 'left', 'right']}
44
+ style={styles.sheet}
45
+ >
46
+ <View style={styles.grabber} />
47
+ <NavBar
48
+ left={<TextButton label={closeLabel} onPress={close} />}
49
+ title={title}
50
+ right={
51
+ primary && (
52
+ <TextButton
53
+ label={primary.label}
54
+ onPress={primary.onPress}
55
+ disabled={primary.disabled}
56
+ bold
57
+ />
58
+ )
59
+ }
60
+ />
61
+ {children}
62
+ </SafeAreaView>
63
+ </KeyboardAvoidingView>
64
+ </SafeAreaProvider>
65
+ </Modal>
66
+ )
67
+ }
68
+ const styles = StyleSheet.create({
69
+ scrim: {
70
+ flex: 1,
71
+ justifyContent: 'flex-end',
72
+ backgroundColor: palette.scrim,
73
+ },
74
+ sheet: {
75
+ height: '88%',
76
+ backgroundColor: apple.grouped,
77
+ borderTopLeftRadius: 12,
78
+ borderTopRightRadius: 12,
79
+ overflow: 'hidden',
80
+ },
81
+ grabber: {
82
+ alignSelf: 'center',
83
+ width: 36,
84
+ height: 5,
85
+ borderRadius: 3,
86
+ backgroundColor: '#c7c7cc',
87
+ marginTop: 6,
88
+ },
89
+ })
@@ -0,0 +1,182 @@
1
+ import { LegendList } from '@legendapp/list/react-native'
2
+ import React, { useCallback, useMemo } from 'react'
3
+ import { Pressable, StyleSheet, Text, View } from 'react-native'
4
+ import type { Metric, Viewer } from './useTraceViewer'
5
+ import { formatMetric } from './viewerModel'
6
+ import type { InspectorTranslator } from './labels'
7
+ import { useMenu } from './native'
8
+ import {
9
+ apple,
10
+ palette,
11
+ Row,
12
+ SearchField,
13
+ Segmented,
14
+ } from './InspectorControls'
15
+
16
+ const categories = ['all', 'runtime', 'operations', 'custom'] as const
17
+ const sorts = ['name', 'count', 'p95'] as const
18
+ const category = (name: string) =>
19
+ name.startsWith('duration: ')
20
+ ? 'operations'
21
+ : /^(js|ui|process|app)\./.test(name)
22
+ ? 'runtime'
23
+ : 'custom'
24
+
25
+ export function MetricsView({ v, t }: { v: Viewer; t: InspectorTranslator }) {
26
+ const menu = useMenu()
27
+ const metrics = useMemo(
28
+ () =>
29
+ v.metrics
30
+ .filter(
31
+ (series) =>
32
+ (v.metricCategory === 'all' ||
33
+ category(series.name) === v.metricCategory) &&
34
+ series.name.toLowerCase().includes(v.metricQuery.toLowerCase())
35
+ )
36
+ .sort((a, b) =>
37
+ v.metricSort === 'name'
38
+ ? a.name.localeCompare(b.name)
39
+ : v.metricSort === 'count'
40
+ ? b.retainedCount - a.retainedCount ||
41
+ a.name.localeCompare(b.name)
42
+ : b.p95 - a.p95 || a.name.localeCompare(b.name)
43
+ ),
44
+ [v.metrics, v.metricCategory, v.metricQuery, v.metricSort]
45
+ )
46
+ const { open } = v
47
+ const renderItem = useCallback(
48
+ ({ item }: { item: Metric }) => (
49
+ <Row
50
+ title={item.metric}
51
+ subtitle={t('metricBrief', {
52
+ latest: formatMetric(item.latest, item.unit),
53
+ p95: formatMetric(item.p95, item.unit),
54
+ count: item.retainedCount,
55
+ })}
56
+ value={formatMetric(item.latest, item.unit)}
57
+ onPress={() => open({ kind: 'metric', value: item })}
58
+ />
59
+ ),
60
+ [open, t]
61
+ )
62
+ return (
63
+ <View style={styles.root}>
64
+ <SearchField
65
+ value={v.metricQuery}
66
+ onChange={v.setMetricQuery}
67
+ placeholder={t('metricSearch')}
68
+ />
69
+ <Segmented
70
+ options={categories.map((value) => ({
71
+ value,
72
+ label: t(value === 'operations' ? 'operationsShort' : value),
73
+ }))}
74
+ value={v.metricCategory as (typeof categories)[number]}
75
+ onChange={v.setMetricCategory}
76
+ />
77
+ <View style={styles.toolbar}>
78
+ <Pressable
79
+ accessibilityRole="button"
80
+ onPress={() =>
81
+ menu.open({
82
+ title: t('sort'),
83
+ cancelLabel: t('cancel'),
84
+ options: sorts.map((sort) => ({
85
+ label: t(sort),
86
+ selected: sort === v.metricSort,
87
+ onPress: () => v.setMetricSort(sort),
88
+ })),
89
+ })
90
+ }
91
+ hitSlop={8}
92
+ >
93
+ <Text style={styles.toolbarButton}>
94
+ {t('sortBy', {
95
+ sort: t(v.metricSort as (typeof sorts)[number]),
96
+ })}{' '}
97
+ ⌄
98
+ </Text>
99
+ </Pressable>
100
+ <Text style={styles.count}>{metrics.length}</Text>
101
+ </View>
102
+ <LegendList
103
+ recycleItems
104
+ contentInsetAdjustmentBehavior="automatic"
105
+ style={styles.list}
106
+ data={metrics}
107
+ keyExtractor={(series) => series.name}
108
+ estimatedItemSize={56}
109
+ keyboardDismissMode="on-drag"
110
+ initialScrollOffset={v.offsets.current.metrics ?? 0}
111
+ onScroll={(event) => {
112
+ v.offsets.current.metrics = event.nativeEvent.contentOffset.y
113
+ }}
114
+ scrollEventThrottle={100}
115
+ onScrollBeginDrag={() => v.setHolding(true)}
116
+ ListEmptyComponent={
117
+ <Text style={styles.empty}>{t('emptyMetrics')}</Text>
118
+ }
119
+ renderItem={renderItem}
120
+ ListFooterComponent={
121
+ v.metricCategory === 'operations' && v.operations.length ? (
122
+ <View>
123
+ <Text style={styles.header}>{t('operations')}</Text>
124
+ {v.operations
125
+ .filter((op) =>
126
+ op.name.toLowerCase().includes(v.metricQuery.toLowerCase())
127
+ )
128
+ .slice(0, 40)
129
+ .map((op) => (
130
+ <Row
131
+ key={op.name}
132
+ title={op.name}
133
+ subtitle={t('operationStats', {
134
+ ...op,
135
+ errorRate:
136
+ op.errorRate === undefined
137
+ ? '—'
138
+ : `${op.errorRate.toFixed(1)}%`,
139
+ })}
140
+ tint={op.errors ? palette.error : undefined}
141
+ onPress={() => v.showSpans({ search: op.name })}
142
+ />
143
+ ))}
144
+ </View>
145
+ ) : null
146
+ }
147
+ />
148
+ {menu.element}
149
+ </View>
150
+ )
151
+ }
152
+
153
+ const styles = StyleSheet.create({
154
+ root: { flex: 1 },
155
+ toolbar: {
156
+ flexDirection: 'row',
157
+ alignItems: 'center',
158
+ justifyContent: 'space-between',
159
+ paddingHorizontal: 16,
160
+ paddingTop: 10,
161
+ paddingBottom: 6,
162
+ },
163
+ toolbarButton: { fontSize: 15, color: palette.accent, fontWeight: '500' },
164
+ count: { fontSize: 13, color: apple.secondary },
165
+ list: { flex: 1, backgroundColor: apple.card },
166
+ header: {
167
+ fontSize: 13,
168
+ fontWeight: '600',
169
+ color: apple.secondary,
170
+ textTransform: 'uppercase',
171
+ paddingHorizontal: 16,
172
+ paddingTop: 18,
173
+ paddingBottom: 6,
174
+ backgroundColor: apple.grouped,
175
+ },
176
+ empty: {
177
+ fontSize: 15,
178
+ color: apple.secondary,
179
+ textAlign: 'center',
180
+ padding: 32,
181
+ },
182
+ })
@@ -0,0 +1,121 @@
1
+ import React from 'react'
2
+ import { ScrollView } from 'react-native'
3
+ import type { InspectorLabelKey, InspectorTranslator } from './labels'
4
+ import type { Viewer } from './useTraceViewer'
5
+ import {
6
+ defaultBudgets,
7
+ topicGlyph,
8
+ type AnyEvent,
9
+ type Issue,
10
+ type TopicId,
11
+ } from './topics'
12
+ import { formatMetric } from './viewerModel'
13
+ import { apple, palette, Row, Section } from './InspectorControls'
14
+ const capital = (id: string) => id[0].toUpperCase() + id.slice(1)
15
+ const issueLabel = (issue: Issue) =>
16
+ `issue${capital(issue.kind)}` as InspectorLabelKey
17
+ const ms = (value: number | undefined) =>
18
+ value === undefined ? '—' : formatMetric(value, 'ms')
19
+ const MAX_ISSUES = 20
20
+ /** Opens the matching detail; metric samples open their series. */
21
+ export const openEvent = (v: Viewer, event: AnyEvent) => {
22
+ if ('spanId' in event) v.open({ kind: 'span', value: event })
23
+ else if ('unit' in event) {
24
+ const series = v.metrics.find(
25
+ (s) => s.name === `${event.name} (${event.unit})`
26
+ )
27
+ if (series) v.open({ kind: 'metric', value: series })
28
+ } else v.open({ kind: 'mark', value: event })
29
+ }
30
+ export function SummaryView({
31
+ v,
32
+ t,
33
+ budgetMs = defaultBudgets.stallMs,
34
+ }: {
35
+ v: Viewer
36
+ t: InspectorTranslator
37
+ budgetMs?: number
38
+ }) {
39
+ const explore = (id: TopicId) => {
40
+ if (id === 'startup' || id === 'responsiveness' || id === 'resources') {
41
+ v.setTab('metrics')
42
+ v.setMetricCategory('runtime')
43
+ } else if (id === 'errors') v.showMarks({ sources: ['error'] })
44
+ else if (id === 'network')
45
+ v.showSpans({ sources: ['network'], sort: 'longest' })
46
+ else if (id === 'screens') v.showSpans({ sources: ['navigation'] })
47
+ else v.showSpans({})
48
+ }
49
+ const shown = v.issues.slice(0, MAX_ISSUES)
50
+ return (
51
+ <ScrollView
52
+ contentInsetAdjustmentBehavior="automatic"
53
+ contentContainerStyle={{ paddingBottom: 32 }}
54
+ >
55
+ <Section
56
+ header={
57
+ v.issues.length ? t('issues', { count: v.issues.length }) : undefined
58
+ }
59
+ footer={
60
+ v.issues.length > shown.length
61
+ ? t('moreIssues', { count: v.issues.length - shown.length })
62
+ : undefined
63
+ }
64
+ >
65
+ {shown.length ? (
66
+ shown.map((issue, index) => (
67
+ <Row
68
+ key={index}
69
+ icon="⚠"
70
+ iconColor={palette.error}
71
+ title={issue.title}
72
+ subtitle={`${t(issueLabel(issue))} · +${(issue.event.timestampMs / 1000).toFixed(1)} s`}
73
+ value={
74
+ issue.valueMs === undefined
75
+ ? undefined
76
+ : formatMetric(issue.valueMs, 'ms')
77
+ }
78
+ tint={palette.error}
79
+ onPress={() => openEvent(v, issue.event)}
80
+ last={index === shown.length - 1}
81
+ />
82
+ ))
83
+ ) : (
84
+ <Row icon="✓" iconColor="#34c759" title={t('noIssues')} last />
85
+ )}
86
+ </Section>
87
+ <Section header={t('topics')} footer={t('summaryScope')}>
88
+ {v.topics.map((topic, index) => {
89
+ const name = capital(topic.id)
90
+ const hasIssues = topic.issues.length > 0
91
+ return (
92
+ <Row
93
+ key={topic.id}
94
+ icon={topicGlyph[topic.id].icon}
95
+ iconColor={
96
+ topic.tracked ? topicGlyph[topic.id].color : apple.secondary
97
+ }
98
+ title={t(`topic${name}` as InspectorLabelKey)}
99
+ subtitle={
100
+ !topic.tracked
101
+ ? `${t('notTracked')} · ${t(`hint${name}` as InspectorLabelKey)}`
102
+ : !topic.count && topic.id !== 'responsiveness'
103
+ ? t('quiet')
104
+ : t(`headline${name}` as InspectorLabelKey, {
105
+ count: topic.count,
106
+ value: ms(topic.value),
107
+ budget: budgetMs,
108
+ ...topic.values,
109
+ })
110
+ }
111
+ value={hasIssues ? String(topic.issues.length) : undefined}
112
+ tint={hasIssues ? palette.error : undefined}
113
+ onPress={() => explore(topic.id)}
114
+ last={index === v.topics.length - 1}
115
+ />
116
+ )
117
+ })}
118
+ </Section>
119
+ </ScrollView>
120
+ )
121
+ }
@@ -0,0 +1,87 @@
1
+ import { LegendList } from '@legendapp/list/react-native'
2
+ import React, { useCallback } from 'react'
3
+ import { StyleSheet, Text } from 'react-native'
4
+ import type { InspectorTranslator } from './labels'
5
+ import type { Viewer } from './useTraceViewer'
6
+ import { topicGlyph, type TimelineItem } from './topics'
7
+ import { openEvent } from './SummaryView'
8
+ import { formatMetric } from './viewerModel'
9
+ import { apple, palette, Row } from './InspectorControls'
10
+
11
+ const valueOf = (item: Extract<TimelineItem, { kind: 'event' }>) =>
12
+ 'durationMs' in item.event
13
+ ? formatMetric(item.event.durationMs, 'ms')
14
+ : 'value' in item.event
15
+ ? formatMetric(item.event.value, item.event.unit)
16
+ : undefined
17
+ const messageOf = (item: Extract<TimelineItem, { kind: 'event' }>) =>
18
+ item.event.attributes.find((a) => a.key === 'message')?.value
19
+
20
+ /** Newest screen visit first; section headers are the screens the events happened on. */
21
+ export function TimelineView({ v, t }: { v: Viewer; t: InspectorTranslator }) {
22
+ const renderItem = useCallback(
23
+ ({ item }: { item: TimelineItem }) => {
24
+ if (item.kind === 'screen')
25
+ return (
26
+ <Text style={styles.header}>
27
+ {item.name || t('beforeNavigation')} ·{' '}
28
+ {(item.timestampMs / 1000).toFixed(1)} s
29
+ </Text>
30
+ )
31
+ const message = messageOf(item)
32
+ return (
33
+ <Row
34
+ icon={topicGlyph[item.topic].icon}
35
+ iconColor={topicGlyph[item.topic].color}
36
+ title={item.event.name}
37
+ subtitle={`${(item.event.timestampMs / 1000).toFixed(2)} s${message ? ` · ${message}` : ''}`}
38
+ value={valueOf(item)}
39
+ tint={item.issue ? palette.error : undefined}
40
+ onPress={() => openEvent(v, item.event)}
41
+ />
42
+ )
43
+ },
44
+ // openEvent reads v.metrics at press time; rows only need new props when data changes.
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ [t, v.metrics]
47
+ )
48
+ return (
49
+ <LegendList<TimelineItem>
50
+ recycleItems
51
+ contentInsetAdjustmentBehavior="automatic"
52
+ style={styles.list}
53
+ data={v.timeline}
54
+ keyExtractor={(item) => item.key}
55
+ getItemType={(item) => item.kind}
56
+ estimatedItemSize={60}
57
+ initialScrollOffset={v.offsets.current.timeline ?? 0}
58
+ onScroll={(event) => {
59
+ v.offsets.current.timeline = event.nativeEvent.contentOffset.y
60
+ }}
61
+ scrollEventThrottle={100}
62
+ onScrollBeginDrag={() => v.setHolding(true)}
63
+ ListEmptyComponent={<Text style={styles.empty}>{t('empty')}</Text>}
64
+ renderItem={renderItem}
65
+ />
66
+ )
67
+ }
68
+
69
+ const styles = StyleSheet.create({
70
+ list: { flex: 1 },
71
+ header: {
72
+ fontSize: 13,
73
+ fontWeight: '600',
74
+ color: apple.secondary,
75
+ textTransform: 'uppercase',
76
+ paddingHorizontal: 16,
77
+ paddingTop: 18,
78
+ paddingBottom: 6,
79
+ backgroundColor: apple.grouped,
80
+ },
81
+ empty: {
82
+ fontSize: 15,
83
+ color: apple.secondary,
84
+ textAlign: 'center',
85
+ padding: 32,
86
+ },
87
+ })