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,439 @@
1
+ import {
2
+ createRuntimeMetricsPlugin,
3
+ type RuntimeMetricsOptions,
4
+ } from '../plugins/runtimeMetrics'
5
+ import {
6
+ formatTracingEntry,
7
+ type TracingEntry,
8
+ type TracingSink,
9
+ } from './TracingSink'
10
+ import type { Recording } from '../specs/Recording.nitro'
11
+ import type { RecordingOptions } from '../types'
12
+ import type { PerformancePluginOptions } from '../plugins/performance'
13
+ import { createPerformancePlugin } from '../plugins/performance'
14
+ import { startPlugins } from '../plugins/startPlugins'
15
+ import type { PluginHandle, TracePlugin } from '../plugins/TracePlugin'
16
+ import type { ReleaseProfilerPlugin } from '../plugins/releaseProfiler'
17
+ import { recordPlayground, type PlaygroundReport } from './playground'
18
+
19
+ /** `recording`: lossless native schema. `perfetto`: Chrome Trace Events for ui.perfetto.dev. */
20
+ export type TraceFormat = 'recording' | 'perfetto'
21
+ export type ClientAttributes = Record<
22
+ string,
23
+ string | number | boolean | undefined
24
+ >
25
+ export interface TraceClientOptions {
26
+ /** Optional app log/breadcrumb output; failures never affect measured work. */
27
+ sink?: TracingSink
28
+ plugins?: TracePlugin[]
29
+ performance?: false | PerformancePluginOptions
30
+ /** Opt in to foreground JS event-loop and frame-callback metrics. */
31
+ runtimeMetrics?: boolean | RuntimeMetricsOptions
32
+ onError?: (error: unknown) => void
33
+ recordingOptions?: Partial<RecordingOptions>
34
+ /** Serialize and deliver a recording; `format` picks raw recording JSON or Perfetto trace events. */
35
+ share?: (recording: Recording, format: TraceFormat) => Promise<void>
36
+ shareProfile?: (path: string) => Promise<void>
37
+ /** Start Hermes sampling with each recording; stopped and saved with it. */
38
+ autoProfile?: boolean
39
+ /**
40
+ * CPU profile window captured by `flag()` when a profiler is attached and idle.
41
+ * Default 10000 ms; 0 records only the flag mark.
42
+ */
43
+ flagProfileMs?: number
44
+ profiler?: ReleaseProfilerPlugin
45
+ }
46
+ export interface TraceClientSnapshot {
47
+ visible: boolean
48
+ recording: boolean
49
+ busy: boolean
50
+ disposed: boolean
51
+ error?: string
52
+ profiling: boolean
53
+ profilePath?: string
54
+ playground?: PlaygroundReport
55
+ /** Plugin IDs started for the current recording, so the UI can tell untracked from quiet. */
56
+ collectors?: string[]
57
+ }
58
+ /** react-native-performance when installed (existing marks live in its buffer), else RN's global Web Performance API. */
59
+ const defaultPerformanceApi = (): PerformancePluginOptions | undefined => {
60
+ try {
61
+ // Directly inside `try` so Metro treats the optional peer as optional.
62
+ const module =
63
+ require('react-native-performance') as typeof import('react-native-performance')
64
+ return {
65
+ performance: module.default,
66
+ PerformanceObserver: module.PerformanceObserver,
67
+ }
68
+ } catch {
69
+ const global = globalThis as unknown as Partial<PerformancePluginOptions>
70
+ return global.performance && global.PerformanceObserver
71
+ ? (global as PerformancePluginOptions)
72
+ : undefined
73
+ }
74
+ }
75
+ /** Each client exclusively owns its recording and producers. Consumers must dispose it. */
76
+ export function createTraceClient(options: TraceClientOptions = {}) {
77
+ let recording: Recording | undefined
78
+ let handle: PluginHandle | undefined
79
+ const readinessStartMs = globalThis.performance.now()
80
+ let readyReported = false
81
+ let enabled = false
82
+ let disposed = false
83
+ let pending = 0
84
+ let tail = Promise.resolve()
85
+ let snapshot: TraceClientSnapshot = {
86
+ visible: false,
87
+ recording: false,
88
+ busy: false,
89
+ disposed: false,
90
+ profiling: false,
91
+ }
92
+ const listeners = new Set<() => void>()
93
+ const marks = new Map<
94
+ string,
95
+ { marks: Map<string, number>; emitted: Set<string> }
96
+ >()
97
+ const publish = (next: Partial<TraceClientSnapshot> = {}) => {
98
+ snapshot = {
99
+ ...snapshot,
100
+ recording: enabled,
101
+ disposed,
102
+ busy: pending > 0,
103
+ profiling: options.profiler?.isProfiling() ?? false,
104
+ profilePath: options.profiler?.getProfilePath(),
105
+ ...next,
106
+ }
107
+ for (const listener of listeners) {
108
+ try {
109
+ listener()
110
+ } catch {
111
+ /* Observers cannot interrupt recording ownership. */
112
+ }
113
+ }
114
+ }
115
+ const reportError = (error: unknown) => {
116
+ publish({ error: String(error) })
117
+ try {
118
+ options.onError?.(error)
119
+ } catch {
120
+ /* Diagnostics cannot change measured operations. */
121
+ }
122
+ }
123
+ const safely = <T>(work: () => T): T | undefined => {
124
+ try {
125
+ return work()
126
+ } catch (error) {
127
+ reportError(error)
128
+ return undefined
129
+ }
130
+ }
131
+ const emit = (entry: TracingEntry) => {
132
+ if (options.sink)
133
+ safely(() => options.sink!(formatTracingEntry(entry), entry))
134
+ }
135
+ const enqueue = (work: () => Promise<void>) => {
136
+ if (disposed) return Promise.reject(new Error('Trace client is disposed'))
137
+ pending++
138
+ publish()
139
+ const result = tail.then(async () => {
140
+ try {
141
+ if (disposed) throw new Error('Trace client is disposed')
142
+ await work()
143
+ } finally {
144
+ pending--
145
+ publish()
146
+ }
147
+ })
148
+ tail = result.catch(reportError)
149
+ return result
150
+ }
151
+ const stopCurrent = async () => {
152
+ enabled = false
153
+ try {
154
+ await handle?.stop()
155
+ } finally {
156
+ handle = undefined
157
+ recording?.stop()
158
+ marks.clear()
159
+ publish()
160
+ }
161
+ }
162
+ const startCurrent = async () => {
163
+ await stopCurrent()
164
+ if (disposed) throw new Error('Trace client is disposed')
165
+ recording?.dispose()
166
+ recording = undefined
167
+ const { Tracing } = require('../index') as typeof import('../index')
168
+ recording = Tracing.startRecording({
169
+ // ~1-2 hours of spans for UAT sessions; metric samples use at most half.
170
+ maxEvents: 10000,
171
+ maxBytes: 6 * 1024 * 1024,
172
+ maxActiveSpans: 256,
173
+ spanTimeoutMs: 30 * 60 * 1000,
174
+ ...options.recordingOptions,
175
+ })
176
+ publish({ error: undefined, playground: undefined })
177
+ try {
178
+ const plugins = [...(options.plugins ?? [])]
179
+ const api =
180
+ options.performance === false
181
+ ? undefined
182
+ : (options.performance ?? defaultPerformanceApi())
183
+ if (api)
184
+ plugins.unshift(
185
+ createPerformancePlugin(api, options.sink ? emit : undefined)
186
+ )
187
+ if (options.runtimeMetrics)
188
+ plugins.push(
189
+ createRuntimeMetricsPlugin(
190
+ options.runtimeMetrics === true ? {} : options.runtimeMetrics
191
+ )
192
+ )
193
+ if (options.profiler) plugins.push(options.profiler)
194
+ handle = startPlugins(recording, plugins, reportError)
195
+ publish({ collectors: plugins.map((plugin) => plugin.id) })
196
+ } catch (error) {
197
+ try {
198
+ await stopCurrent()
199
+ } catch (cleanupError) {
200
+ reportError(cleanupError)
201
+ }
202
+ try {
203
+ recording?.dispose()
204
+ } finally {
205
+ recording = undefined
206
+ }
207
+ throw error
208
+ }
209
+ enabled = true
210
+ if (options.autoProfile && options.profiler)
211
+ safely(() => options.profiler!.startProfiling())
212
+ publish()
213
+ }
214
+ const context = (name: string, attributes?: ClientAttributes) => ({
215
+ name,
216
+ correlationId: String(attributes?.id ?? ''),
217
+ attributes: Object.entries(attributes ?? {})
218
+ .filter(([, value]) => value !== undefined)
219
+ .slice(0, 32)
220
+ .map(([key, value]) => ({
221
+ key: key.slice(0, 64),
222
+ value: String(value).slice(0, 512),
223
+ })),
224
+ })
225
+ const trace = {
226
+ mark(name: string, attributes?: ClientAttributes) {
227
+ safely(() => {
228
+ if (!enabled || !recording) return
229
+ recording.mark(context(name, attributes))
230
+ emit({ name, entryType: 'mark', durationMs: 0, attributes })
231
+ if (typeof attributes?.id === 'string') {
232
+ let bucket = marks.get(attributes.id)
233
+ if (!bucket) {
234
+ if (marks.size >= 512) marks.delete(marks.keys().next().value!)
235
+ bucket = { marks: new Map(), emitted: new Set() }
236
+ marks.set(attributes.id, bucket)
237
+ }
238
+ if (bucket.marks.size >= 64 && !bucket.marks.has(name))
239
+ bucket.marks.delete(bucket.marks.keys().next().value!)
240
+ bucket.marks.set(name, recording.getStats().nowMs)
241
+ }
242
+ })
243
+ },
244
+ metric(
245
+ name: string,
246
+ value: number | string,
247
+ attributes?: ClientAttributes
248
+ ) {
249
+ safely(() => {
250
+ if (!enabled || !recording) return
251
+ const numeric = Number(value)
252
+ if (Number.isFinite(numeric))
253
+ recording.recordMetric({
254
+ ...context(name, attributes),
255
+ value: numeric,
256
+ unit: String(attributes?.unit ?? 'count'),
257
+ })
258
+ else recording.mark(context(name, { ...attributes, value }))
259
+ emit({ name, entryType: 'metric', durationMs: 0, value, attributes })
260
+ })
261
+ },
262
+ async measure<T>(
263
+ name: string,
264
+ fn: () => Promise<T>,
265
+ attributes?: ClientAttributes
266
+ ): Promise<T> {
267
+ const started =
268
+ enabled && options.sink ? globalThis.performance.now() : undefined
269
+ const span = safely(() =>
270
+ enabled ? recording?.startSpan(context(name, attributes)) : undefined
271
+ )
272
+ try {
273
+ const value = await fn()
274
+ safely(() => span?.end('success'))
275
+ return value
276
+ } catch (error) {
277
+ safely(() => span?.end('error'))
278
+ throw error
279
+ } finally {
280
+ if (started !== undefined)
281
+ emit({
282
+ name,
283
+ entryType: 'measure',
284
+ durationMs: Math.max(0, globalThis.performance.now() - started),
285
+ attributes,
286
+ })
287
+ }
288
+ },
289
+ measureSince(
290
+ name: string,
291
+ startName: string,
292
+ id: string,
293
+ attributes?: ClientAttributes
294
+ ) {
295
+ safely(() => {
296
+ if (!enabled || !recording) return
297
+ const bucket = marks.get(id)
298
+ const start = bucket?.marks.get(startName)
299
+ if (
300
+ start === undefined ||
301
+ bucket!.emitted.has(name) ||
302
+ bucket!.emitted.size >= 64
303
+ )
304
+ return
305
+ bucket!.emitted.add(name)
306
+ const durationMs = Math.max(0, recording.getStats().nowMs - start)
307
+ recording.recordSpan({
308
+ ...context(name, { ...attributes, id, startName }),
309
+ timestampMs: start,
310
+ durationMs,
311
+ outcome: 'success',
312
+ })
313
+ emit({
314
+ name,
315
+ entryType: 'measure',
316
+ durationMs,
317
+ attributes: { ...attributes, id },
318
+ })
319
+ })
320
+ },
321
+ clear(id: string) {
322
+ marks.delete(id)
323
+ },
324
+ }
325
+ return {
326
+ /** Once per client lifetime: elapsed time from client creation to the app-defined ready signal. */
327
+ reportAppReady() {
328
+ if (readyReported || !enabled) return
329
+ readyReported = true
330
+ trace.metric(
331
+ 'app.ready.after_tracer_init',
332
+ Math.max(0, globalThis.performance.now() - readinessStartMs),
333
+ {
334
+ unit: 'ms',
335
+ source: 'app-readiness',
336
+ definition:
337
+ 'Tracer initialization to app ready signal; excludes earlier JS and native startup',
338
+ }
339
+ )
340
+ },
341
+ trace,
342
+ start: () => enqueue(startCurrent),
343
+ stop: () => enqueue(stopCurrent),
344
+ clear: () =>
345
+ enqueue(async () => {
346
+ const wasEnabled = enabled
347
+ await startCurrent()
348
+ if (!wasEnabled) await stopCurrent()
349
+ }),
350
+ getRecording: () => recording,
351
+ getError: () => snapshot.error,
352
+ getSnapshot: () => snapshot,
353
+ subscribe(listener: () => void) {
354
+ listeners.add(listener)
355
+ return () => {
356
+ listeners.delete(listener)
357
+ }
358
+ },
359
+ open() {
360
+ if (!disposed) publish({ visible: true })
361
+ },
362
+ close() {
363
+ publish({ visible: false })
364
+ },
365
+ reportError,
366
+ canShare: Boolean(options.share),
367
+ canProfile: Boolean(options.profiler),
368
+ canShareProfile: Boolean(options.shareProfile),
369
+ export: () =>
370
+ enqueue(async () => {
371
+ if (!recording || !options.share) return
372
+ await stopCurrent()
373
+ await options.share(recording, 'recording')
374
+ }),
375
+ /** Share a snapshot while recording continues, e.g. right after catching a problem. */
376
+ shareTrace: (format: TraceFormat = 'perfetto') =>
377
+ enqueue(async () => {
378
+ if (recording && options.share) await options.share(recording, format)
379
+ }),
380
+ /**
381
+ * Mark the moment a tester saw a problem. With an idle profiler this also captures
382
+ * a bounded Hermes profile, so the flag ships with what the JS thread was doing.
383
+ */
384
+ flag: (note?: string) => {
385
+ trace.mark('flag', { source: 'flag', note })
386
+ const profiler = options.profiler
387
+ const windowMs = options.flagProfileMs ?? 10000
388
+ if (!profiler || windowMs <= 0) return
389
+ // enqueue already reports failures; the catches only avoid unhandled rejections.
390
+ enqueue(async () => {
391
+ if (!enabled || profiler.isProfiling()) return
392
+ profiler.startProfiling()
393
+ setTimeout(() => {
394
+ enqueue(async () => {
395
+ if (profiler.isProfiling()) await profiler.stopProfiling()
396
+ }).catch(() => {})
397
+ }, windowMs)
398
+ }).catch(() => {})
399
+ },
400
+ toggleProfile: () =>
401
+ enqueue(async () => {
402
+ if (!enabled || !options.profiler) return
403
+ if (options.profiler.isProfiling())
404
+ await options.profiler.stopProfiling()
405
+ else options.profiler.startProfiling()
406
+ }),
407
+ shareProfile: () =>
408
+ enqueue(async () => {
409
+ const path = options.profiler?.getProfilePath()
410
+ if (path) await options.shareProfile?.(path)
411
+ }),
412
+ playground: () =>
413
+ enqueue(async () => {
414
+ if (enabled && recording)
415
+ publish({ playground: await recordPlayground(recording) })
416
+ }),
417
+ dispose() {
418
+ if (disposed) return tail
419
+ disposed = true
420
+ enabled = false
421
+ publish({ visible: false })
422
+ const result = tail.then(async () => {
423
+ try {
424
+ await stopCurrent()
425
+ } finally {
426
+ try {
427
+ recording?.dispose()
428
+ } finally {
429
+ recording = undefined
430
+ listeners.clear()
431
+ }
432
+ }
433
+ })
434
+ tail = result.catch(reportError)
435
+ return result
436
+ },
437
+ }
438
+ }
439
+ export type TraceClient = ReturnType<typeof createTraceClient>
@@ -0,0 +1,143 @@
1
+ import type { Recording } from '../specs/Recording.nitro'
2
+ import type { SpanEvent, MetricEvent } from '../types'
3
+ export type PlaygroundCheckId =
4
+ 'nested' | 'concurrency' | 'cancelled' | 'idempotent' | 'error' | 'metrics'
5
+ export interface PlaygroundReport {
6
+ correlationId: string
7
+ checks: { id: PlaygroundCheckId; passed: boolean }[]
8
+ passed: boolean
9
+ }
10
+ const delay = (ms: number) =>
11
+ new Promise<void>((resolve) => setTimeout(resolve, ms))
12
+ /** Adds a small isolated scenario; never resets or stops the user's recording. */
13
+ export const recordPlayground = async (
14
+ current: Recording
15
+ ): Promise<PlaygroundReport> => {
16
+ const identity = current.startSpan({
17
+ name: 'playground.run',
18
+ correlationId: '',
19
+ attributes: [],
20
+ })
21
+ const correlationId = `playground-${identity.spanId}`
22
+ identity.end('success')
23
+ const root = current.startSpan({
24
+ name: 'playground.pipeline',
25
+ correlationId,
26
+ attributes: [{ key: 'source', value: 'playground' }],
27
+ })
28
+ const start = (name: string) =>
29
+ current.startSpan({
30
+ name,
31
+ correlationId,
32
+ parentSpanId: root.spanId,
33
+ attributes: [],
34
+ })
35
+ try {
36
+ const first = start('playground.concurrent')
37
+ const second = start('playground.concurrent')
38
+ await Promise.all([
39
+ delay(30).then(() => first.end('success')),
40
+ delay(60).then(() => second.end('success')),
41
+ ])
42
+ const cancelled = start('playground.cancelled')
43
+ cancelled.end('cancelled')
44
+ const repeated = start('playground.idempotent')
45
+ repeated.end('success')
46
+ repeated.end('error')
47
+ const failed = start('playground.error')
48
+ failed.end('error')
49
+ for (const value of [20, 45, 80])
50
+ current.recordMetric({
51
+ name: 'playground.throughput',
52
+ correlationId,
53
+ attributes: [],
54
+ value,
55
+ unit: 'byte/second',
56
+ })
57
+ root.end('success')
58
+ const spans: SpanEvent[] = []
59
+ const metrics: MetricEvent[] = []
60
+ let afterSequence = 0
61
+ // Native recordings cap at 100000 events. Retain only this scenario while scanning.
62
+ for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
63
+ const page = await current.readEvents({ afterSequence, limit: 1000 })
64
+ spans.push(
65
+ ...page.spans.filter(
66
+ (event) =>
67
+ event.correlationId === correlationId ||
68
+ event.spanId === root.spanId
69
+ )
70
+ )
71
+ metrics.push(
72
+ ...page.metrics.filter((event) => event.correlationId === correlationId)
73
+ )
74
+ if (
75
+ page.nextSequence <= afterSequence ||
76
+ page.spans.length + page.marks.length + page.metrics.length < 1000
77
+ )
78
+ break
79
+ afterSequence = page.nextSequence
80
+ }
81
+ const concurrent = spans.filter(
82
+ (span) => span.name === 'playground.concurrent'
83
+ )
84
+ const repeatedEvents = spans.filter(
85
+ (span) => span.spanId === repeated.spanId
86
+ )
87
+ const checks: PlaygroundReport['checks'] = [
88
+ {
89
+ id: 'nested',
90
+ passed:
91
+ spans.length === 6 &&
92
+ spans
93
+ .filter((span) => span.spanId !== root.spanId)
94
+ .every((span) => span.parentSpanId === root.spanId),
95
+ },
96
+ {
97
+ id: 'concurrency',
98
+ passed:
99
+ concurrent.length === 2 &&
100
+ concurrent[0].spanId !== concurrent[1].spanId &&
101
+ Math.max(...concurrent.map((span) => span.timestampMs)) <=
102
+ Math.min(
103
+ ...concurrent.map((span) => span.timestampMs + span.durationMs)
104
+ ),
105
+ },
106
+ {
107
+ id: 'cancelled',
108
+ passed: spans.some(
109
+ (span) =>
110
+ span.spanId === cancelled.spanId && span.outcome === 'cancelled'
111
+ ),
112
+ },
113
+ {
114
+ id: 'idempotent',
115
+ passed:
116
+ repeatedEvents.length === 1 &&
117
+ repeatedEvents[0].outcome === 'success',
118
+ },
119
+ {
120
+ id: 'error',
121
+ passed: spans.some(
122
+ (span) => span.spanId === failed.spanId && span.outcome === 'error'
123
+ ),
124
+ },
125
+ {
126
+ id: 'metrics',
127
+ passed:
128
+ metrics.length === 3 &&
129
+ metrics.map((metric) => metric.value).join(',') === '20,45,80',
130
+ },
131
+ ]
132
+ return {
133
+ correlationId,
134
+ checks,
135
+ passed: checks.every((check) => check.passed),
136
+ }
137
+ } catch (error) {
138
+ try {
139
+ root.end('error')
140
+ } catch {}
141
+ throw error
142
+ }
143
+ }
@@ -0,0 +1,71 @@
1
+ import * as FileSystem from 'expo-file-system/legacy'
2
+ import * as Sharing from 'expo-sharing'
3
+ import type { TraceClient, TraceFormat } from '../client/createTraceClient'
4
+ import type { Recording } from '../specs/Recording.nitro'
5
+ export interface ExpoSharingOptions {
6
+ dialogTitle?: string
7
+ }
8
+ /** Native OS file sharing. No remote upload or file-path export to telemetry. */
9
+ export function createExpoTraceSharing(options: ExpoSharingOptions = {}) {
10
+ const shareFile = async (uri: string, mimeType: string) => {
11
+ if (!(await Sharing.isAvailableAsync()))
12
+ throw new Error('Native file sharing is unavailable on this device')
13
+ await Sharing.shareAsync(uri, {
14
+ mimeType,
15
+ dialogTitle: options.dialogTitle ?? 'Share performance recording',
16
+ })
17
+ }
18
+ return {
19
+ async share(recording: Recording, format: TraceFormat = 'recording') {
20
+ if (!FileSystem.cacheDirectory)
21
+ throw new Error('Application cache is unavailable')
22
+ const prefix = format === 'perfetto' ? 'perfetto-trace' : 'native-trace'
23
+ const uri = `${FileSystem.cacheDirectory}${prefix}-${
24
+ recording.getStats().sessionId
25
+ }-${Date.now()}.json`
26
+ // Both formats serialize off the JS thread; only the finished string crosses JSI.
27
+ const json =
28
+ format === 'perfetto'
29
+ ? await recording.exportTraceEvents()
30
+ : await recording.exportJson()
31
+ try {
32
+ await FileSystem.writeAsStringAsync(uri, json, {
33
+ encoding: FileSystem.EncodingType.UTF8,
34
+ })
35
+ await shareFile(uri, 'application/json')
36
+ } finally {
37
+ await FileSystem.deleteAsync(uri, { idempotent: true })
38
+ }
39
+ },
40
+ async shareProfile(path: string) {
41
+ await shareFile(
42
+ path.startsWith('file://') ? path : `file://${path}`,
43
+ 'application/octet-stream'
44
+ )
45
+ },
46
+ }
47
+ }
48
+ /** Register only in a development build. The consumer owns client disposal. */
49
+ export async function registerTraceDevMenu(
50
+ client: TraceClient,
51
+ options: {
52
+ name?: string
53
+ registerDevMenuItems?: typeof import('expo-dev-client').registerDevMenuItems
54
+ } = {}
55
+ ) {
56
+ try {
57
+ const register =
58
+ options.registerDevMenuItems ??
59
+ (require('expo-dev-client') as typeof import('expo-dev-client'))
60
+ .registerDevMenuItems
61
+ await register([
62
+ {
63
+ name: options.name ?? 'Performance inspector',
64
+ shouldCollapse: true,
65
+ callback: () => client.open(),
66
+ },
67
+ ])
68
+ } catch (error) {
69
+ client.reportError(error)
70
+ }
71
+ }
@@ -0,0 +1,2 @@
1
+ export { createExpoTraceSharing, registerTraceDevMenu } from './ExpoAdapter'
2
+ export type { ExpoSharingOptions } from './ExpoAdapter'
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { NitroModules } from 'react-native-nitro-modules'
2
+ import type { Tracing as TracingFactory } from './specs/Tracing.nitro'
3
+ /** Native recording factory. See Recording for ownership and cleanup. */
4
+ export const Tracing =
5
+ NitroModules.createHybridObject<TracingFactory>('Tracing')
6
+ export type { Recording } from './specs/Recording.nitro'
7
+ export type { TraceSpan } from './specs/TraceSpan.nitro'
8
+ export type * from './types'
@@ -0,0 +1,22 @@
1
+ import type { Recording } from '../specs/Recording.nitro'
2
+ /** Shared native destination supplied to measurement and export plugins. */
3
+ export interface PluginContext {
4
+ /** Active native recording. */
5
+ recording: Recording
6
+ /** Surface plugin failures without changing application operations. */
7
+ reportError(error: unknown): void
8
+ }
9
+ /** Cleanup owned by a started plugin. */
10
+ export interface PluginHandle {
11
+ /** Disconnect observers and finish export before native recording disposal. */
12
+ stop(): void | Promise<void>
13
+ /** Optional exporter flush after sources stop and native history freezes. */
14
+ flush?(): void | Promise<void>
15
+ }
16
+ /** Optional measurement source, exporter or profiler integration. */
17
+ export interface TracePlugin {
18
+ /** Unique stable configuration identity. */
19
+ readonly id: string
20
+ /** Start once per recording; must roll back resources if setup throws. */
21
+ start(context: PluginContext): PluginHandle
22
+ }