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,333 @@
1
+ import { createStore } from './store'
2
+ import { readSnapshot } from './readSnapshot'
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useDeferredValue,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ useSyncExternalStore,
11
+ } from 'react'
12
+ import type { TraceClient } from '../client/createTraceClient'
13
+ import type { RecordingStats, TracePage, SpanEvent, MarkEvent } from '../types'
14
+ import {
15
+ buildTraces,
16
+ defaultQuery,
17
+ queryEvents,
18
+ queryTraces,
19
+ traceForSpan,
20
+ type ExplorerMode,
21
+ type Query,
22
+ type Trace,
23
+ } from './explorerModel'
24
+ import { recordingMetrics, operationMetrics } from './viewerModel'
25
+ import {
26
+ buildTimeline,
27
+ currentScreen,
28
+ defaultBudgets,
29
+ summarizeTopics,
30
+ type Budgets,
31
+ } from './topics'
32
+ export type Metric = ReturnType<typeof recordingMetrics>[number]
33
+ export type Detail =
34
+ | { kind: 'trace'; value: Trace }
35
+ | { kind: 'span'; value: SpanEvent }
36
+ | { kind: 'mark'; value: MarkEvent }
37
+ | { kind: 'metric'; value: Metric }
38
+ export type Tab = 'overview' | 'timeline' | 'explore' | 'metrics' | 'tools'
39
+ export interface Telemetry {
40
+ stats?: RecordingStats
41
+ error?: string
42
+ pending?: Pick<TracePage, 'nextSequence' | 'earliestSequence'>
43
+ }
44
+ const empty = (): TracePage => ({
45
+ spans: [],
46
+ marks: [],
47
+ metrics: [],
48
+ nextSequence: 0,
49
+ earliestSequence: 0,
50
+ droppedEvents: 0,
51
+ })
52
+ export const useTraceViewer = (
53
+ client: TraceClient,
54
+ budgets?: Partial<Budgets>
55
+ ) => {
56
+ const snapshot = useSyncExternalStore(
57
+ client.subscribe,
58
+ client.getSnapshot,
59
+ client.getSnapshot
60
+ )
61
+ const [tab, setTab] = useState<Tab>('overview')
62
+ const [mode, setMode] = useState<ExplorerMode>('traces')
63
+ const [page, setPage] = useState(empty)
64
+ const [telemetry] = useState(() => createStore<Telemetry>({}))
65
+ const setStats = (stats: RecordingStats) => telemetry.set({ stats })
66
+ const setError = (error: string | undefined) => telemetry.set({ error })
67
+ const pendingPage = useRef<TracePage | undefined>(undefined)
68
+ const setPending = (value: TracePage | undefined) => {
69
+ pendingPage.current = value
70
+ telemetry.set({
71
+ pending: value
72
+ ? {
73
+ nextSequence: value.nextSequence,
74
+ earliestSequence: value.earliestSequence,
75
+ }
76
+ : undefined,
77
+ })
78
+ }
79
+ const [queries, setQueries] = useState<Record<ExplorerMode, Query>>({
80
+ traces: defaultQuery(),
81
+ spans: defaultQuery(),
82
+ marks: defaultQuery(),
83
+ })
84
+ const [details, setDetails] = useState<Detail[]>([])
85
+ const [paused, setPaused] = useState(false)
86
+ const [holding, setHolding] = useState(false)
87
+ const [metricQuery, setMetricQuery] = useState('')
88
+ const [metricCategory, setMetricCategory] = useState('all')
89
+ const [metricSort, setMetricSort] = useState('name')
90
+ const offsets = useRef<Record<string, number>>({})
91
+ const detailState = useRef<
92
+ Record<
93
+ string,
94
+ {
95
+ collapsed: string[]
96
+ layout: 'waterfall' | 'list'
97
+ attributes: string
98
+ offset: number
99
+ }
100
+ >
101
+ >({})
102
+ const latestPage = useRef(page)
103
+ const session = useRef<string | undefined>(undefined)
104
+ const controls = useRef({ paused, holding, detail: details.length > 0 })
105
+ controls.current = { paused, holding, detail: details.length > 0 }
106
+ latestPage.current = page
107
+ useEffect(() => {
108
+ if (!snapshot.visible) return
109
+ let cached: TracePage | undefined
110
+ let cachedRecording: ReturnType<TraceClient['getRecording']>
111
+ let cancelled = false
112
+ let timer: ReturnType<typeof setTimeout>
113
+ const refresh = async () => {
114
+ try {
115
+ const recording = client.getRecording()
116
+ if (recording) {
117
+ if (cachedRecording !== recording) {
118
+ cached = undefined
119
+ cachedRecording = recording
120
+ }
121
+ const next = await readSnapshot(recording, cached)
122
+ cached = next
123
+ if (cancelled || recording !== client.getRecording()) return
124
+ const current = recording.getStats()
125
+ setStats(current)
126
+ if (session.current !== current.sessionId) {
127
+ session.current = current.sessionId
128
+ setDetails([])
129
+ setPending(undefined)
130
+ setPage(next)
131
+ setHolding(false)
132
+ offsets.current = {}
133
+ detailState.current = {}
134
+ setQueries(
135
+ (previous) =>
136
+ Object.fromEntries(
137
+ Object.entries(previous).map(([key, q]) => [
138
+ key,
139
+ { ...q, from: '', to: '' },
140
+ ])
141
+ ) as Record<ExplorerMode, Query>
142
+ )
143
+ } else if (
144
+ controls.current.paused ||
145
+ controls.current.holding ||
146
+ controls.current.detail
147
+ ) {
148
+ if (
149
+ next.nextSequence !== latestPage.current.nextSequence ||
150
+ next.droppedEvents !== latestPage.current.droppedEvents
151
+ )
152
+ setPending(next)
153
+ } else {
154
+ setPage(next)
155
+ setPending(undefined)
156
+ }
157
+ }
158
+ if (!cancelled) setError(client.getError())
159
+ } catch (failure) {
160
+ if (!cancelled) setError(String(failure))
161
+ } finally {
162
+ if (!cancelled) timer = setTimeout(refresh, 1000)
163
+ }
164
+ }
165
+ void refresh()
166
+ return () => {
167
+ cancelled = true
168
+ clearTimeout(timer)
169
+ }
170
+ }, [client, snapshot.visible])
171
+ const traces = useMemo(() => buildTraces(page), [page.spans, page.marks])
172
+ const metrics = useMemo(
173
+ () => (tab === 'explore' ? [] : recordingMetrics(page)),
174
+ [page, tab]
175
+ )
176
+ const collectors = snapshot.collectors
177
+ const { appReadyMs, screenMs, requestMs, stallMs } = {
178
+ ...defaultBudgets,
179
+ ...budgets,
180
+ }
181
+ const summary = useMemo(
182
+ () =>
183
+ summarizeTopics(page, collectors ?? [], {
184
+ appReadyMs,
185
+ screenMs,
186
+ requestMs,
187
+ stallMs,
188
+ }),
189
+ [page, collectors, appReadyMs, screenMs, requestMs, stallMs]
190
+ )
191
+ const timeline = useMemo(
192
+ () => (tab === 'timeline' ? buildTimeline(page, summary.issues) : []),
193
+ [page, summary.issues, tab]
194
+ )
195
+ const screen = useMemo(() => currentScreen(page), [page.marks])
196
+ const operations = useMemo(
197
+ () => (tab === 'metrics' ? operationMetrics(page.spans) : []),
198
+ [page.spans, tab]
199
+ )
200
+ const query = queries[mode]
201
+ const deferredQuery = useDeferredValue(query)
202
+ const results = useMemo(
203
+ () =>
204
+ mode === 'traces'
205
+ ? queryTraces(traces, deferredQuery)
206
+ : queryEvents(
207
+ mode === 'spans' ? page.spans : page.marks,
208
+ deferredQuery
209
+ ),
210
+ [mode, traces, page.spans, page.marks, deferredQuery]
211
+ )
212
+ const updateQuery = (patch: Partial<Query>) => {
213
+ setHolding(true)
214
+ setQueries((previous) => ({
215
+ ...previous,
216
+ [mode]: { ...previous[mode], ...patch },
217
+ }))
218
+ offsets.current[mode] = 0
219
+ }
220
+ const apply = () => {
221
+ const pending = pendingPage.current
222
+ if (pending) setPage(pending)
223
+ setPending(undefined)
224
+ setHolding(false)
225
+ }
226
+ const act = (work: () => void | Promise<void>) => {
227
+ void Promise.resolve()
228
+ .then(work)
229
+ .catch((failure) => {
230
+ setError(String(failure))
231
+ client.reportError(failure)
232
+ })
233
+ }
234
+ const detail = details[details.length - 1]
235
+ // Stable identity lets memoized list rows skip re-rendering on live ticks.
236
+ const open = useCallback((next: Detail) => {
237
+ setHolding(true)
238
+ setDetails((previous) => [...previous.slice(-31), next])
239
+ }, [])
240
+ const showEvents = (mode: 'spans' | 'marks', patch: Partial<Query>) => {
241
+ setTab('explore')
242
+ setMode(mode)
243
+ setDetails([])
244
+ setQueries((previous) => ({
245
+ ...previous,
246
+ [mode]: { ...defaultQuery(), ...patch },
247
+ }))
248
+ offsets.current[mode] = 0
249
+ setHolding(true)
250
+ }
251
+ return {
252
+ ...snapshot,
253
+ client,
254
+ tab,
255
+ setTab: (next: Tab) => {
256
+ setDetails([])
257
+ setTab(next)
258
+ },
259
+ mode,
260
+ setMode,
261
+ page,
262
+ telemetry,
263
+ traces,
264
+ topics: summary.topics,
265
+ issues: summary.issues,
266
+ timeline,
267
+ screen,
268
+ results,
269
+ query,
270
+ updateQuery,
271
+ resetQuery: () => {
272
+ setQueries((previous) => ({ ...previous, [mode]: defaultQuery() }))
273
+ offsets.current[mode] = 0
274
+ },
275
+ loaded:
276
+ mode === 'traces'
277
+ ? traces.length
278
+ : mode === 'spans'
279
+ ? page.spans.length
280
+ : page.marks.length,
281
+ uncorrelated: useMemo(
282
+ () =>
283
+ page.spans.filter((s) => !s.correlationId).length +
284
+ page.marks.filter((s) => !s.correlationId).length,
285
+ [page.spans, page.marks]
286
+ ),
287
+ detail,
288
+ detailState,
289
+ details,
290
+ open,
291
+ back: () => setDetails((previous) => previous.slice(0, -1)),
292
+ traceFor: (span: SpanEvent) => traceForSpan(traces, span),
293
+ showSpans: (patch: Partial<Query>) => showEvents('spans', patch),
294
+ showMarks: (patch: Partial<Query>) => showEvents('marks', patch),
295
+ paused,
296
+ holding,
297
+ setHolding,
298
+ apply,
299
+ togglePause: () => {
300
+ if (paused) apply()
301
+ setPaused(!paused)
302
+ },
303
+ offsets,
304
+ metrics,
305
+ operations,
306
+ metricQuery,
307
+ setMetricQuery,
308
+ metricCategory,
309
+ setMetricCategory,
310
+ metricSort,
311
+ setMetricSort,
312
+ start: () => act(client.start),
313
+ stop: () => act(client.stop),
314
+ export: () => act(client.export),
315
+ sharePerfetto: () => act(() => client.shareTrace('perfetto')),
316
+ toggleProfile: () => act(client.toggleProfile),
317
+ shareProfile: () => act(client.shareProfile),
318
+ playground: () => act(client.playground),
319
+ playgroundResult: snapshot.playground,
320
+ openPlayground: () => {
321
+ setTab('explore')
322
+ setMode('traces')
323
+ setQueries((previous) => ({
324
+ ...previous,
325
+ traces: {
326
+ ...defaultQuery(),
327
+ correlation: snapshot.playground?.correlationId ?? '',
328
+ },
329
+ }))
330
+ },
331
+ }
332
+ }
333
+ export type Viewer = ReturnType<typeof useTraceViewer>
@@ -0,0 +1,172 @@
1
+ import type { MetricEvent, SpanEvent, TracePage } from '../types'
2
+ export interface TraceGroup {
3
+ id: string
4
+ name: string
5
+ start: number
6
+ duration: number
7
+ errors: number
8
+ events: number
9
+ }
10
+ export const groupTraces = (page: TracePage, query: string): TraceGroup[] => {
11
+ const groups = new Map<string, TraceGroup>()
12
+ for (const event of [...page.spans, ...page.marks, ...page.metrics]) {
13
+ const id = event.correlationId || 'uncorrelated'
14
+ const end =
15
+ event.timestampMs +
16
+ ('durationMs' in event && typeof event.durationMs === 'number'
17
+ ? event.durationMs
18
+ : 0)
19
+ const previous = groups.get(id)
20
+ const start = Math.min(
21
+ previous?.start ?? event.timestampMs,
22
+ event.timestampMs
23
+ )
24
+ groups.set(id, {
25
+ id,
26
+ name:
27
+ previous && previous.start <= event.timestampMs
28
+ ? previous.name
29
+ : event.name,
30
+ start,
31
+ duration:
32
+ Math.max(previous ? previous.start + previous.duration : end, end) -
33
+ start,
34
+ errors:
35
+ (previous?.errors ?? 0) +
36
+ ('outcome' in event && event.outcome === 'error' ? 1 : 0),
37
+ events: (previous?.events ?? 0) + 1,
38
+ })
39
+ }
40
+ const search = query.toLowerCase()
41
+ const matchingIds = new Set(
42
+ [...page.spans, ...page.marks, ...page.metrics]
43
+ .filter((event) =>
44
+ `${event.name} ${event.correlationId}`.toLowerCase().includes(search)
45
+ )
46
+ .map((event) => event.correlationId || 'uncorrelated')
47
+ )
48
+ return [...groups.values()]
49
+ .filter((group) => !search || matchingIds.has(group.id))
50
+ .sort((a, b) => b.start - a.start)
51
+ }
52
+ export const waterfall = (spans: SpanEvent[]) => {
53
+ const byId = new Map(spans.map((span) => [span.spanId, span]))
54
+ const base = spans.length
55
+ ? Math.min(...spans.map((span) => span.timestampMs))
56
+ : 0
57
+ const duration = Math.max(
58
+ 1,
59
+ ...spans.map((span) => span.timestampMs + span.durationMs - base)
60
+ )
61
+ return [...spans]
62
+ .sort(
63
+ (a, b) => a.timestampMs - b.timestampMs || b.durationMs - a.durationMs
64
+ )
65
+ .map((span) => {
66
+ const seen = new Set([span.spanId])
67
+ let parent = byId.get(span.parentSpanId)
68
+ let depth = 0
69
+ while (parent && depth < 8 && !seen.has(parent.spanId)) {
70
+ seen.add(parent.spanId)
71
+ depth++
72
+ parent = byId.get(parent.parentSpanId)
73
+ }
74
+ return {
75
+ span,
76
+ depth: Math.min(depth, 8),
77
+ offset: (span.timestampMs - base) / duration,
78
+ width: span.durationMs / duration,
79
+ }
80
+ })
81
+ }
82
+ const num = (value: number) =>
83
+ Math.abs(value) >= 100 ? value.toFixed(0) : value.toFixed(1)
84
+ /** Human value with unit: 3653 ms → "3.65 s", 1240 MB → "1.21 GB". */
85
+ export const formatMetric = (value: number, unit: string) => {
86
+ if (unit === 'ms' && Math.abs(value) >= 1000)
87
+ return `${(value / 1000).toFixed(2)} s`
88
+ if (unit === 'MB' && Math.abs(value) >= 1024)
89
+ return `${(value / 1024).toFixed(2)} GB`
90
+ if (unit === 'count') return value.toFixed(0)
91
+ if (unit === '%') return `${num(value)}%`
92
+ if (unit.endsWith('/s')) return `${num(value)}/s`
93
+ return `${num(value)} ${unit}`
94
+ }
95
+
96
+ export const metricSeries = (metrics: MetricEvent[]) => {
97
+ const groups = new Map<string, MetricEvent[]>()
98
+ for (const metric of metrics) {
99
+ const key = `${metric.name} (${metric.unit})`
100
+ const group = groups.get(key) ?? []
101
+ group.push(metric)
102
+ groups.set(key, group)
103
+ }
104
+ return [...groups].map(([name, retained]) => {
105
+ const samples = retained
106
+ .sort((a, b) => a.timestampMs - b.timestampMs)
107
+ .slice(-40)
108
+ const values = retained.map((sample) => sample.value).sort((a, b) => a - b)
109
+ return {
110
+ latest: retained[retained.length - 1]?.value ?? 0,
111
+ median: values.length
112
+ ? (values[Math.floor((values.length - 1) / 2)] +
113
+ values[Math.ceil((values.length - 1) / 2)]) /
114
+ 2
115
+ : 0,
116
+ min: values[0] ?? 0,
117
+ peak: values[values.length - 1] ?? 0,
118
+ p95: values[Math.max(0, Math.ceil(values.length * 0.95) - 1)] ?? 0,
119
+ name,
120
+ /** Metric name without the unit suffix used for grouping. */
121
+ metric: retained[0].name,
122
+ unit: retained[0].unit,
123
+ samples,
124
+ retainedCount: retained.length,
125
+ startMs: samples[0]?.timestampMs ?? 0,
126
+ endMs: samples[samples.length - 1]?.timestampMs ?? 0,
127
+ max: Math.max(1, ...samples.map((sample) => Math.abs(sample.value))),
128
+ }
129
+ })
130
+ }
131
+
132
+ /** Durations are derived from retained completed spans, not additional native samples. */
133
+ export const recordingMetrics = (page: TracePage) =>
134
+ metricSeries([
135
+ ...page.metrics,
136
+ ...page.spans
137
+ .filter((span) => span.outcome === 'success' || span.outcome === 'error')
138
+ .map((span) => ({
139
+ ...span,
140
+ name: `duration: ${span.name}`,
141
+ value: span.durationMs,
142
+ unit: 'ms',
143
+ })),
144
+ ])
145
+
146
+ export const operationMetrics = (spans: SpanEvent[]) => {
147
+ const groups = new Map<string, SpanEvent[]>()
148
+ for (const span of spans) {
149
+ const group = groups.get(span.name) ?? []
150
+ group.push(span)
151
+ groups.set(span.name, group)
152
+ }
153
+ return [...groups]
154
+ .map(([name, samples]) => {
155
+ const completed = samples.filter(
156
+ (s) => s.outcome === 'success' || s.outcome === 'error'
157
+ )
158
+ return {
159
+ name,
160
+ count: samples.length,
161
+ errors: completed.filter((s) => s.outcome === 'error').length,
162
+ errorRate: completed.length
163
+ ? (completed.filter((s) => s.outcome === 'error').length /
164
+ completed.length) *
165
+ 100
166
+ : undefined,
167
+ cancelled: samples.filter((s) => s.outcome === 'cancelled').length,
168
+ interrupted: samples.filter((s) => s.outcome === 'interrupted').length,
169
+ }
170
+ })
171
+ .sort((a, b) => b.count - a.count)
172
+ }
@@ -0,0 +1,40 @@
1
+ import type { HybridObject } from 'react-native-nitro-modules'
2
+ import type { TraceSpan } from './TraceSpan.nitro'
3
+ import type {
4
+ CompletedSpanOptions,
5
+ SpanOptions,
6
+ MarkOptions,
7
+ MetricOptions,
8
+ ReadOptions,
9
+ TracePage,
10
+ RecordingStats,
11
+ NativeSamplingOptions,
12
+ } from '../types'
13
+ /** Owns bounded native history. Stop before export; dispose when all readers are done. */
14
+ export interface Recording extends HybridObject<{
15
+ ios: 'c++'
16
+ android: 'c++'
17
+ }> {
18
+ /** Start a span, optionally linked to an explicit parent. */
19
+ startSpan(options: SpanOptions): TraceSpan
20
+ /** Import a completed measurement; returns its native span identity. */
21
+ recordSpan(options: CompletedSpanOptions): string
22
+ /** Append a point event. */
23
+ mark(options: MarkOptions): void
24
+ /** Append a finite numeric sample with units. */
25
+ recordMetric(options: MetricOptions): void
26
+ /** Read at most 1000 completed events without per-event callbacks. */
27
+ readEvents(options: ReadOptions): Promise<TracePage>
28
+ /** Read cheap recording counters and clock anchor. */
29
+ getStats(): RecordingStats
30
+ /** Freeze history and close active spans as interrupted. Idempotent. */
31
+ stop(): void
32
+ /** Serialize a snapshot off the JS thread; includes schema version and clock anchor. */
33
+ exportJson(): Promise<string>
34
+ /** Chrome Trace Event JSON for ui.perfetto.dev; serialized off the JS thread. */
35
+ exportTraceEvents(): Promise<string>
36
+ /** Start a native thread writing process.cpu, process.memory and ui.* frame metrics. Replaces a running sampler. */
37
+ startNativeSampling(options: NativeSamplingOptions): void
38
+ /** Stop native sampling. Idempotent; stop() and dispose() also stop it. */
39
+ stopNativeSampling(): void
40
+ }
@@ -0,0 +1,14 @@
1
+ import type { HybridObject } from 'react-native-nitro-modules'
2
+ import type { SpanOutcome } from '../types'
3
+ /** A native span handle returned by Recording.startSpan. */
4
+ export interface TraceSpan extends HybridObject<{
5
+ ios: 'c++'
6
+ android: 'c++'
7
+ }> {
8
+ /** Empty when rejected by capacity limits. */
9
+ readonly spanId: string
10
+ /** Whether capacity allowed this span to be recorded. */
11
+ readonly recorded: boolean
12
+ /** Complete once. Repeated completion and completion after stop are no-ops. */
13
+ end(outcome: SpanOutcome): void
14
+ }
@@ -0,0 +1,14 @@
1
+ import type { HybridObject } from 'react-native-nitro-modules'
2
+ import type { Recording } from './Recording.nitro'
3
+ import type { RecordingOptions } from '../types'
4
+ /** Factory for independent native recordings. Exported as Tracing. */
5
+ export interface Tracing extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
6
+ /** Create an active recording with explicit validated retention limits. */
7
+ startRecording(options: RecordingOptions): Recording
8
+ /**
9
+ * Stop Hermes' process-wide sampling profiler. The release-profiler plugin calls this
10
+ * after saving a profile: on React Native 0.85 Android the profiler's own stop restarts
11
+ * sampling. Returns false where Hermes is not linked (iOS stops correctly itself).
12
+ */
13
+ disableHermesSampling(): boolean
14
+ }
@@ -0,0 +1,15 @@
1
+ import type { SpanOutcome } from './SpanOutcome'
2
+ import type { SpanOptions } from './SpanOptions'
3
+ /** Completed external measurement in Recording's native clock domain. */
4
+ export interface CompletedSpanOptions extends SpanOptions {
5
+ /** Optional source-qualified identity, for example sentry:traceId:spanId. */
6
+ sourceSpanId?: string
7
+ /** Source-qualified parent identity; resolves even when parent ends later. */
8
+ sourceParentSpanId?: string
9
+ /** Start in milliseconds relative to recording creation. */
10
+ timestampMs: number
11
+ /** Finite nonnegative duration. */
12
+ durationMs: number
13
+ /** Terminal result from the measurement source. */
14
+ outcome: SpanOutcome
15
+ }
@@ -0,0 +1,8 @@
1
+ import type { TraceContext } from './TraceContext'
2
+ /** Immutable native event identity and timing shared by recorded variants. */
3
+ export interface EventContext extends TraceContext {
4
+ /** Increasing session-local completion sequence. */
5
+ sequence: number
6
+ /** Milliseconds since recording creation on its native monotonic clock. */
7
+ timestampMs: number
8
+ }
@@ -0,0 +1,3 @@
1
+ import type { EventContext } from './EventContext'
2
+ /** Point-in-time event from Recording.readEvents. */
3
+ export interface MarkEvent extends EventContext {}
@@ -0,0 +1,6 @@
1
+ import type { TraceContext } from './TraceContext'
2
+ /** Native mark, optionally imported with a translated source timestamp. */
3
+ export interface MarkOptions extends TraceContext {
4
+ /** Milliseconds in the recording clock; omit to record now. */
5
+ timestampMs?: number
6
+ }
@@ -0,0 +1,8 @@
1
+ import type { EventContext } from './EventContext'
2
+ /** Numeric sample from Recording.readEvents. */
3
+ export interface MetricEvent extends EventContext {
4
+ /** Finite numeric sample. */
5
+ value: number
6
+ /** Sample unit. */
7
+ unit: string
8
+ }
@@ -0,0 +1,10 @@
1
+ import type { TraceContext } from './TraceContext'
2
+ /** Input to Recording.recordMetric. */
3
+ export interface MetricOptions extends TraceContext {
4
+ /** Optional source timestamp translated to the recording clock. */
5
+ timestampMs?: number
6
+ /** Finite numeric sample. */
7
+ value: number
8
+ /** Unit, for example bytes, byte/second, count, millisecond. */
9
+ unit: string
10
+ }
@@ -0,0 +1,7 @@
1
+ /** Native sampler settings for Recording.startNativeSampling. */
2
+ export interface NativeSamplingOptions {
3
+ /** Window length for CPU, memory and frame metrics; 250–60000 ms. */
4
+ intervalMs: number
5
+ /** Observe main-thread display callbacks (CADisplayLink / Choreographer). */
6
+ frames: boolean
7
+ }
@@ -0,0 +1,7 @@
1
+ /** Bounded page query for Recording.readEvents; sequence zero starts at history. */
2
+ export interface ReadOptions {
3
+ /** Exclusive completion sequence cursor. */
4
+ afterSequence: number
5
+ /** Maximum total events returned across all arrays; 1–1000. */
6
+ limit: number
7
+ }
@@ -0,0 +1,11 @@
1
+ /** Native retention limits for Tracing.startRecording. All fields are required. */
2
+ export interface RecordingOptions {
3
+ /** Maximum completed events retained; 1–100000. */
4
+ maxEvents: number
5
+ /** Retained payload budget including active spans; 1024–67108864 bytes. */
6
+ maxBytes: number
7
+ /** Maximum simultaneously active spans; 1–10000. */
8
+ maxActiveSpans: number
9
+ /** Expire abandoned spans on the next recorder access; milliseconds. */
10
+ spanTimeoutMs: number
11
+ }
@@ -0,0 +1,25 @@
1
+ /** Snapshot of a recording, returned by Recording.getStats. */
2
+ export interface RecordingStats {
3
+ /** Recording identity. */
4
+ sessionId: string
5
+ /** Wall-clock anchor in Unix milliseconds, for correlation/export only. */
6
+ startedAtUnixMs: number
7
+ /** Native elapsed milliseconds at snapshot time. */
8
+ nowMs: number
9
+ /** Whether writes are accepted. */
10
+ recording: boolean
11
+ /** Retained completed events. */
12
+ eventCount: number
13
+ /** Accounted native payload bytes, including active spans. */
14
+ retainedBytes: number
15
+ /** Evicted or rejected event/span count. */
16
+ droppedEvents: number
17
+ /** Open spans. */
18
+ activeSpans: number
19
+ /** Evicted or rejected spans (part of droppedEvents). */
20
+ droppedSpans: number
21
+ /** Evicted or rejected marks (part of droppedEvents). */
22
+ droppedMarks: number
23
+ /** Evicted or rejected metric samples; rolled-over samples are not counted. */
24
+ droppedMetrics: number
25
+ }