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,285 @@
|
|
|
1
|
+
import type { MarkEvent, SpanEvent, TracePage } from '../types'
|
|
2
|
+
|
|
3
|
+
export type ExplorerMode = 'traces' | 'spans' | 'marks'
|
|
4
|
+
export type Sort =
|
|
5
|
+
'newest' | 'oldest' | 'longest' | 'shortest' | 'errors' | 'count' | 'name'
|
|
6
|
+
export interface Query {
|
|
7
|
+
search: string
|
|
8
|
+
outcomes: string[]
|
|
9
|
+
sources: string[]
|
|
10
|
+
minDuration: string
|
|
11
|
+
from: string
|
|
12
|
+
to: string
|
|
13
|
+
correlation: string
|
|
14
|
+
sort: Sort
|
|
15
|
+
}
|
|
16
|
+
export const defaultQuery = (): Query => ({
|
|
17
|
+
search: '',
|
|
18
|
+
outcomes: [],
|
|
19
|
+
sources: [],
|
|
20
|
+
minDuration: '',
|
|
21
|
+
from: '',
|
|
22
|
+
to: '',
|
|
23
|
+
correlation: '',
|
|
24
|
+
sort: 'newest',
|
|
25
|
+
})
|
|
26
|
+
export type Event = SpanEvent | MarkEvent
|
|
27
|
+
export interface Trace {
|
|
28
|
+
id: string
|
|
29
|
+
name: string
|
|
30
|
+
start: number
|
|
31
|
+
duration: number
|
|
32
|
+
errors: number
|
|
33
|
+
spans: SpanEvent[]
|
|
34
|
+
marks: MarkEvent[]
|
|
35
|
+
sources: string[]
|
|
36
|
+
}
|
|
37
|
+
const isSpan = (event: Event): event is SpanEvent => 'spanId' in event
|
|
38
|
+
export const sourceOf = (event: Event): string =>
|
|
39
|
+
event.attributes.find((a) => a.key === 'source')?.value || 'unknown'
|
|
40
|
+
const compareText = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
|
41
|
+
const identity = (event: Event) =>
|
|
42
|
+
isSpan(event) ? event.spanId : `mark:${event.sequence}`
|
|
43
|
+
const tie = (a: Event, b: Event) =>
|
|
44
|
+
compareText(identity(a), identity(b)) || a.sequence - b.sequence
|
|
45
|
+
const chronological = (a: Event, b: Event) =>
|
|
46
|
+
a.timestampMs - b.timestampMs || tie(a, b)
|
|
47
|
+
const durationOf = (event: Event) => (isSpan(event) ? event.durationMs : 0)
|
|
48
|
+
const endOf = (event: Event) => event.timestampMs + durationOf(event)
|
|
49
|
+
const numeric = (value: string): number | undefined =>
|
|
50
|
+
value.trim() && Number.isFinite(Number(value)) ? Number(value) : undefined
|
|
51
|
+
const overlaps = (start: number, duration: number, query: Query) => {
|
|
52
|
+
const from = numeric(query.from)
|
|
53
|
+
const to = numeric(query.to)
|
|
54
|
+
return (
|
|
55
|
+
(from === undefined || start + duration >= from) &&
|
|
56
|
+
(to === undefined || start <= to)
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
export const matchesSearch = (event: Event, search: string) =>
|
|
60
|
+
!search ||
|
|
61
|
+
[
|
|
62
|
+
event.name,
|
|
63
|
+
event.correlationId,
|
|
64
|
+
isSpan(event) ? event.spanId : '',
|
|
65
|
+
...event.attributes.flatMap((a) => [a.key, a.value]),
|
|
66
|
+
].some((value) => value.toLowerCase().includes(search.toLowerCase()))
|
|
67
|
+
|
|
68
|
+
/** A deterministic forest: missing parents are roots and cycles are cut once. */
|
|
69
|
+
const forest = (spans: SpanEvent[]) => {
|
|
70
|
+
const sorted = [...spans].sort(chronological)
|
|
71
|
+
const byId = new Map(sorted.map((span) => [span.spanId, span]))
|
|
72
|
+
const children = new Map<SpanEvent, SpanEvent[]>()
|
|
73
|
+
const roots: SpanEvent[] = []
|
|
74
|
+
for (const span of sorted) {
|
|
75
|
+
const parent = byId.get(span.parentSpanId)
|
|
76
|
+
if (!parent || parent === span) roots.push(span)
|
|
77
|
+
else {
|
|
78
|
+
const siblings = children.get(parent) ?? []
|
|
79
|
+
siblings.push(span)
|
|
80
|
+
children.set(parent, siblings)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const visited = new Set<SpanEvent>()
|
|
84
|
+
const rows: Array<{
|
|
85
|
+
span: SpanEvent
|
|
86
|
+
depth: number
|
|
87
|
+
root: SpanEvent
|
|
88
|
+
parent?: SpanEvent
|
|
89
|
+
children: number
|
|
90
|
+
}> = []
|
|
91
|
+
const rowBySpan = new Map<SpanEvent, (typeof rows)[number]>()
|
|
92
|
+
for (const root of [...roots, ...sorted]) {
|
|
93
|
+
if (visited.has(root)) continue
|
|
94
|
+
const stack: Array<{ span: SpanEvent; depth: number; parent?: SpanEvent }> =
|
|
95
|
+
[{ span: root, depth: 0 }]
|
|
96
|
+
while (stack.length) {
|
|
97
|
+
const next = stack.pop()!
|
|
98
|
+
if (visited.has(next.span)) continue
|
|
99
|
+
visited.add(next.span)
|
|
100
|
+
const row = { ...next, root, children: 0 }
|
|
101
|
+
rows.push(row)
|
|
102
|
+
rowBySpan.set(next.span, row)
|
|
103
|
+
const descendants = children.get(next.span) ?? []
|
|
104
|
+
for (let i = descendants.length - 1; i >= 0; i--)
|
|
105
|
+
stack.push({
|
|
106
|
+
span: descendants[i],
|
|
107
|
+
depth: next.depth + 1,
|
|
108
|
+
parent: next.span,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (let i = rows.length - 1; i >= 0; i--) {
|
|
113
|
+
const row = rows[i]
|
|
114
|
+
const parent = row.parent && rowBySpan.get(row.parent)
|
|
115
|
+
if (parent) parent.children += row.children + 1
|
|
116
|
+
}
|
|
117
|
+
return rows
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const buildTraces = (page: TracePage): Trace[] => {
|
|
121
|
+
const groups = new Map<string, { id: string; events: Event[] }>()
|
|
122
|
+
for (const event of [...page.spans, ...page.marks]) {
|
|
123
|
+
if (!event.correlationId) continue
|
|
124
|
+
const key = `correlation:${event.correlationId}`
|
|
125
|
+
const group = groups.get(key) ?? { id: key, events: [] }
|
|
126
|
+
group.events.push(event)
|
|
127
|
+
groups.set(key, group)
|
|
128
|
+
}
|
|
129
|
+
for (const row of forest(page.spans.filter((span) => !span.correlationId))) {
|
|
130
|
+
const key = `span:${row.root.spanId}`
|
|
131
|
+
const group = groups.get(key) ?? { id: key, events: [] }
|
|
132
|
+
group.events.push(row.span)
|
|
133
|
+
groups.set(key, group)
|
|
134
|
+
}
|
|
135
|
+
return [...groups.values()]
|
|
136
|
+
.map(({ id, events }) => {
|
|
137
|
+
events.sort(chronological)
|
|
138
|
+
const spans = events.filter(isSpan)
|
|
139
|
+
const start = events[0].timestampMs
|
|
140
|
+
const end = events.reduce(
|
|
141
|
+
(latest, event) => Math.max(latest, endOf(event)),
|
|
142
|
+
start
|
|
143
|
+
)
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
name: events[0].name,
|
|
147
|
+
start,
|
|
148
|
+
duration: end - start,
|
|
149
|
+
errors: spans.filter((s) => s.outcome === 'error').length,
|
|
150
|
+
spans,
|
|
151
|
+
marks: events.filter((e) => !isSpan(e)),
|
|
152
|
+
sources: [...new Set(events.map(sourceOf))].sort(compareText),
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.sort((a, b) => b.start - a.start || compareText(a.id, b.id))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const queryEvents = (events: Event[], query: Query): Event[] => {
|
|
159
|
+
const search = query.search.toLowerCase()
|
|
160
|
+
const minimum = numeric(query.minDuration)
|
|
161
|
+
return events
|
|
162
|
+
.filter(
|
|
163
|
+
(event) =>
|
|
164
|
+
matchesSearch(event, search) &&
|
|
165
|
+
(!query.correlation || event.correlationId === query.correlation) &&
|
|
166
|
+
(!query.sources.length || query.sources.includes(sourceOf(event))) &&
|
|
167
|
+
(!isSpan(event) ||
|
|
168
|
+
!query.outcomes.length ||
|
|
169
|
+
query.outcomes.includes(event.outcome)) &&
|
|
170
|
+
(!isSpan(event) ||
|
|
171
|
+
minimum === undefined ||
|
|
172
|
+
event.durationMs >= minimum) &&
|
|
173
|
+
overlaps(event.timestampMs, durationOf(event), query)
|
|
174
|
+
)
|
|
175
|
+
.sort((a, b) => {
|
|
176
|
+
let order = 0
|
|
177
|
+
switch (query.sort) {
|
|
178
|
+
case 'oldest':
|
|
179
|
+
order = a.timestampMs - b.timestampMs
|
|
180
|
+
break
|
|
181
|
+
case 'longest':
|
|
182
|
+
order = durationOf(b) - durationOf(a)
|
|
183
|
+
break
|
|
184
|
+
case 'shortest':
|
|
185
|
+
order = durationOf(a) - durationOf(b)
|
|
186
|
+
break
|
|
187
|
+
case 'name':
|
|
188
|
+
order = compareText(a.name, b.name)
|
|
189
|
+
break
|
|
190
|
+
case 'errors':
|
|
191
|
+
order =
|
|
192
|
+
Number(isSpan(b) && b.outcome === 'error') -
|
|
193
|
+
Number(isSpan(a) && a.outcome === 'error')
|
|
194
|
+
break
|
|
195
|
+
default:
|
|
196
|
+
order = b.timestampMs - a.timestampMs
|
|
197
|
+
}
|
|
198
|
+
return order || tie(a, b)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const queryTraces = (traces: Trace[], query: Query): Trace[] => {
|
|
203
|
+
const search = query.search.toLowerCase()
|
|
204
|
+
const minimum = numeric(query.minDuration)
|
|
205
|
+
return traces
|
|
206
|
+
.filter((trace) => {
|
|
207
|
+
const events: Event[] = [...trace.spans, ...trace.marks]
|
|
208
|
+
return (
|
|
209
|
+
(!search || events.some((event) => matchesSearch(event, search))) &&
|
|
210
|
+
(!query.correlation ||
|
|
211
|
+
events.some((event) => event.correlationId === query.correlation)) &&
|
|
212
|
+
(!query.sources.length ||
|
|
213
|
+
trace.sources.some((source) => query.sources.includes(source))) &&
|
|
214
|
+
(!query.outcomes.length ||
|
|
215
|
+
query.outcomes.some((outcome) =>
|
|
216
|
+
outcome === 'error'
|
|
217
|
+
? trace.errors > 0
|
|
218
|
+
: (outcome === 'success' || outcome === 'no-error') &&
|
|
219
|
+
trace.errors === 0
|
|
220
|
+
)) &&
|
|
221
|
+
(minimum === undefined || trace.duration >= minimum) &&
|
|
222
|
+
overlaps(trace.start, trace.duration, query)
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
.sort((a, b) => {
|
|
226
|
+
let order = 0
|
|
227
|
+
switch (query.sort) {
|
|
228
|
+
case 'oldest':
|
|
229
|
+
order = a.start - b.start
|
|
230
|
+
break
|
|
231
|
+
case 'longest':
|
|
232
|
+
order = b.duration - a.duration
|
|
233
|
+
break
|
|
234
|
+
case 'shortest':
|
|
235
|
+
order = a.duration - b.duration
|
|
236
|
+
break
|
|
237
|
+
case 'errors':
|
|
238
|
+
order = b.errors - a.errors
|
|
239
|
+
break
|
|
240
|
+
case 'count':
|
|
241
|
+
order = b.spans.length - a.spans.length
|
|
242
|
+
break
|
|
243
|
+
case 'name':
|
|
244
|
+
order = compareText(a.name, b.name)
|
|
245
|
+
break
|
|
246
|
+
default:
|
|
247
|
+
order = b.start - a.start
|
|
248
|
+
}
|
|
249
|
+
return order || compareText(a.id, b.id)
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const hierarchyRows = (spans: SpanEvent[], collapsed: Set<string>) => {
|
|
254
|
+
const rows = forest(spans)
|
|
255
|
+
const start = spans.reduce(
|
|
256
|
+
(min, span) => Math.min(min, span.timestampMs),
|
|
257
|
+
Infinity
|
|
258
|
+
)
|
|
259
|
+
const end = spans.reduce((max, span) => Math.max(max, endOf(span)), -Infinity)
|
|
260
|
+
const duration = Math.max(1, end - start)
|
|
261
|
+
let hiddenBelow = Infinity
|
|
262
|
+
return rows
|
|
263
|
+
.filter((row) => {
|
|
264
|
+
if (row.depth > hiddenBelow) return false
|
|
265
|
+
hiddenBelow = collapsed.has(row.span.spanId) ? row.depth : Infinity
|
|
266
|
+
return true
|
|
267
|
+
})
|
|
268
|
+
.map(({ span, depth, children }) => ({
|
|
269
|
+
span,
|
|
270
|
+
depth,
|
|
271
|
+
children,
|
|
272
|
+
offset: (span.timestampMs - start) / duration,
|
|
273
|
+
width: span.durationMs / duration,
|
|
274
|
+
}))
|
|
275
|
+
}
|
|
276
|
+
export const traceForSpan = (
|
|
277
|
+
traces: Trace[],
|
|
278
|
+
span: SpanEvent
|
|
279
|
+
): Trace | undefined =>
|
|
280
|
+
traces.find((trace) =>
|
|
281
|
+
trace.spans.some(
|
|
282
|
+
(member) =>
|
|
283
|
+
member.spanId === span.spanId && member.sequence === span.sequence
|
|
284
|
+
)
|
|
285
|
+
)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
export const defaultLabels = {
|
|
2
|
+
applyFilters: 'Apply',
|
|
3
|
+
title: 'Performance',
|
|
4
|
+
searchMatch: 'Search match',
|
|
5
|
+
explore: 'Explore',
|
|
6
|
+
profiles: 'Profiles',
|
|
7
|
+
spans: 'Spans',
|
|
8
|
+
events: 'events',
|
|
9
|
+
actions: 'Actions',
|
|
10
|
+
tools: 'Tools',
|
|
11
|
+
back: 'Back',
|
|
12
|
+
done: 'Done',
|
|
13
|
+
details: 'Details',
|
|
14
|
+
cancel: 'Cancel',
|
|
15
|
+
filters: 'Filters',
|
|
16
|
+
sort: 'Sort',
|
|
17
|
+
reset: 'Reset',
|
|
18
|
+
exploreSearch: 'Search',
|
|
19
|
+
metricSearch: 'Search metrics',
|
|
20
|
+
newest: 'Newest',
|
|
21
|
+
oldest: 'Oldest',
|
|
22
|
+
longest: 'Longest',
|
|
23
|
+
shortest: 'Shortest',
|
|
24
|
+
errors: 'Most errors',
|
|
25
|
+
count: 'Most samples',
|
|
26
|
+
name: 'Name',
|
|
27
|
+
p95: 'Highest p95',
|
|
28
|
+
source: 'Source',
|
|
29
|
+
outcome: 'Outcome',
|
|
30
|
+
success: 'Success',
|
|
31
|
+
error: 'Error',
|
|
32
|
+
cancelled: 'Cancelled',
|
|
33
|
+
interrupted: 'Interrupted',
|
|
34
|
+
noErrors: 'No recorded errors',
|
|
35
|
+
minDuration: 'Minimum duration (ms)',
|
|
36
|
+
from: 'From (ms since recording)',
|
|
37
|
+
to: 'To (ms since recording)',
|
|
38
|
+
correlation: 'Exact correlation ID',
|
|
39
|
+
invalidRange: 'Enter nonnegative numbers, with From no later than To.',
|
|
40
|
+
resultCount: '{{matching}} / {{loaded}} matching',
|
|
41
|
+
traceRow: '{{spans}} spans · {{errors}} errors · {{source}}',
|
|
42
|
+
noResults:
|
|
43
|
+
'No matching events. Change filters or reset to see the retained recording.',
|
|
44
|
+
uncorrelated: 'Uncorrelated',
|
|
45
|
+
uncorrelatedHint:
|
|
46
|
+
'{{count}} uncorrelated events are also available in Spans and Marks.',
|
|
47
|
+
pauseUpdates: 'Freeze view',
|
|
48
|
+
resumeUpdates: 'Resume live view',
|
|
49
|
+
newEvents: '{{count}} new events',
|
|
50
|
+
detailFrozen:
|
|
51
|
+
'Detail is held steady while capture continues. Return to load newer events.',
|
|
52
|
+
expandAll: 'Expand all',
|
|
53
|
+
collapseAll: 'Collapse all',
|
|
54
|
+
missingParent: 'Parent is outside retained history.',
|
|
55
|
+
wholeTrace: 'Show entire trace',
|
|
56
|
+
similar: 'Find similar spans',
|
|
57
|
+
parent: 'Parent',
|
|
58
|
+
children: 'Children',
|
|
59
|
+
attributes: 'Search attributes',
|
|
60
|
+
evicted:
|
|
61
|
+
'This event is outside retained history. Showing the last inspected snapshot.',
|
|
62
|
+
all: 'All',
|
|
63
|
+
runtime: 'Runtime',
|
|
64
|
+
custom: 'Custom',
|
|
65
|
+
metricBrief: 'p95 {{p95}} · n={{count}}',
|
|
66
|
+
metricSortNotice:
|
|
67
|
+
'All retained traces. Numeric sorting compares raw values; check units when comparing series.',
|
|
68
|
+
exportAll: 'Recording JSON (stops recording)',
|
|
69
|
+
sharePerfetto: 'Perfetto trace',
|
|
70
|
+
replaceTitle: 'Replace current recording?',
|
|
71
|
+
replaceDescription:
|
|
72
|
+
'Start a new recording and discard the currently retained history. Export first to keep a copy.',
|
|
73
|
+
retainedScope:
|
|
74
|
+
'Counts and statistics cover retained history. Pause updates freezes this view, not recording or profiling.',
|
|
75
|
+
lostHistory: 'Lost {{what}}; this history is incomplete.',
|
|
76
|
+
lostSpans: '{{count}} spans',
|
|
77
|
+
lostMarks: '{{count}} marks',
|
|
78
|
+
lostMetrics: '{{count}} metric samples',
|
|
79
|
+
slowest: 'Slowest spans',
|
|
80
|
+
errorsOnly: 'Errors',
|
|
81
|
+
profileSeparate:
|
|
82
|
+
'CPU profiles are separate from trace JSON. Stop sampling to save and share the latest artifact.',
|
|
83
|
+
noProfiler: 'No CPU profiler is configured for this client.',
|
|
84
|
+
|
|
85
|
+
eyebrow: 'DEVELOPER TOOLS',
|
|
86
|
+
close: 'Close',
|
|
87
|
+
recording: 'Recording live',
|
|
88
|
+
stopped: 'Recording stopped',
|
|
89
|
+
counters:
|
|
90
|
+
'{{events}} events \u00b7 {{active}} active \u00b7 {{dropped}} dropped \u00b7 {{bytes}} bytes',
|
|
91
|
+
start: 'Start recording',
|
|
92
|
+
stop: 'Stop recording',
|
|
93
|
+
clear: 'Clear',
|
|
94
|
+
export: 'Stop & export',
|
|
95
|
+
retention: 'Bounded native history \u00b7 refreshes while open',
|
|
96
|
+
traces: 'Traces',
|
|
97
|
+
search: 'Search operation or correlation ID',
|
|
98
|
+
traceSummary:
|
|
99
|
+
'{{duration}} ms \u00b7 {{events}} events \u00b7 {{errors}} errors',
|
|
100
|
+
empty: 'No traces yet. Use the app to capture activity.',
|
|
101
|
+
waterfall: 'Span waterfall',
|
|
102
|
+
noSpans: 'No completed spans in this trace.',
|
|
103
|
+
spanDetails: 'Span {{id}} \u00b7 parent {{parent}} \u00b7 {{outcome}}',
|
|
104
|
+
metrics: 'Metrics',
|
|
105
|
+
operations: 'Operation outcomes',
|
|
106
|
+
operationsShort: 'Operations',
|
|
107
|
+
operationStats:
|
|
108
|
+
'{{count}} spans · {{errors}} errors · {{errorRate}} error rate · {{cancelled}} cancelled · {{interrupted}} interrupted',
|
|
109
|
+
metricsScope:
|
|
110
|
+
'All traces · app.ready.after_tracer_init measures tracer setup to app readiness, not TTI. duration: series are derived from completed spans. Statistics cover retained samples, not the whole session.',
|
|
111
|
+
emptyMetrics:
|
|
112
|
+
'No samples yet. Use the app or run the playground. App-ready timing appears after a fresh launch; TTI requires an explicit readiness definition.',
|
|
113
|
+
metricStats:
|
|
114
|
+
'Latest {{latest}} · median {{median}} · min {{min}} · max {{max}} · p95 {{p95}} · {{count}} samples',
|
|
115
|
+
marks: 'Marks',
|
|
116
|
+
playground: 'Instrumentation playground',
|
|
117
|
+
playgroundDescription:
|
|
118
|
+
'Check nested spans, concurrent operations with the same name, cancellation, repeated completion, errors and metrics.',
|
|
119
|
+
run: 'Run sample trace',
|
|
120
|
+
profiling: 'CPU profile',
|
|
121
|
+
profilingDescription:
|
|
122
|
+
'Capture a native profiler artifact alongside this recording.',
|
|
123
|
+
startProfile: 'Start CPU profile',
|
|
124
|
+
stopProfile: 'Stop CPU profile',
|
|
125
|
+
shareProfile: 'CPU profile',
|
|
126
|
+
shareArtifact: 'Share performance recording',
|
|
127
|
+
sharingUnavailable: 'File sharing is unavailable on this device.',
|
|
128
|
+
cacheUnavailable: 'The application cache is unavailable.',
|
|
129
|
+
truncated:
|
|
130
|
+
'Showing up to 100 traces, spans and marks, and 40 metric series. Search to narrow traces; export includes all retained events.',
|
|
131
|
+
overview: 'Summary',
|
|
132
|
+
tabs: 'Inspector views',
|
|
133
|
+
recordingSummary: 'Recording summary',
|
|
134
|
+
summary:
|
|
135
|
+
'{{traces}} traces · {{spans}} spans · {{marks}} marks · {{metrics}} metric samples loaded',
|
|
136
|
+
session: 'Session {{id}} · elapsed {{elapsed}} ms',
|
|
137
|
+
metricWindow:
|
|
138
|
+
'Showing {{displayed}} / {{retained}} loaded samples · {{start}}–{{end}} ms since recording start',
|
|
139
|
+
passed: 'PASS',
|
|
140
|
+
failed: 'FAIL',
|
|
141
|
+
scenarioResults: 'Scenario results',
|
|
142
|
+
scenarioNotice:
|
|
143
|
+
'Checks inspect retained native events; dropped or expired samples can fail a check.',
|
|
144
|
+
scenarioNested: 'Explicit parent hierarchy',
|
|
145
|
+
scenarioConcurrency: 'Overlapping spans with the same name',
|
|
146
|
+
scenarioCancelled: 'Cancelled outcome',
|
|
147
|
+
scenarioIdempotent: 'Repeated completion records once',
|
|
148
|
+
scenarioError: 'Error outcome',
|
|
149
|
+
scenarioMetrics: 'Native metric values',
|
|
150
|
+
openScenario: 'Open scenario trace',
|
|
151
|
+
timeline: 'Timeline',
|
|
152
|
+
issues: 'Issues: {{count}}',
|
|
153
|
+
noIssues: 'Nothing over budget in retained history.',
|
|
154
|
+
summaryScope:
|
|
155
|
+
'Issues compare retained events with local budgets, not production percentiles. Tap a topic to explore it.',
|
|
156
|
+
moreIssues: '{{count}} more in Explore',
|
|
157
|
+
issueValue: '{{value}} ms · budget {{budget}} ms',
|
|
158
|
+
notTracked: 'Not tracked',
|
|
159
|
+
quiet: 'No events yet',
|
|
160
|
+
topicStartup: 'Startup',
|
|
161
|
+
topicScreens: 'Screens',
|
|
162
|
+
topicNetwork: 'Network',
|
|
163
|
+
topicResponsiveness: 'Responsiveness',
|
|
164
|
+
topicErrors: 'Errors',
|
|
165
|
+
topicCustom: 'Custom spans',
|
|
166
|
+
headlineStartup: 'App ready {{value}} after tracer init',
|
|
167
|
+
headlineScreens: '{{count}} visits · p95 transition {{value}}',
|
|
168
|
+
headlineNetwork: '{{count}} requests · p95 {{value}}',
|
|
169
|
+
headlineResponsiveness: '{{count}} stalls over {{budget}} ms',
|
|
170
|
+
headlineErrors: '{{count}} errors',
|
|
171
|
+
headlineCustom: '{{count}} spans',
|
|
172
|
+
hintStartup: 'Call useAppReadyMetric(client, ready) from /react.',
|
|
173
|
+
hintScreens: 'Add createNavigationPlugin(navigationRef) from /plugins.',
|
|
174
|
+
hintNetwork: 'Add createNetworkPlugin() from /plugins.',
|
|
175
|
+
hintResponsiveness:
|
|
176
|
+
'Add createNativeMetricsPlugin(), Sentry frames, or runtimeMetrics: true.',
|
|
177
|
+
hintErrors: 'Add createErrorsPlugin() from /plugins.',
|
|
178
|
+
hintCustom: 'Use client.trace or native spans.',
|
|
179
|
+
issueSlowStartup: 'Slow startup',
|
|
180
|
+
issueSlowScreen: 'Slow screen',
|
|
181
|
+
issueSlowRequest: 'Slow request',
|
|
182
|
+
issueFailedRequest: 'Failed request',
|
|
183
|
+
issueStall: 'JS stall',
|
|
184
|
+
issueError: 'Error',
|
|
185
|
+
beforeNavigation: 'Before first screen',
|
|
186
|
+
liveAllActivity: 'All activity',
|
|
187
|
+
liveInspector: 'Inspector',
|
|
188
|
+
liveRequests: 'requests',
|
|
189
|
+
liveStalls: 'stalls',
|
|
190
|
+
liveErrors: 'errors',
|
|
191
|
+
liveEmpty:
|
|
192
|
+
'No activity on this screen yet. Use the app; events stream in here.',
|
|
193
|
+
topicResources: 'Resources',
|
|
194
|
+
headlineResources: 'CPU {{cpu}}% · {{memory}} MB',
|
|
195
|
+
hintResources: 'Add createNativeMetricsPlugin() from /plugins.',
|
|
196
|
+
issueUiStall: 'UI stall',
|
|
197
|
+
issueFrozenFrames: 'Frozen frames',
|
|
198
|
+
liveFlag: 'Flag',
|
|
199
|
+
liveShare: 'Share trace',
|
|
200
|
+
liveFlagged: 'Flagged',
|
|
201
|
+
share: 'Share',
|
|
202
|
+
more: 'More',
|
|
203
|
+
shareTitle: 'Share recording',
|
|
204
|
+
startNew: 'Start new recording',
|
|
205
|
+
frozen: 'Frozen',
|
|
206
|
+
topics: 'Topics',
|
|
207
|
+
sortBy: 'Sort: {{sort}}',
|
|
208
|
+
filter: 'Filter',
|
|
209
|
+
range: 'Range',
|
|
210
|
+
stat_latest: 'Latest',
|
|
211
|
+
stat_median: 'Median',
|
|
212
|
+
stat_p95: 'p95',
|
|
213
|
+
stat_min: 'Min',
|
|
214
|
+
stat_max: 'Max',
|
|
215
|
+
samples: 'Samples',
|
|
216
|
+
duration: 'Duration',
|
|
217
|
+
time: 'Time',
|
|
218
|
+
spanId: 'Span ID',
|
|
219
|
+
correlationId: 'Correlation ID',
|
|
220
|
+
attributeSearch: 'Search attributes',
|
|
221
|
+
} as const
|
|
222
|
+
export type InspectorLabels = { [K in keyof typeof defaultLabels]: string }
|
|
223
|
+
export type InspectorLabelKey = keyof InspectorLabels
|
|
224
|
+
export type InspectorTranslator = (
|
|
225
|
+
key: InspectorLabelKey,
|
|
226
|
+
values?: Record<string, unknown>
|
|
227
|
+
) => string
|
|
228
|
+
export const createTranslator =
|
|
229
|
+
(labels?: Partial<InspectorLabels>): InspectorTranslator =>
|
|
230
|
+
(key, values) =>
|
|
231
|
+
(labels?.[key] ?? defaultLabels[key]).replace(
|
|
232
|
+
/{{(\w+)}}/g,
|
|
233
|
+
(_, name: string) => String(values?.[name] ?? '')
|
|
234
|
+
)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React, { useCallback, useState, type ReactNode } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ActionSheetIOS,
|
|
4
|
+
Platform,
|
|
5
|
+
View,
|
|
6
|
+
type ColorValue,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native'
|
|
10
|
+
import { ActionSheet, type SheetOption } from './InspectorControls'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Native building blocks with React Native fallbacks. Optional peers are required directly
|
|
14
|
+
* inside `try` so Metro treats them as optional; apps without them get the RN UI.
|
|
15
|
+
*/
|
|
16
|
+
type BottomTabs = typeof import('react-native-bottom-tabs').default
|
|
17
|
+
type GlassModule = typeof import('expo-glass-effect')
|
|
18
|
+
let NativeTabView: BottomTabs | undefined
|
|
19
|
+
let glass: GlassModule | undefined
|
|
20
|
+
try {
|
|
21
|
+
NativeTabView = require('react-native-bottom-tabs').default
|
|
22
|
+
} catch {
|
|
23
|
+
NativeTabView = undefined
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
glass = require('expo-glass-effect')
|
|
27
|
+
} catch {
|
|
28
|
+
glass = undefined
|
|
29
|
+
}
|
|
30
|
+
const liquidGlass = (() => {
|
|
31
|
+
try {
|
|
32
|
+
return Platform.OS === 'ios' && !!glass?.isLiquidGlassAvailable()
|
|
33
|
+
} catch {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
})()
|
|
37
|
+
|
|
38
|
+
export interface MenuConfig {
|
|
39
|
+
title?: string
|
|
40
|
+
options: SheetOption[]
|
|
41
|
+
cancelLabel: string
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `open(config)` shows a native action sheet on iOS (UIAlertController, Liquid Glass on
|
|
45
|
+
* iOS 26) and the RN sheet elsewhere; render `element` once in the calling component.
|
|
46
|
+
*/
|
|
47
|
+
export function useMenu() {
|
|
48
|
+
const [config, setConfig] = useState<MenuConfig>()
|
|
49
|
+
const open = useCallback((next: MenuConfig) => {
|
|
50
|
+
if (Platform.OS !== 'ios') return setConfig(next)
|
|
51
|
+
const enabled = next.options.filter((option) => !option.disabled)
|
|
52
|
+
const destructive = enabled.findIndex((option) => option.destructive)
|
|
53
|
+
ActionSheetIOS.showActionSheetWithOptions(
|
|
54
|
+
{
|
|
55
|
+
title: next.title,
|
|
56
|
+
options: [
|
|
57
|
+
...enabled.map(
|
|
58
|
+
(option) => `${option.selected ? '✓ ' : ''}${option.label}`
|
|
59
|
+
),
|
|
60
|
+
next.cancelLabel,
|
|
61
|
+
],
|
|
62
|
+
cancelButtonIndex: enabled.length,
|
|
63
|
+
destructiveButtonIndex: destructive >= 0 ? destructive : undefined,
|
|
64
|
+
},
|
|
65
|
+
(index) => enabled[index]?.onPress()
|
|
66
|
+
)
|
|
67
|
+
}, [])
|
|
68
|
+
const element = config ? (
|
|
69
|
+
<ActionSheet {...config} close={() => setConfig(undefined)} />
|
|
70
|
+
) : null
|
|
71
|
+
return { open, element }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface NativeTab<K extends string> {
|
|
75
|
+
key: K
|
|
76
|
+
title: string
|
|
77
|
+
/** SF Symbol name for the native iOS tab bar. */
|
|
78
|
+
sfSymbol: string
|
|
79
|
+
badge?: string
|
|
80
|
+
render: () => ReactNode
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Native tab bar (UITabBarController / Material) when react-native-bottom-tabs is
|
|
84
|
+
* installed; inactive screens are lazy and frozen so live ticks only render the visible tab.
|
|
85
|
+
*/
|
|
86
|
+
export function NativeTabs<K extends string>({
|
|
87
|
+
tabs,
|
|
88
|
+
value,
|
|
89
|
+
onChange,
|
|
90
|
+
tint,
|
|
91
|
+
fallback,
|
|
92
|
+
}: {
|
|
93
|
+
tabs: NativeTab<K>[]
|
|
94
|
+
value: K
|
|
95
|
+
onChange: (key: K) => void
|
|
96
|
+
tint: ColorValue
|
|
97
|
+
fallback: (content: ReactNode) => ReactNode
|
|
98
|
+
}) {
|
|
99
|
+
const index = Math.max(
|
|
100
|
+
0,
|
|
101
|
+
tabs.findIndex((tab) => tab.key === value)
|
|
102
|
+
)
|
|
103
|
+
// ponytail: iOS only. Android's Material tab bar needs a MaterialComponents app theme
|
|
104
|
+
// (crashes on AppCompat hosts) and image icons instead of SF Symbols.
|
|
105
|
+
if (!NativeTabView || Platform.OS !== 'ios')
|
|
106
|
+
return <>{fallback(tabs[index]?.render())}</>
|
|
107
|
+
return (
|
|
108
|
+
<NativeTabView
|
|
109
|
+
navigationState={{
|
|
110
|
+
index,
|
|
111
|
+
routes: tabs.map((tab) => ({
|
|
112
|
+
key: tab.key,
|
|
113
|
+
title: tab.title,
|
|
114
|
+
focusedIcon: { sfSymbol: tab.sfSymbol } as never,
|
|
115
|
+
badge: tab.badge,
|
|
116
|
+
})),
|
|
117
|
+
}}
|
|
118
|
+
onIndexChange={(next) => onChange(tabs[next].key)}
|
|
119
|
+
renderScene={({ route }) =>
|
|
120
|
+
tabs.find((tab) => tab.key === route.key)?.render() ?? null
|
|
121
|
+
}
|
|
122
|
+
getLazy={() => true}
|
|
123
|
+
getFreezeOnBlur={() => true}
|
|
124
|
+
getBadge={({ route }) => tabs.find((tab) => tab.key === route.key)?.badge}
|
|
125
|
+
minimizeBehavior="onScrollDown"
|
|
126
|
+
tabBarActiveTintColor={tint}
|
|
127
|
+
hapticFeedbackEnabled
|
|
128
|
+
/>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Liquid Glass surface on iOS 26 (expo-glass-effect); a solid fallback elsewhere. */
|
|
133
|
+
export function Glass({
|
|
134
|
+
style,
|
|
135
|
+
fallbackColor,
|
|
136
|
+
children,
|
|
137
|
+
interactive,
|
|
138
|
+
}: {
|
|
139
|
+
style?: StyleProp<ViewStyle>
|
|
140
|
+
fallbackColor: ColorValue
|
|
141
|
+
children?: ReactNode
|
|
142
|
+
interactive?: boolean
|
|
143
|
+
}) {
|
|
144
|
+
if (liquidGlass && glass) {
|
|
145
|
+
const { GlassView } = glass
|
|
146
|
+
return (
|
|
147
|
+
<GlassView
|
|
148
|
+
style={style}
|
|
149
|
+
glassEffectStyle="regular"
|
|
150
|
+
isInteractive={interactive}
|
|
151
|
+
>
|
|
152
|
+
{children}
|
|
153
|
+
</GlassView>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
return (
|
|
157
|
+
<View style={[style, { backgroundColor: fallbackColor }]}>{children}</View>
|
|
158
|
+
)
|
|
159
|
+
}
|