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,69 @@
1
+ import type { TracePlugin } from './TracePlugin'
2
+ type Handler = (error: unknown, isFatal?: boolean) => void
3
+ export interface ErrorUtilsLike {
4
+ getGlobalHandler(): Handler
5
+ setGlobalHandler(handler: Handler): void
6
+ }
7
+ export interface ErrorsPluginOptions {
8
+ /** Defaults to React Native's global ErrorUtils. */
9
+ errorUtils?: ErrorUtilsLike
10
+ /** Also record console.error calls. Default false; libraries log non-fatal noise there. */
11
+ console?: boolean
12
+ }
13
+ const messageOf = (value: unknown) =>
14
+ value instanceof Error ? `${value.name}: ${value.message}` : String(value)
15
+ /** Records uncaught JS errors as `js.error` marks, then calls the previous handler unchanged. */
16
+ export function createErrorsPlugin(
17
+ options: ErrorsPluginOptions = {}
18
+ ): TracePlugin {
19
+ return {
20
+ id: 'errors',
21
+ start({ recording, reportError }) {
22
+ const utils =
23
+ options.errorUtils ??
24
+ (globalThis as unknown as { ErrorUtils?: ErrorUtilsLike }).ErrorUtils
25
+ let active = true
26
+ let recordingError = false
27
+ const record = (kind: string, message: string, fatal: boolean) => {
28
+ if (!active || recordingError) return
29
+ recordingError = true
30
+ try {
31
+ recording.mark({
32
+ name: 'js.error',
33
+ correlationId: '',
34
+ attributes: [
35
+ { key: 'source', value: 'error' },
36
+ { key: 'kind', value: kind },
37
+ { key: 'fatal', value: String(fatal) },
38
+ { key: 'message', value: message.slice(0, 1024) },
39
+ ],
40
+ })
41
+ } catch (error) {
42
+ reportError(error)
43
+ } finally {
44
+ recordingError = false
45
+ }
46
+ }
47
+ const previous = utils?.getGlobalHandler()
48
+ const handler: Handler = (error, isFatal) => {
49
+ record('global', messageOf(error), Boolean(isFatal))
50
+ previous?.(error, isFatal)
51
+ }
52
+ utils?.setGlobalHandler(handler)
53
+ const consoleError = console.error
54
+ const patchedConsole = (...args: unknown[]) => {
55
+ record('console', args.map(messageOf).join(' '), false)
56
+ consoleError.apply(console, args)
57
+ }
58
+ if (options.console) console.error = patchedConsole
59
+ return {
60
+ stop() {
61
+ active = false
62
+ if (utils && previous && utils.getGlobalHandler() === handler)
63
+ utils.setGlobalHandler(previous)
64
+ if (console.error === patchedConsole) console.error = consoleError
65
+ },
66
+ }
67
+ },
68
+ }
69
+ }
@@ -0,0 +1,26 @@
1
+ import type { TracePlugin } from './TracePlugin'
2
+ export interface NativeMetricsOptions {
3
+ /** Metric window; 250–60000 ms. Default 1000. */
4
+ intervalMs?: number
5
+ /** Main-thread frame metrics (ui.fps, ui.frame_gap.max, ui.frames.slow/frozen). Default true. */
6
+ frames?: boolean
7
+ }
8
+ /**
9
+ * Native measurement layer: a sampler thread writes process.cpu, process.memory and
10
+ * main-thread frame windows straight into the recording, with no per-frame JS work.
11
+ * Alternative or complement to Sentry's measurements; each metric carries source=native.
12
+ */
13
+ export function createNativeMetricsPlugin(
14
+ options: NativeMetricsOptions = {}
15
+ ): TracePlugin {
16
+ return {
17
+ id: 'native-metrics',
18
+ start({ recording }) {
19
+ recording.startNativeSampling({
20
+ intervalMs: options.intervalMs ?? 1000,
21
+ frames: options.frames ?? true,
22
+ })
23
+ return { stop: () => recording.stopNativeSampling() }
24
+ },
25
+ }
26
+ }
@@ -0,0 +1,111 @@
1
+ import type { TraceSpan } from '../specs/TraceSpan.nitro'
2
+ import type { TracePlugin } from './TracePlugin'
3
+ /** Structural subset of a React Navigation container ref (expo-router: useNavigationContainerRef()). */
4
+ export interface NavigationRefLike {
5
+ addListener(event: 'state' | 'ready', listener: () => void): () => void
6
+ getCurrentRoute(): { name: string; key?: string } | undefined
7
+ /** React Navigation logs an error when getCurrentRoute() runs before mount. */
8
+ isReady?(): boolean
9
+ }
10
+ export interface NavigationPluginOptions {
11
+ /** Defaults to requestAnimationFrame. */
12
+ requestFrame?: (callback: () => void) => unknown
13
+ }
14
+ /**
15
+ * Per focused route: a `navigation.enter` mark, a `screen <name>` visit span (dwell time)
16
+ * and a `navigation.transition` metric from state change to the second frame callback.
17
+ * The transition is a render-opportunity estimate, not content readiness.
18
+ */
19
+ export function createNavigationPlugin(
20
+ ref: NavigationRefLike,
21
+ options: NavigationPluginOptions = {}
22
+ ): TracePlugin {
23
+ return {
24
+ id: 'navigation',
25
+ start({ recording, reportError }) {
26
+ const requestFrame =
27
+ options.requestFrame ??
28
+ ((callback: () => void) => requestAnimationFrame(callback))
29
+ let visit: TraceSpan | undefined
30
+ let current: string | undefined
31
+ let visits = 0
32
+ let stopped = false
33
+ const enter = () => {
34
+ if (stopped) return
35
+ try {
36
+ if (ref.isReady?.() === false) return
37
+ const route = ref.getCurrentRoute()
38
+ const key = route && (route.key ?? route.name)
39
+ if (!route || key === current) return
40
+ current = key
41
+ visit?.end('success')
42
+ visit = undefined
43
+ const correlationId = `screen:${++visits}`
44
+ const attributes = [
45
+ { key: 'source', value: 'navigation' },
46
+ { key: 'screen', value: route.name.slice(0, 1024) },
47
+ ]
48
+ const startedAt = recording.getStats().nowMs
49
+ recording.mark({
50
+ name: 'navigation.enter',
51
+ correlationId,
52
+ attributes,
53
+ })
54
+ visit = recording.startSpan({
55
+ name: `screen ${route.name}`.slice(0, 200),
56
+ correlationId,
57
+ attributes,
58
+ })
59
+ requestFrame(() =>
60
+ requestFrame(() => {
61
+ if (stopped) return
62
+ try {
63
+ recording.recordMetric({
64
+ name: 'navigation.transition',
65
+ value: recording.getStats().nowMs - startedAt,
66
+ unit: 'ms',
67
+ correlationId,
68
+ attributes,
69
+ })
70
+ } catch (error) {
71
+ reportError(error)
72
+ }
73
+ })
74
+ )
75
+ } catch (error) {
76
+ reportError(error)
77
+ }
78
+ }
79
+ // The initial route emits `ready`, not `state`; single-screen apps would record nothing.
80
+ const unsubscribers = [
81
+ ref.addListener('ready', enter),
82
+ ref.addListener('state', enter),
83
+ ]
84
+ enter()
85
+ return {
86
+ stop() {
87
+ stopped = true
88
+ for (const unsubscribe of unsubscribers) unsubscribe()
89
+ visit?.end('success')
90
+ },
91
+ }
92
+ },
93
+ }
94
+ }
95
+ /** For apps without React Navigation (custom tabs): pass to createNavigationPlugin, call setScreen on change. */
96
+ export function createScreenTracker(initial?: string) {
97
+ let route = initial ? { name: initial } : undefined
98
+ const listeners = new Set<() => void>()
99
+ return {
100
+ addListener(_event: 'state' | 'ready', listener: () => void) {
101
+ listeners.add(listener)
102
+ return () => void listeners.delete(listener)
103
+ },
104
+ getCurrentRoute: () => route,
105
+ setScreen(name: string) {
106
+ if (route?.name === name) return
107
+ route = { name }
108
+ for (const listener of listeners) listener()
109
+ },
110
+ }
111
+ }
@@ -0,0 +1,178 @@
1
+ import type { TracePlugin } from './TracePlugin'
2
+ type Fetch = typeof fetch
3
+ export interface NetworkPluginOptions {
4
+ /** Defaults to the global XMLHttpRequest, which backs axios and React Native's polyfilled fetch. */
5
+ XMLHttpRequest?: { prototype: XMLHttpRequest }
6
+ /** Object whose `fetch` is wrapped. Defaults to globalThis. */
7
+ fetchHost?: { fetch?: Fetch }
8
+ /** Skip matching URLs. Defaults to Metro symbolication requests. */
9
+ ignore?: (url: string) => boolean
10
+ }
11
+ /** Path without origin/query; numeric, UUID and long hex segments become `:id` so requests group. */
12
+ export const urlTemplate = (url: string) =>
13
+ url
14
+ .split(/[?#]/)[0]
15
+ .replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, '')
16
+ .replace(
17
+ /\/(\d+|[0-9a-f]{8}-[0-9a-f-]{27,}|[0-9a-f]{16,})(?=\/|$)/gi,
18
+ '/:id'
19
+ ) || '/'
20
+ /**
21
+ * Opt-in request timing: method, URL without query, status and duration. Bodies and headers
22
+ * are never read. XMLHttpRequest spans end at loadend; native fetch spans (expo/fetch,
23
+ * nitro-fetch) end when response headers arrive, since reading bodies is left to the app.
24
+ */
25
+ export function createNetworkPlugin(
26
+ options: NetworkPluginOptions = {}
27
+ ): TracePlugin {
28
+ const ignore =
29
+ options.ignore ?? ((url: string) => url.includes('/symbolicate'))
30
+ return {
31
+ id: 'network',
32
+ start({ recording, reportError }) {
33
+ let active = true
34
+ const record = (
35
+ method: string,
36
+ url: string,
37
+ startedAt: number,
38
+ status: number,
39
+ failure?: 'cancelled' | 'error'
40
+ ) => {
41
+ if (!active) return
42
+ try {
43
+ recording.recordSpan({
44
+ name: `${method} ${urlTemplate(url)}`.slice(0, 200),
45
+ correlationId: '',
46
+ timestampMs: startedAt,
47
+ durationMs: Math.max(0, recording.getStats().nowMs - startedAt),
48
+ outcome:
49
+ failure ?? (status === 0 || status >= 400 ? 'error' : 'success'),
50
+ attributes: [
51
+ { key: 'source', value: 'network' },
52
+ { key: 'method', value: method },
53
+ { key: 'url', value: url.split(/[?#]/)[0].slice(0, 1024) },
54
+ { key: 'status', value: String(status) },
55
+ ],
56
+ })
57
+ } catch (error) {
58
+ reportError(error)
59
+ }
60
+ }
61
+
62
+ const proto = (options.XMLHttpRequest ?? globalThis.XMLHttpRequest)
63
+ ?.prototype
64
+ const open = proto?.open
65
+ const send = proto?.send
66
+ const requests = new WeakMap<
67
+ XMLHttpRequest,
68
+ { method: string; url: string }
69
+ >()
70
+ const patchedOpen = function (
71
+ this: XMLHttpRequest,
72
+ method: string,
73
+ url: string | URL,
74
+ ...rest: unknown[]
75
+ ) {
76
+ if (active && !ignore(String(url)))
77
+ requests.set(this, {
78
+ method: String(method).toUpperCase(),
79
+ url: String(url),
80
+ })
81
+ return (open as (...args: unknown[]) => void).call(
82
+ this,
83
+ method,
84
+ url,
85
+ ...rest
86
+ )
87
+ }
88
+ const patchedSend = function (
89
+ this: XMLHttpRequest,
90
+ body?: Document | XMLHttpRequestBodyInit | null
91
+ ) {
92
+ const request = requests.get(this)
93
+ if (active && request) {
94
+ try {
95
+ const startedAt = recording.getStats().nowMs
96
+ let failure: 'cancelled' | 'error' | undefined
97
+ this.addEventListener('abort', () => (failure = 'cancelled'))
98
+ this.addEventListener('timeout', () => (failure = 'error'))
99
+ this.addEventListener('error', () => (failure = 'error'))
100
+ this.addEventListener('loadend', () =>
101
+ record(
102
+ request.method,
103
+ request.url,
104
+ startedAt,
105
+ this.status,
106
+ failure
107
+ )
108
+ )
109
+ } catch (error) {
110
+ reportError(error)
111
+ }
112
+ }
113
+ return send!.call(this, body)
114
+ }
115
+ if (proto) {
116
+ proto.open = patchedOpen as typeof proto.open
117
+ proto.send = patchedSend
118
+ }
119
+
120
+ // whatwg-fetch marks itself `polyfill` and already runs through the XHR hook above.
121
+ const host = options.fetchHost ?? (globalThis as { fetch?: Fetch })
122
+ const originalFetch = host.fetch
123
+ const wrapFetch =
124
+ typeof originalFetch === 'function' &&
125
+ !(originalFetch as Fetch & { polyfill?: boolean }).polyfill
126
+ const patchedFetch: Fetch = async (input, init) => {
127
+ const url =
128
+ typeof input === 'string'
129
+ ? input
130
+ : input instanceof URL
131
+ ? input.href
132
+ : input.url
133
+ if (!active || ignore(url)) return originalFetch!(input, init)
134
+ const method = String(
135
+ init?.method ??
136
+ (typeof input === 'object' && 'method' in input
137
+ ? input.method
138
+ : 'GET')
139
+ ).toUpperCase()
140
+ let startedAt = 0
141
+ try {
142
+ startedAt = recording.getStats().nowMs
143
+ } catch (error) {
144
+ reportError(error)
145
+ }
146
+ try {
147
+ const response = await originalFetch!(input, init)
148
+ record(method, url, startedAt, response.status)
149
+ return response
150
+ } catch (error) {
151
+ record(
152
+ method,
153
+ url,
154
+ startedAt,
155
+ 0,
156
+ (error as { name?: string })?.name === 'AbortError'
157
+ ? 'cancelled'
158
+ : 'error'
159
+ )
160
+ throw error
161
+ }
162
+ }
163
+ if (wrapFetch) host.fetch = patchedFetch
164
+
165
+ return {
166
+ stop() {
167
+ active = false
168
+ // ponytail: if another library wrapped after us, leave our pass-through wrappers in place.
169
+ if (proto && proto.open === (patchedOpen as unknown))
170
+ proto.open = open!
171
+ if (proto && proto.send === patchedSend) proto.send = send!
172
+ if (wrapFetch && host.fetch === patchedFetch)
173
+ host.fetch = originalFetch
174
+ },
175
+ }
176
+ },
177
+ }
178
+ }
@@ -0,0 +1,22 @@
1
+ import type { SpanEvent } from '../types'
2
+ /** Parent-first order in O(n), including parents delivered after their children. */
3
+ export function orderSpans(spans: readonly SpanEvent[]): SpanEvent[] {
4
+ const byId = new Map(spans.map((span) => [span.spanId, span]))
5
+ const children = new Map<string, SpanEvent[]>()
6
+ const ordered: SpanEvent[] = []
7
+ for (const span of byId.values()) {
8
+ if (!span.parentSpanId || !byId.has(span.parentSpanId)) ordered.push(span)
9
+ else {
10
+ const siblings = children.get(span.parentSpanId)
11
+ if (siblings) siblings.push(span)
12
+ else children.set(span.parentSpanId, [span])
13
+ }
14
+ }
15
+ for (let index = 0; index < ordered.length; index++) {
16
+ const descendants = children.get(ordered[index]!.spanId)
17
+ if (descendants) for (const child of descendants) ordered.push(child)
18
+ }
19
+ if (ordered.length !== byId.size)
20
+ throw new Error('Cannot export cyclic span parents')
21
+ return ordered
22
+ }
@@ -0,0 +1,120 @@
1
+ import type { TracingEntry } from '../client/TracingSink'
2
+ import type performance from 'react-native-performance'
3
+ import type { PerformanceObserver } from 'react-native-performance'
4
+ import type { TracePlugin } from './TracePlugin'
5
+ /** Default measurement-source dependencies; supports the installed RN performance API. */
6
+ export interface PerformancePluginOptions {
7
+ /** Same clock used by the supplied observer's entries. */
8
+ performance: typeof performance
9
+ /** PerformanceObserver from react-native-performance. */
10
+ PerformanceObserver: typeof PerformanceObserver
11
+ }
12
+ /** Observe user timing without patching globals, clearing shared buffers or timing network bodies. */
13
+ export function createPerformancePlugin(
14
+ api: PerformancePluginOptions,
15
+ onEntry?: (entry: TracingEntry) => void
16
+ ): TracePlugin {
17
+ return {
18
+ id: 'performance',
19
+ start({ recording, reportError }) {
20
+ const offset = recording.getStats().nowMs - api.performance.now()
21
+ let stopped = false
22
+ const ingest = (
23
+ entries: ReturnType<PerformanceObserver['takeRecords']>
24
+ ) => {
25
+ if (stopped) return
26
+ for (const entry of entries) {
27
+ try {
28
+ const timestampMs = entry.startTime + offset
29
+ if (timestampMs < 0) continue // Previous history is outside this recording.
30
+ const data = entry as typeof entry & {
31
+ value?: unknown
32
+ detail?: { id?: unknown; unit?: unknown }
33
+ }
34
+ onEntry?.({
35
+ name: entry.name,
36
+ entryType: entry.entryType,
37
+ durationMs: entry.duration,
38
+ value:
39
+ typeof data.value === 'string' || typeof data.value === 'number'
40
+ ? data.value
41
+ : undefined,
42
+ attributes: data.detail,
43
+ })
44
+ const context = {
45
+ name: entry.name.slice(0, 128),
46
+ correlationId:
47
+ typeof data.detail?.id === 'string'
48
+ ? data.detail.id.slice(0, 128)
49
+ : '',
50
+ attributes: [
51
+ { key: 'source', value: 'performance' },
52
+ { key: 'source.startTimeMs', value: String(entry.startTime) },
53
+ ],
54
+ }
55
+ if (entry.entryType === 'longtask')
56
+ recording.recordSpan({
57
+ ...context,
58
+ name: 'js.longtask',
59
+ timestampMs,
60
+ durationMs: entry.duration,
61
+ outcome: 'success',
62
+ })
63
+ else if (entry.entryType === 'measure')
64
+ recording.recordSpan({
65
+ ...context,
66
+ timestampMs,
67
+ durationMs: entry.duration,
68
+ outcome: 'success',
69
+ })
70
+ else if (entry.entryType === 'mark')
71
+ recording.mark({ ...context, timestampMs })
72
+ else if (
73
+ entry.entryType === 'metric' &&
74
+ typeof data.value === 'number' &&
75
+ Number.isFinite(data.value)
76
+ )
77
+ recording.recordMetric({
78
+ ...context,
79
+ timestampMs,
80
+ value: data.value,
81
+ unit:
82
+ typeof data.detail?.unit === 'string'
83
+ ? data.detail.unit.slice(0, 32)
84
+ : 'unspecified',
85
+ })
86
+ } catch (error) {
87
+ reportError(error)
88
+ }
89
+ }
90
+ }
91
+ const observer = new api.PerformanceObserver((list) =>
92
+ ingest(list.getEntries())
93
+ )
94
+ // Global RN observers reject unknown types; react-native-performance lacks longtask.
95
+ const supported = (
96
+ api.PerformanceObserver as { supportedEntryTypes?: readonly string[] }
97
+ ).supportedEntryTypes
98
+ const entryTypes = ['mark', 'measure', 'metric', 'longtask'].filter(
99
+ (type) => (supported ? supported.includes(type) : type !== 'longtask')
100
+ )
101
+ try {
102
+ observer.observe({ entryTypes } as never)
103
+ } catch (error) {
104
+ observer.disconnect()
105
+ throw error
106
+ }
107
+ return {
108
+ stop() {
109
+ if (stopped) return
110
+ try {
111
+ ingest(observer.takeRecords())
112
+ } finally {
113
+ stopped = true
114
+ observer.disconnect()
115
+ }
116
+ },
117
+ }
118
+ },
119
+ }
120
+ }
@@ -0,0 +1,123 @@
1
+ import type { TraceSpan } from '../specs/TraceSpan.nitro'
2
+ import type { PluginContext, TracePlugin } from './TracePlugin'
3
+ /** Injected optional react-native-release-profiler module; never imported by core. */
4
+ export interface ReleaseProfilerApi {
5
+ /** Start the process-wide Hermes sampler; false means unavailable. */
6
+ startProfiling(): boolean | void
7
+ /** Stop and save a local profile artifact. */
8
+ stopProfiling(saveToDownloads?: boolean): Promise<string>
9
+ }
10
+ /** Manually controlled profiler integration. Sampling never starts on plugin setup. */
11
+ export interface ReleaseProfilerPlugin extends TracePlugin {
12
+ /** Start one session owned by this plugin. */
13
+ startProfiling(): void
14
+ /** Finish profiling and return the artifact's local path. */
15
+ stopProfiling(): Promise<string>
16
+ /** Whether this plugin owns an active or stopping profile. */
17
+ isProfiling(): boolean
18
+ /** Last successful artifact; not sent to remote exporters. */
19
+ getProfilePath(): string | undefined
20
+ }
21
+ // Hermes profiling is process-wide, so two plugin instances must not compete.
22
+ let owner: object | undefined
23
+ /** Stops Hermes sampling for real; on RN 0.85 Android the profiler's own stop restarts it. */
24
+ const stopHermesSampler = () => {
25
+ try {
26
+ const { Tracing } = require('../index') as typeof import('../index')
27
+ Tracing.disableHermesSampling()
28
+ } catch {
29
+ // No native module (tests, non-native hosts): nothing is sampling.
30
+ }
31
+ }
32
+ export interface ReleaseProfilerOptions {
33
+ /** Runs after every stop. Defaults to Tracing.disableHermesSampling(). */
34
+ stopSampler?: () => void
35
+ }
36
+ /** Attach manual Hermes profile controls to a recording. */
37
+ export function createReleaseProfilerPlugin(
38
+ api: ReleaseProfilerApi,
39
+ options: ReleaseProfilerOptions = {}
40
+ ): ReleaseProfilerPlugin {
41
+ const identity = {}
42
+ let context: PluginContext | undefined
43
+ let active = false
44
+ let span: TraceSpan | undefined
45
+ let path: string | undefined
46
+ let stopping: Promise<string> | undefined
47
+ const plugin: ReleaseProfilerPlugin = {
48
+ id: 'release-profiler',
49
+ start(next) {
50
+ if (context)
51
+ throw new Error('Release profiler plugin is already attached')
52
+ context = next
53
+ return {
54
+ async stop() {
55
+ try {
56
+ if (active) await plugin.stopProfiling()
57
+ } finally {
58
+ context = undefined
59
+ }
60
+ },
61
+ }
62
+ },
63
+ startProfiling() {
64
+ if (!context)
65
+ throw new Error('Attach release-profiler plugin before profiling')
66
+ if (owner) throw new Error('A release profiler session is already active')
67
+ if (api.startProfiling() === false)
68
+ throw new Error('Hermes sampling profiler is unavailable in this build')
69
+ owner = identity
70
+ active = true
71
+ path = undefined
72
+ try {
73
+ span = context.recording.startSpan({
74
+ name: 'hermes.profile',
75
+ correlationId: '',
76
+ attributes: [{ key: 'source', value: 'release-profiler' }],
77
+ })
78
+ } catch (error) {
79
+ context.reportError(error)
80
+ }
81
+ },
82
+ stopProfiling() {
83
+ if (stopping) return stopping
84
+ if (!active)
85
+ return Promise.reject(
86
+ new Error('No release profiler session is active')
87
+ )
88
+ stopping = Promise.resolve().then(async () => {
89
+ try {
90
+ path = await api.stopProfiling(false)
91
+ span?.end('success')
92
+ // Lets a Perfetto export point at the .cpuprofile covering the span above.
93
+ try {
94
+ context?.recording.mark({
95
+ name: 'hermes.profile.saved',
96
+ correlationId: '',
97
+ attributes: [
98
+ { key: 'source', value: 'release-profiler' },
99
+ { key: 'path', value: path.slice(0, 512) },
100
+ ],
101
+ })
102
+ } catch (error) {
103
+ context?.reportError(error)
104
+ }
105
+ return path
106
+ } catch (error) {
107
+ span?.end('error')
108
+ throw error
109
+ } finally {
110
+ ;(options.stopSampler ?? stopHermesSampler)()
111
+ active = false
112
+ if (owner === identity) owner = undefined
113
+ span = undefined
114
+ stopping = undefined
115
+ }
116
+ })
117
+ return stopping
118
+ },
119
+ isProfiling: () => active,
120
+ getProfilePath: () => path,
121
+ }
122
+ return plugin
123
+ }