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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/NitroTracing.podspec +32 -0
- package/README.md +75 -0
- package/android/CMakeLists.txt +49 -0
- package/android/build.gradle +133 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/proguard-rules.pro +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +47 -0
- package/android/src/main/java/com/margelo/nitro/nitrotracing/HermesSamplingGuard.kt +43 -0
- package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingFrameSource.kt +36 -0
- package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingPackage.kt +31 -0
- package/client.ts +15 -0
- package/cpp/HermesSampling.cpp +26 -0
- package/cpp/HermesSampling.hpp +10 -0
- package/cpp/HybridRecording.cpp +66 -0
- package/cpp/HybridRecording.hpp +30 -0
- package/cpp/HybridTraceSpan.cpp +10 -0
- package/cpp/HybridTraceSpan.hpp +17 -0
- package/cpp/HybridTracing.cpp +10 -0
- package/cpp/HybridTracing.hpp +10 -0
- package/cpp/NativeConversions.cpp +77 -0
- package/cpp/NativeConversions.hpp +15 -0
- package/cpp/core/NativeSampler.cpp +152 -0
- package/cpp/core/NativeSampler.hpp +56 -0
- package/cpp/core/Recorder.cpp +218 -0
- package/cpp/core/Recorder.hpp +60 -0
- package/cpp/core/Retention.cpp +26 -0
- package/cpp/core/Retention.hpp +7 -0
- package/cpp/core/TraceJson.cpp +214 -0
- package/cpp/core/TraceJson.hpp +7 -0
- package/cpp/core/TraceTypes.hpp +62 -0
- package/expo.ts +2 -0
- package/ios/NitroTracingFrameSource.mm +35 -0
- package/lib/commonjs/client.cjs +767 -0
- package/lib/commonjs/client.cjs.map +7 -0
- package/lib/commonjs/expo.cjs +88 -0
- package/lib/commonjs/expo.cjs.map +7 -0
- package/lib/commonjs/index.cjs +28 -0
- package/lib/commonjs/index.cjs.map +7 -0
- package/lib/commonjs/performance.cjs +112 -0
- package/lib/commonjs/performance.cjs.map +7 -0
- package/lib/commonjs/plugins.cjs +477 -0
- package/lib/commonjs/plugins.cjs.map +7 -0
- package/lib/commonjs/react.cjs +3833 -0
- package/lib/commonjs/react.cjs.map +7 -0
- package/lib/commonjs/release-profiler.cjs +142 -0
- package/lib/commonjs/release-profiler.cjs.map +7 -0
- package/lib/commonjs/sentry.cjs +188 -0
- package/lib/commonjs/sentry.cjs.map +7 -0
- package/lib/module/client.mjs +768 -0
- package/lib/module/client.mjs.map +7 -0
- package/lib/module/expo.mjs +62 -0
- package/lib/module/expo.mjs.map +7 -0
- package/lib/module/index.mjs +7 -0
- package/lib/module/index.mjs.map +7 -0
- package/lib/module/performance.mjs +89 -0
- package/lib/module/performance.mjs.map +7 -0
- package/lib/module/plugins.mjs +461 -0
- package/lib/module/plugins.mjs.map +7 -0
- package/lib/module/react.mjs +3851 -0
- package/lib/module/react.mjs.map +7 -0
- package/lib/module/release-profiler.mjs +137 -0
- package/lib/module/release-profiler.mjs.map +7 -0
- package/lib/module/sentry.mjs +165 -0
- package/lib/module/sentry.mjs.map +7 -0
- package/lib/typescript/client.d.ts +4 -0
- package/lib/typescript/expo.d.ts +2 -0
- package/lib/typescript/performance.d.ts +2 -0
- package/lib/typescript/plugins.d.ts +12 -0
- package/lib/typescript/react.d.ts +5 -0
- package/lib/typescript/release-profiler.d.ts +2 -0
- package/lib/typescript/sentry.d.ts +2 -0
- package/lib/typescript/src/client/TracingSink.d.ts +10 -0
- package/lib/typescript/src/client/createTraceClient.d.ts +82 -0
- package/lib/typescript/src/client/playground.d.ts +12 -0
- package/lib/typescript/src/expo/ExpoAdapter.d.ts +15 -0
- package/lib/typescript/src/expo/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/plugins/TracePlugin.d.ts +22 -0
- package/lib/typescript/src/plugins/errors.d.ts +15 -0
- package/lib/typescript/src/plugins/nativeMetrics.d.ts +13 -0
- package/lib/typescript/src/plugins/navigation.d.ts +29 -0
- package/lib/typescript/src/plugins/network.d.ts +23 -0
- package/lib/typescript/src/plugins/orderSpans.d.ts +3 -0
- package/lib/typescript/src/plugins/performance.d.ts +13 -0
- package/lib/typescript/src/plugins/releaseProfiler.d.ts +25 -0
- package/lib/typescript/src/plugins/runtimeMetrics.d.ts +17 -0
- package/lib/typescript/src/plugins/sentry.d.ts +15 -0
- package/lib/typescript/src/plugins/startPlugins.d.ts +4 -0
- package/lib/typescript/src/react/DetailView.d.ts +11 -0
- package/lib/typescript/src/react/ExplorerFilters.d.ts +9 -0
- package/lib/typescript/src/react/ExplorerView.d.ts +9 -0
- package/lib/typescript/src/react/InspectorControls.d.ts +229 -0
- package/lib/typescript/src/react/InspectorHeader.d.ts +8 -0
- package/lib/typescript/src/react/InspectorSheet.d.ts +16 -0
- package/lib/typescript/src/react/MetricsView.d.ts +7 -0
- package/lib/typescript/src/react/SummaryView.d.ts +11 -0
- package/lib/typescript/src/react/TimelineView.d.ts +8 -0
- package/lib/typescript/src/react/TraceInspector.d.ts +11 -0
- package/lib/typescript/src/react/TraceOverlay.d.ts +17 -0
- package/lib/typescript/src/react/explorerModel.d.ts +38 -0
- package/lib/typescript/src/react/labels.d.ts +203 -0
- package/lib/typescript/src/react/native.d.ts +42 -0
- package/lib/typescript/src/react/readSnapshot.d.ts +4 -0
- package/lib/typescript/src/react/store.d.ts +9 -0
- package/lib/typescript/src/react/topics.d.ts +80 -0
- package/lib/typescript/src/react/useAppReadyMetric.d.ts +3 -0
- package/lib/typescript/src/react/useLiveRecording.d.ts +4 -0
- package/lib/typescript/src/react/useTraceViewer.d.ts +140 -0
- package/lib/typescript/src/react/viewerModel.d.ts +59 -0
- package/lib/typescript/src/specs/Recording.nitro.d.ts +31 -0
- package/lib/typescript/src/specs/TraceSpan.nitro.d.ts +14 -0
- package/lib/typescript/src/specs/Tracing.nitro.d.ts +17 -0
- package/lib/typescript/src/types/CompletedSpanOptions.d.ts +15 -0
- package/lib/typescript/src/types/EventContext.d.ts +8 -0
- package/lib/typescript/src/types/MarkEvent.d.ts +4 -0
- package/lib/typescript/src/types/MarkOptions.d.ts +6 -0
- package/lib/typescript/src/types/MetricEvent.d.ts +8 -0
- package/lib/typescript/src/types/MetricOptions.d.ts +10 -0
- package/lib/typescript/src/types/NativeSamplingOptions.d.ts +7 -0
- package/lib/typescript/src/types/ReadOptions.d.ts +7 -0
- package/lib/typescript/src/types/RecordingOptions.d.ts +11 -0
- package/lib/typescript/src/types/RecordingStats.d.ts +25 -0
- package/lib/typescript/src/types/SpanEvent.d.ts +13 -0
- package/lib/typescript/src/types/SpanOptions.d.ts +6 -0
- package/lib/typescript/src/types/SpanOutcome.d.ts +2 -0
- package/lib/typescript/src/types/TraceAttribute.d.ts +7 -0
- package/lib/typescript/src/types/TraceAttributes.d.ts +3 -0
- package/lib/typescript/src/types/TraceContext.d.ts +10 -0
- package/lib/typescript/src/types/TracePage.d.ts +18 -0
- package/lib/typescript/src/types.d.ts +17 -0
- package/nitro.json +20 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroTracing+autolinking.cmake +83 -0
- package/nitrogen/generated/android/NitroTracing+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroTracingOnLoad.cpp +49 -0
- package/nitrogen/generated/android/NitroTracingOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrotracing/NitroTracingOnLoad.kt +35 -0
- package/nitrogen/generated/ios/NitroTracing+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/NitroTracingAutolinking.mm +35 -0
- package/nitrogen/generated/ios/NitroTracingAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/CompletedSpanOptions.hpp +122 -0
- package/nitrogen/generated/shared/c++/HybridRecordingSpec.cpp +31 -0
- package/nitrogen/generated/shared/c++/HybridRecordingSpec.hpp +100 -0
- package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/HybridTracingSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridTracingSpec.hpp +68 -0
- package/nitrogen/generated/shared/c++/MarkEvent.hpp +102 -0
- package/nitrogen/generated/shared/c++/MarkOptions.hpp +99 -0
- package/nitrogen/generated/shared/c++/MetricEvent.hpp +110 -0
- package/nitrogen/generated/shared/c++/MetricOptions.hpp +107 -0
- package/nitrogen/generated/shared/c++/NativeSamplingOptions.hpp +87 -0
- package/nitrogen/generated/shared/c++/ReadOptions.hpp +87 -0
- package/nitrogen/generated/shared/c++/RecordingOptions.hpp +95 -0
- package/nitrogen/generated/shared/c++/RecordingStats.hpp +123 -0
- package/nitrogen/generated/shared/c++/SpanEvent.hpp +121 -0
- package/nitrogen/generated/shared/c++/SpanOptions.hpp +99 -0
- package/nitrogen/generated/shared/c++/SpanOutcome.hpp +84 -0
- package/nitrogen/generated/shared/c++/TraceAttribute.hpp +87 -0
- package/nitrogen/generated/shared/c++/TracePage.hpp +111 -0
- package/package.json +236 -0
- package/performance.ts +2 -0
- package/plugins.ts +24 -0
- package/react-native.config.js +16 -0
- package/react.ts +10 -0
- package/release-profiler.ts +6 -0
- package/sentry.ts +2 -0
- package/src/client/TracingSink.ts +25 -0
- package/src/client/createTraceClient.ts +439 -0
- package/src/client/playground.ts +143 -0
- package/src/expo/ExpoAdapter.ts +71 -0
- package/src/expo/index.ts +2 -0
- package/src/index.ts +8 -0
- package/src/plugins/TracePlugin.ts +22 -0
- package/src/plugins/errors.ts +69 -0
- package/src/plugins/nativeMetrics.ts +26 -0
- package/src/plugins/navigation.ts +111 -0
- package/src/plugins/network.ts +178 -0
- package/src/plugins/orderSpans.ts +22 -0
- package/src/plugins/performance.ts +120 -0
- package/src/plugins/releaseProfiler.ts +123 -0
- package/src/plugins/runtimeMetrics.ts +131 -0
- package/src/plugins/sentry.ts +176 -0
- package/src/plugins/startPlugins.ts +58 -0
- package/src/react/DetailView.tsx +514 -0
- package/src/react/ExplorerFilters.tsx +151 -0
- package/src/react/ExplorerView.tsx +243 -0
- package/src/react/InspectorControls.tsx +651 -0
- package/src/react/InspectorHeader.tsx +171 -0
- package/src/react/InspectorSheet.tsx +89 -0
- package/src/react/MetricsView.tsx +182 -0
- package/src/react/SummaryView.tsx +121 -0
- package/src/react/TimelineView.tsx +87 -0
- package/src/react/TraceInspector.tsx +196 -0
- package/src/react/TraceOverlay.tsx +585 -0
- package/src/react/explorerModel.ts +285 -0
- package/src/react/labels.ts +234 -0
- package/src/react/native.tsx +159 -0
- package/src/react/readSnapshot.ts +58 -0
- package/src/react/store.ts +31 -0
- package/src/react/topics.ts +427 -0
- package/src/react/useAppReadyMetric.ts +10 -0
- package/src/react/useLiveRecording.ts +43 -0
- package/src/react/useTraceViewer.ts +333 -0
- package/src/react/viewerModel.ts +172 -0
- package/src/specs/Recording.nitro.ts +40 -0
- package/src/specs/TraceSpan.nitro.ts +14 -0
- package/src/specs/Tracing.nitro.ts +14 -0
- package/src/types/CompletedSpanOptions.ts +15 -0
- package/src/types/EventContext.ts +8 -0
- package/src/types/MarkEvent.ts +3 -0
- package/src/types/MarkOptions.ts +6 -0
- package/src/types/MetricEvent.ts +8 -0
- package/src/types/MetricOptions.ts +10 -0
- package/src/types/NativeSamplingOptions.ts +7 -0
- package/src/types/ReadOptions.ts +7 -0
- package/src/types/RecordingOptions.ts +11 -0
- package/src/types/RecordingStats.ts +25 -0
- package/src/types/SpanEvent.ts +13 -0
- package/src/types/SpanOptions.ts +6 -0
- package/src/types/SpanOutcome.ts +2 -0
- package/src/types/TraceAttribute.ts +7 -0
- package/src/types/TraceAttributes.ts +4 -0
- package/src/types/TraceContext.ts +10 -0
- package/src/types/TracePage.ts +18 -0
- 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
|
+
}
|