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,58 @@
|
|
|
1
|
+
import type { Recording } from '../specs/Recording.nitro'
|
|
2
|
+
import type { TracePage } from '../types'
|
|
3
|
+
/** Bound work to retained history at read start, not an arbitrary UI result limit. */
|
|
4
|
+
export async function readSnapshot(
|
|
5
|
+
recording: Recording,
|
|
6
|
+
previous?: TracePage
|
|
7
|
+
): Promise<TracePage> {
|
|
8
|
+
const page: TracePage = {
|
|
9
|
+
spans: [],
|
|
10
|
+
marks: [],
|
|
11
|
+
metrics: [],
|
|
12
|
+
nextSequence: previous?.nextSequence ?? 0,
|
|
13
|
+
earliestSequence: 0,
|
|
14
|
+
droppedEvents: 0,
|
|
15
|
+
}
|
|
16
|
+
const budget = Math.max(1, Math.ceil(recording.getStats().eventCount / 1000))
|
|
17
|
+
for (let i = 0; i < budget; i++) {
|
|
18
|
+
const batch = await recording.readEvents({
|
|
19
|
+
afterSequence: page.nextSequence,
|
|
20
|
+
limit: 1000,
|
|
21
|
+
})
|
|
22
|
+
const cursor = page.nextSequence
|
|
23
|
+
page.spans.push(...batch.spans)
|
|
24
|
+
page.marks.push(...batch.marks)
|
|
25
|
+
page.metrics.push(...batch.metrics)
|
|
26
|
+
page.nextSequence = batch.nextSequence
|
|
27
|
+
page.earliestSequence = batch.earliestSequence
|
|
28
|
+
page.droppedEvents = batch.droppedEvents
|
|
29
|
+
if (
|
|
30
|
+
batch.nextSequence === cursor ||
|
|
31
|
+
batch.spans.length + batch.marks.length + batch.metrics.length < 1000
|
|
32
|
+
)
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
if (!previous) return page
|
|
36
|
+
if (
|
|
37
|
+
page.nextSequence === previous.nextSequence &&
|
|
38
|
+
page.earliestSequence === previous.earliestSequence &&
|
|
39
|
+
page.droppedEvents === previous.droppedEvents
|
|
40
|
+
)
|
|
41
|
+
return previous
|
|
42
|
+
const keep = <T extends { sequence: number }>(old: T[], added: T[]): T[] => {
|
|
43
|
+
const evicted = old.length > 0 && old[0].sequence < page.earliestSequence
|
|
44
|
+
if (!evicted && added.length === 0) return old
|
|
45
|
+
return [
|
|
46
|
+
...(evicted
|
|
47
|
+
? old.filter((event) => event.sequence >= page.earliestSequence)
|
|
48
|
+
: old),
|
|
49
|
+
...added,
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
...page,
|
|
54
|
+
spans: keep(previous.spans, page.spans),
|
|
55
|
+
marks: keep(previous.marks, page.marks),
|
|
56
|
+
metrics: keep(previous.metrics, page.metrics),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
/** Minimal external store so only subscribed components rerender on telemetry ticks. */
|
|
3
|
+
export interface Store<T> {
|
|
4
|
+
get(): T
|
|
5
|
+
set(patch: Partial<T>): void
|
|
6
|
+
subscribe(listener: () => void): () => void
|
|
7
|
+
}
|
|
8
|
+
export function createStore<T extends object>(initial: T): Store<T> {
|
|
9
|
+
let value = initial
|
|
10
|
+
const listeners = new Set<() => void>()
|
|
11
|
+
return {
|
|
12
|
+
get: () => value,
|
|
13
|
+
set(patch) {
|
|
14
|
+
value = { ...value, ...patch }
|
|
15
|
+
for (const listener of listeners) listener()
|
|
16
|
+
},
|
|
17
|
+
subscribe(listener) {
|
|
18
|
+
listeners.add(listener)
|
|
19
|
+
return () => {
|
|
20
|
+
listeners.delete(listener)
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Selectors must return primitives or existing references. */
|
|
26
|
+
export const useStore = <T, S>(store: Store<T>, select: (value: T) => S): S =>
|
|
27
|
+
useSyncExternalStore(
|
|
28
|
+
store.subscribe,
|
|
29
|
+
() => select(store.get()),
|
|
30
|
+
() => select(store.get())
|
|
31
|
+
)
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import type { MarkEvent, MetricEvent, SpanEvent, TracePage } from '../types'
|
|
2
|
+
|
|
3
|
+
export type TopicId =
|
|
4
|
+
| 'startup'
|
|
5
|
+
| 'screens'
|
|
6
|
+
| 'network'
|
|
7
|
+
| 'responsiveness'
|
|
8
|
+
| 'resources'
|
|
9
|
+
| 'errors'
|
|
10
|
+
| 'custom'
|
|
11
|
+
export const topicIds: TopicId[] = [
|
|
12
|
+
'startup',
|
|
13
|
+
'screens',
|
|
14
|
+
'network',
|
|
15
|
+
'responsiveness',
|
|
16
|
+
'resources',
|
|
17
|
+
'errors',
|
|
18
|
+
'custom',
|
|
19
|
+
]
|
|
20
|
+
/** Text-presentation glyphs (no color emoji) and system tints per topic. */
|
|
21
|
+
export const topicGlyph: Record<TopicId, { icon: string; color: string }> = {
|
|
22
|
+
startup: { icon: '◴', color: '#ff9500' },
|
|
23
|
+
screens: { icon: '▢', color: '#007aff' },
|
|
24
|
+
network: { icon: '⇅', color: '#34c759' },
|
|
25
|
+
responsiveness: { icon: '◷', color: '#5856d6' },
|
|
26
|
+
errors: { icon: '△', color: '#ff3b30' },
|
|
27
|
+
resources: { icon: '◫', color: '#30b0c7' },
|
|
28
|
+
custom: { icon: '•', color: '#8e8e93' },
|
|
29
|
+
}
|
|
30
|
+
/** Local triage thresholds, not production SLOs. */
|
|
31
|
+
export interface Budgets {
|
|
32
|
+
appReadyMs: number
|
|
33
|
+
screenMs: number
|
|
34
|
+
requestMs: number
|
|
35
|
+
stallMs: number
|
|
36
|
+
}
|
|
37
|
+
export const defaultBudgets: Budgets = {
|
|
38
|
+
appReadyMs: 2000,
|
|
39
|
+
screenMs: 1000,
|
|
40
|
+
requestMs: 1000,
|
|
41
|
+
stallMs: 250,
|
|
42
|
+
}
|
|
43
|
+
export type AnyEvent = SpanEvent | MarkEvent | MetricEvent
|
|
44
|
+
export type IssueKind =
|
|
45
|
+
| 'slowStartup'
|
|
46
|
+
| 'slowScreen'
|
|
47
|
+
| 'slowRequest'
|
|
48
|
+
| 'failedRequest'
|
|
49
|
+
| 'stall'
|
|
50
|
+
| 'uiStall'
|
|
51
|
+
| 'frozenFrames'
|
|
52
|
+
| 'error'
|
|
53
|
+
export interface Issue {
|
|
54
|
+
kind: IssueKind
|
|
55
|
+
topic: TopicId
|
|
56
|
+
title: string
|
|
57
|
+
valueMs?: number
|
|
58
|
+
budgetMs?: number
|
|
59
|
+
event: AnyEvent
|
|
60
|
+
/** Errors outrank budget overruns; overruns rank by value / budget. */
|
|
61
|
+
severity: number
|
|
62
|
+
}
|
|
63
|
+
export interface TopicSummary {
|
|
64
|
+
id: TopicId
|
|
65
|
+
/** A collector is configured or the topic has events. */
|
|
66
|
+
tracked: boolean
|
|
67
|
+
count: number
|
|
68
|
+
/** Nearest-rank p95 (screens: transition, network: duration) or latest app-ready. */
|
|
69
|
+
value?: number
|
|
70
|
+
/** Extra headline values, e.g. resources: latest cpu and memory. */
|
|
71
|
+
values?: Record<string, string>
|
|
72
|
+
issues: Issue[]
|
|
73
|
+
}
|
|
74
|
+
/** Collector plugin IDs that make a quiet topic "tracked" rather than "not tracked". */
|
|
75
|
+
const collectorsFor: Record<TopicId, string[]> = {
|
|
76
|
+
startup: [],
|
|
77
|
+
screens: ['navigation'],
|
|
78
|
+
network: ['network', 'sentry'],
|
|
79
|
+
responsiveness: ['runtime-metrics', 'native-metrics', 'sentry'],
|
|
80
|
+
resources: ['native-metrics'],
|
|
81
|
+
errors: ['errors'],
|
|
82
|
+
custom: [],
|
|
83
|
+
}
|
|
84
|
+
export const attribute = (event: AnyEvent, key: string) =>
|
|
85
|
+
event.attributes.find((a) => a.key === key)?.value
|
|
86
|
+
const isSpan = (event: AnyEvent): event is SpanEvent => 'spanId' in event
|
|
87
|
+
const isMetric = (event: AnyEvent): event is MetricEvent => 'unit' in event
|
|
88
|
+
|
|
89
|
+
export const topicOf = (event: AnyEvent): TopicId => {
|
|
90
|
+
const source = attribute(event, 'source')
|
|
91
|
+
if (
|
|
92
|
+
event.name.startsWith('app.ready.') ||
|
|
93
|
+
event.name.startsWith('app.start.')
|
|
94
|
+
)
|
|
95
|
+
return 'startup'
|
|
96
|
+
if (event.name.startsWith('process.')) return 'resources'
|
|
97
|
+
if (event.name.startsWith('ui.')) return 'responsiveness'
|
|
98
|
+
if (source === 'navigation') return 'screens'
|
|
99
|
+
if (source === 'network') return 'network'
|
|
100
|
+
if (source === 'error') return 'errors'
|
|
101
|
+
if (source === 'js-runtime' || event.name === 'js.longtask')
|
|
102
|
+
return 'responsiveness'
|
|
103
|
+
return 'custom'
|
|
104
|
+
}
|
|
105
|
+
const p95 = (values: number[]) => {
|
|
106
|
+
if (!values.length) return undefined
|
|
107
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
108
|
+
return sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)]
|
|
109
|
+
}
|
|
110
|
+
const over = (
|
|
111
|
+
kind: IssueKind,
|
|
112
|
+
topic: TopicId,
|
|
113
|
+
title: string,
|
|
114
|
+
event: AnyEvent,
|
|
115
|
+
valueMs: number,
|
|
116
|
+
budgetMs: number
|
|
117
|
+
): Issue | undefined =>
|
|
118
|
+
valueMs > budgetMs
|
|
119
|
+
? {
|
|
120
|
+
kind,
|
|
121
|
+
topic,
|
|
122
|
+
title,
|
|
123
|
+
event,
|
|
124
|
+
valueMs,
|
|
125
|
+
budgetMs,
|
|
126
|
+
severity: valueMs / budgetMs,
|
|
127
|
+
}
|
|
128
|
+
: undefined
|
|
129
|
+
|
|
130
|
+
export const summarizeTopics = (
|
|
131
|
+
page: TracePage,
|
|
132
|
+
collectors: readonly string[],
|
|
133
|
+
budgets: Budgets = defaultBudgets
|
|
134
|
+
): { topics: TopicSummary[]; issues: Issue[] } => {
|
|
135
|
+
const byTopic = new Map<TopicId, AnyEvent[]>(topicIds.map((id) => [id, []]))
|
|
136
|
+
for (const event of [...page.spans, ...page.marks, ...page.metrics])
|
|
137
|
+
byTopic.get(topicOf(event))!.push(event)
|
|
138
|
+
const issues: Issue[] = []
|
|
139
|
+
const add = (issue: Issue | undefined) => issue && issues.push(issue)
|
|
140
|
+
for (const event of [...page.spans, ...page.marks, ...page.metrics]) {
|
|
141
|
+
const topic = topicOf(event)
|
|
142
|
+
if (isMetric(event)) {
|
|
143
|
+
if (topic === 'startup')
|
|
144
|
+
add(
|
|
145
|
+
over(
|
|
146
|
+
'slowStartup',
|
|
147
|
+
topic,
|
|
148
|
+
event.name,
|
|
149
|
+
event,
|
|
150
|
+
event.value,
|
|
151
|
+
budgets.appReadyMs
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
else if (event.name === 'navigation.transition')
|
|
155
|
+
add(
|
|
156
|
+
over(
|
|
157
|
+
'slowScreen',
|
|
158
|
+
topic,
|
|
159
|
+
attribute(event, 'screen') ?? event.name,
|
|
160
|
+
event,
|
|
161
|
+
event.value,
|
|
162
|
+
budgets.screenMs
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
else if (event.name === 'ui.frame_gap.max')
|
|
166
|
+
add(
|
|
167
|
+
over(
|
|
168
|
+
'uiStall',
|
|
169
|
+
topic,
|
|
170
|
+
event.name,
|
|
171
|
+
event,
|
|
172
|
+
event.value,
|
|
173
|
+
budgets.stallMs
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
else if (event.name === 'ui.frames.frozen' && event.value > 0)
|
|
177
|
+
add({
|
|
178
|
+
kind: 'frozenFrames',
|
|
179
|
+
topic,
|
|
180
|
+
title: `${event.value} frozen frames`,
|
|
181
|
+
event,
|
|
182
|
+
severity: 900,
|
|
183
|
+
})
|
|
184
|
+
else if (
|
|
185
|
+
event.name === 'js.frame_gap.max' ||
|
|
186
|
+
event.name === 'js.event_loop.delay'
|
|
187
|
+
)
|
|
188
|
+
add(
|
|
189
|
+
over('stall', topic, event.name, event, event.value, budgets.stallMs)
|
|
190
|
+
)
|
|
191
|
+
} else if (isSpan(event)) {
|
|
192
|
+
if (topic === 'network')
|
|
193
|
+
add(
|
|
194
|
+
event.outcome === 'error'
|
|
195
|
+
? {
|
|
196
|
+
kind: 'failedRequest',
|
|
197
|
+
topic,
|
|
198
|
+
title: event.name,
|
|
199
|
+
event,
|
|
200
|
+
severity: 1000,
|
|
201
|
+
}
|
|
202
|
+
: over(
|
|
203
|
+
'slowRequest',
|
|
204
|
+
topic,
|
|
205
|
+
event.name,
|
|
206
|
+
event,
|
|
207
|
+
event.durationMs,
|
|
208
|
+
budgets.requestMs
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
else if (event.name === 'js.longtask')
|
|
212
|
+
add(
|
|
213
|
+
over(
|
|
214
|
+
'stall',
|
|
215
|
+
topic,
|
|
216
|
+
event.name,
|
|
217
|
+
event,
|
|
218
|
+
event.durationMs,
|
|
219
|
+
budgets.stallMs
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
else if (event.outcome === 'error')
|
|
223
|
+
add({
|
|
224
|
+
kind: 'error',
|
|
225
|
+
topic: 'errors',
|
|
226
|
+
title: event.name,
|
|
227
|
+
event,
|
|
228
|
+
severity: 1000,
|
|
229
|
+
})
|
|
230
|
+
} else if (topic === 'errors')
|
|
231
|
+
add({
|
|
232
|
+
kind: 'error',
|
|
233
|
+
topic,
|
|
234
|
+
title: attribute(event, 'message') ?? event.name,
|
|
235
|
+
event,
|
|
236
|
+
severity: 1000,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
issues.sort(
|
|
240
|
+
(a, b) =>
|
|
241
|
+
b.severity - a.severity || b.event.timestampMs - a.event.timestampMs
|
|
242
|
+
)
|
|
243
|
+
const topics = topicIds.map((id): TopicSummary => {
|
|
244
|
+
const events = byTopic.get(id)!
|
|
245
|
+
const own = issues.filter((issue) => issue.topic === id)
|
|
246
|
+
const metricValues = (name: string) =>
|
|
247
|
+
events
|
|
248
|
+
.filter((e): e is MetricEvent => isMetric(e) && e.name === name)
|
|
249
|
+
.map((e) => e.value)
|
|
250
|
+
const spans = events.filter(isSpan)
|
|
251
|
+
const summary = {
|
|
252
|
+
id,
|
|
253
|
+
tracked:
|
|
254
|
+
id === 'custom' ||
|
|
255
|
+
events.length > 0 ||
|
|
256
|
+
collectorsFor[id].some((collector) => collectors.includes(collector)),
|
|
257
|
+
issues: own,
|
|
258
|
+
}
|
|
259
|
+
switch (id) {
|
|
260
|
+
case 'startup':
|
|
261
|
+
return {
|
|
262
|
+
...summary,
|
|
263
|
+
count: events.length,
|
|
264
|
+
value: events.filter(isMetric).at(-1)?.value,
|
|
265
|
+
}
|
|
266
|
+
case 'screens':
|
|
267
|
+
return {
|
|
268
|
+
...summary,
|
|
269
|
+
count: events.filter((e) => e.name === 'navigation.enter').length,
|
|
270
|
+
value: p95(metricValues('navigation.transition')),
|
|
271
|
+
}
|
|
272
|
+
case 'network':
|
|
273
|
+
return {
|
|
274
|
+
...summary,
|
|
275
|
+
count: spans.length,
|
|
276
|
+
value: p95(spans.map((s) => s.durationMs)),
|
|
277
|
+
}
|
|
278
|
+
case 'resources': {
|
|
279
|
+
const latest = (name: string) => {
|
|
280
|
+
const values = metricValues(name)
|
|
281
|
+
return values.length ? values[values.length - 1].toFixed(0) : '—'
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
...summary,
|
|
285
|
+
count: events.length,
|
|
286
|
+
values: {
|
|
287
|
+
cpu: latest('process.cpu'),
|
|
288
|
+
memory: latest('process.memory'),
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
case 'responsiveness':
|
|
293
|
+
case 'errors':
|
|
294
|
+
return { ...summary, count: own.length }
|
|
295
|
+
default:
|
|
296
|
+
return { ...summary, count: spans.length }
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
return { topics, issues }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export type TimelineItem =
|
|
303
|
+
| { kind: 'screen'; key: string; name: string; timestampMs: number }
|
|
304
|
+
| {
|
|
305
|
+
kind: 'event'
|
|
306
|
+
key: string
|
|
307
|
+
event: AnyEvent
|
|
308
|
+
topic: TopicId
|
|
309
|
+
issue: boolean
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Newest screen visit first, each followed by its events newest first. Visits come from
|
|
313
|
+
* `navigation.enter` marks, so the active screen appears before its visit span completes.
|
|
314
|
+
* Metrics are listed only when they are issues; periodic samples belong in Metrics.
|
|
315
|
+
*/
|
|
316
|
+
export const buildTimeline = (
|
|
317
|
+
page: TracePage,
|
|
318
|
+
issues: readonly Issue[],
|
|
319
|
+
limit = 500
|
|
320
|
+
): TimelineItem[] => {
|
|
321
|
+
const flagged = new Set<AnyEvent>(issues.map((issue) => issue.event))
|
|
322
|
+
const visits = page.marks
|
|
323
|
+
.filter(
|
|
324
|
+
(m) =>
|
|
325
|
+
m.name === 'navigation.enter' && attribute(m, 'source') === 'navigation'
|
|
326
|
+
)
|
|
327
|
+
.sort((a, b) => a.timestampMs - b.timestampMs)
|
|
328
|
+
const visitSet = new Set<AnyEvent>(visits)
|
|
329
|
+
const events = [
|
|
330
|
+
...page.spans.filter((s) => attribute(s, 'source') !== 'navigation'),
|
|
331
|
+
...page.marks.filter((m) => !visitSet.has(m)),
|
|
332
|
+
...page.metrics.filter((m) => flagged.has(m)),
|
|
333
|
+
]
|
|
334
|
+
.sort((a, b) => b.timestampMs - a.timestampMs || b.sequence - a.sequence)
|
|
335
|
+
.slice(0, limit)
|
|
336
|
+
const groups = new Map<number, AnyEvent[]>()
|
|
337
|
+
for (const event of events) {
|
|
338
|
+
// ponytail: linear scan per event; binary search if visit counts grow large.
|
|
339
|
+
let index = -1
|
|
340
|
+
for (let i = visits.length - 1; i >= 0; i--)
|
|
341
|
+
if (visits[i].timestampMs <= event.timestampMs) {
|
|
342
|
+
index = i
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
const group = groups.get(index) ?? []
|
|
346
|
+
group.push(event)
|
|
347
|
+
groups.set(index, group)
|
|
348
|
+
}
|
|
349
|
+
const items: TimelineItem[] = []
|
|
350
|
+
for (let index = visits.length - 1; index >= -1; index--) {
|
|
351
|
+
const group = groups.get(index) ?? []
|
|
352
|
+
const visit = visits[index]
|
|
353
|
+
if (!visit && !group.length) continue
|
|
354
|
+
if (visits.length)
|
|
355
|
+
items.push({
|
|
356
|
+
kind: 'screen',
|
|
357
|
+
key: visit ? `screen:${visit.sequence}` : 'screen:before',
|
|
358
|
+
name: visit ? (attribute(visit, 'screen') ?? '') : '',
|
|
359
|
+
timestampMs: visit?.timestampMs ?? 0,
|
|
360
|
+
})
|
|
361
|
+
for (const event of group)
|
|
362
|
+
items.push({
|
|
363
|
+
kind: 'event',
|
|
364
|
+
key: `${'spanId' in event ? event.spanId : 'unit' in event ? 'metric' : 'mark'}:${event.sequence}`,
|
|
365
|
+
event,
|
|
366
|
+
topic: topicOf(event),
|
|
367
|
+
issue: flagged.has(event),
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
return items
|
|
371
|
+
}
|
|
372
|
+
export const currentScreen = (page: TracePage) => {
|
|
373
|
+
let latest: MarkEvent | undefined
|
|
374
|
+
for (const mark of page.marks)
|
|
375
|
+
if (
|
|
376
|
+
mark.name === 'navigation.enter' &&
|
|
377
|
+
(!latest || mark.timestampMs >= latest.timestampMs)
|
|
378
|
+
)
|
|
379
|
+
latest = mark
|
|
380
|
+
return latest && attribute(latest, 'screen')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface VisitSummary {
|
|
384
|
+
/** Undefined when no navigation collector has recorded a screen yet. */
|
|
385
|
+
screen?: string
|
|
386
|
+
sinceMs?: number
|
|
387
|
+
events: Extract<TimelineItem, { kind: 'event' }>[]
|
|
388
|
+
requests: number
|
|
389
|
+
stalls: number
|
|
390
|
+
errors: number
|
|
391
|
+
issues: number
|
|
392
|
+
}
|
|
393
|
+
/** The newest screen visit in a timeline: what happened on the screen the user is looking at. */
|
|
394
|
+
export const currentVisit = (items: readonly TimelineItem[]): VisitSummary => {
|
|
395
|
+
const start = items.findIndex((item) => item.kind === 'screen')
|
|
396
|
+
const header = start >= 0 ? items[start] : undefined
|
|
397
|
+
const next = items.findIndex((item, i) => i > start && item.kind === 'screen')
|
|
398
|
+
const events = items
|
|
399
|
+
.slice(start + 1, start >= 0 && next >= 0 ? next : undefined)
|
|
400
|
+
.filter((item) => item.kind === 'event')
|
|
401
|
+
const count = (test: (item: (typeof events)[number]) => boolean) =>
|
|
402
|
+
events.filter(test).length
|
|
403
|
+
return {
|
|
404
|
+
screen: header?.kind === 'screen' ? header.name : undefined,
|
|
405
|
+
sinceMs: header?.kind === 'screen' ? header.timestampMs : undefined,
|
|
406
|
+
events,
|
|
407
|
+
requests: count((item) => item.topic === 'network'),
|
|
408
|
+
stalls: count((item) => item.topic === 'responsiveness' && item.issue),
|
|
409
|
+
errors: count(
|
|
410
|
+
(item) =>
|
|
411
|
+
item.topic === 'errors' ||
|
|
412
|
+
('outcome' in item.event && item.event.outcome === 'error')
|
|
413
|
+
),
|
|
414
|
+
issues: count((item) => item.issue),
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Latest sample of a metric by name; undefined when not collected. */
|
|
418
|
+
export const latestMetric = (page: TracePage, name: string) => {
|
|
419
|
+
let latest: MetricEvent | undefined
|
|
420
|
+
for (const metric of page.metrics)
|
|
421
|
+
if (
|
|
422
|
+
metric.name === name &&
|
|
423
|
+
(!latest || metric.timestampMs >= latest.timestampMs)
|
|
424
|
+
)
|
|
425
|
+
latest = metric
|
|
426
|
+
return latest?.value
|
|
427
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import type { TraceClient } from '../client/createTraceClient'
|
|
3
|
+
/** Report the consumer's ready signal after a frame opportunity. Not native launch time or guaranteed TTI. */
|
|
4
|
+
export function useAppReadyMetric(client: TraceClient, ready: boolean) {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!ready) return
|
|
7
|
+
const frame = requestAnimationFrame(() => client.reportAppReady())
|
|
8
|
+
return () => cancelAnimationFrame(frame)
|
|
9
|
+
}, [client, ready])
|
|
10
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import type { TraceClient } from '../client/createTraceClient'
|
|
3
|
+
import type { Recording } from '../specs/Recording.nitro'
|
|
4
|
+
import type { TracePage } from '../types'
|
|
5
|
+
import { readSnapshot } from './readSnapshot'
|
|
6
|
+
/** Incremental polling of the active recording; unchanged snapshots keep their identity. */
|
|
7
|
+
export function useLiveRecording(
|
|
8
|
+
client: TraceClient,
|
|
9
|
+
active: boolean,
|
|
10
|
+
intervalMs = 1000
|
|
11
|
+
) {
|
|
12
|
+
const [page, setPage] = useState<TracePage>()
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!active) return
|
|
15
|
+
let cancelled = false
|
|
16
|
+
let timer: ReturnType<typeof setTimeout>
|
|
17
|
+
let cached: TracePage | undefined
|
|
18
|
+
let cachedRecording: Recording | undefined
|
|
19
|
+
const tick = async () => {
|
|
20
|
+
try {
|
|
21
|
+
const recording = client.getRecording()
|
|
22
|
+
if (recording) {
|
|
23
|
+
if (recording !== cachedRecording) {
|
|
24
|
+
cached = undefined
|
|
25
|
+
cachedRecording = recording
|
|
26
|
+
}
|
|
27
|
+
cached = await readSnapshot(recording, cached)
|
|
28
|
+
if (!cancelled) setPage(cached)
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
client.reportError(error)
|
|
32
|
+
} finally {
|
|
33
|
+
if (!cancelled) timer = setTimeout(tick, intervalMs)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
void tick()
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true
|
|
39
|
+
clearTimeout(timer)
|
|
40
|
+
}
|
|
41
|
+
}, [client, active, intervalMs])
|
|
42
|
+
return page
|
|
43
|
+
}
|