react-native-nitro-tracing 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/NitroTracing.podspec +32 -0
  4. package/README.md +75 -0
  5. package/android/CMakeLists.txt +49 -0
  6. package/android/build.gradle +133 -0
  7. package/android/fix-prefab.gradle +51 -0
  8. package/android/gradle.properties +5 -0
  9. package/android/proguard-rules.pro +5 -0
  10. package/android/src/main/AndroidManifest.xml +2 -0
  11. package/android/src/main/cpp/cpp-adapter.cpp +47 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrotracing/HermesSamplingGuard.kt +43 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingFrameSource.kt +36 -0
  14. package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingPackage.kt +31 -0
  15. package/client.ts +15 -0
  16. package/cpp/HermesSampling.cpp +26 -0
  17. package/cpp/HermesSampling.hpp +10 -0
  18. package/cpp/HybridRecording.cpp +66 -0
  19. package/cpp/HybridRecording.hpp +30 -0
  20. package/cpp/HybridTraceSpan.cpp +10 -0
  21. package/cpp/HybridTraceSpan.hpp +17 -0
  22. package/cpp/HybridTracing.cpp +10 -0
  23. package/cpp/HybridTracing.hpp +10 -0
  24. package/cpp/NativeConversions.cpp +77 -0
  25. package/cpp/NativeConversions.hpp +15 -0
  26. package/cpp/core/NativeSampler.cpp +152 -0
  27. package/cpp/core/NativeSampler.hpp +56 -0
  28. package/cpp/core/Recorder.cpp +218 -0
  29. package/cpp/core/Recorder.hpp +60 -0
  30. package/cpp/core/Retention.cpp +26 -0
  31. package/cpp/core/Retention.hpp +7 -0
  32. package/cpp/core/TraceJson.cpp +214 -0
  33. package/cpp/core/TraceJson.hpp +7 -0
  34. package/cpp/core/TraceTypes.hpp +62 -0
  35. package/expo.ts +2 -0
  36. package/ios/NitroTracingFrameSource.mm +35 -0
  37. package/lib/commonjs/client.cjs +767 -0
  38. package/lib/commonjs/client.cjs.map +7 -0
  39. package/lib/commonjs/expo.cjs +88 -0
  40. package/lib/commonjs/expo.cjs.map +7 -0
  41. package/lib/commonjs/index.cjs +28 -0
  42. package/lib/commonjs/index.cjs.map +7 -0
  43. package/lib/commonjs/performance.cjs +112 -0
  44. package/lib/commonjs/performance.cjs.map +7 -0
  45. package/lib/commonjs/plugins.cjs +477 -0
  46. package/lib/commonjs/plugins.cjs.map +7 -0
  47. package/lib/commonjs/react.cjs +3833 -0
  48. package/lib/commonjs/react.cjs.map +7 -0
  49. package/lib/commonjs/release-profiler.cjs +142 -0
  50. package/lib/commonjs/release-profiler.cjs.map +7 -0
  51. package/lib/commonjs/sentry.cjs +188 -0
  52. package/lib/commonjs/sentry.cjs.map +7 -0
  53. package/lib/module/client.mjs +768 -0
  54. package/lib/module/client.mjs.map +7 -0
  55. package/lib/module/expo.mjs +62 -0
  56. package/lib/module/expo.mjs.map +7 -0
  57. package/lib/module/index.mjs +7 -0
  58. package/lib/module/index.mjs.map +7 -0
  59. package/lib/module/performance.mjs +89 -0
  60. package/lib/module/performance.mjs.map +7 -0
  61. package/lib/module/plugins.mjs +461 -0
  62. package/lib/module/plugins.mjs.map +7 -0
  63. package/lib/module/react.mjs +3851 -0
  64. package/lib/module/react.mjs.map +7 -0
  65. package/lib/module/release-profiler.mjs +137 -0
  66. package/lib/module/release-profiler.mjs.map +7 -0
  67. package/lib/module/sentry.mjs +165 -0
  68. package/lib/module/sentry.mjs.map +7 -0
  69. package/lib/typescript/client.d.ts +4 -0
  70. package/lib/typescript/expo.d.ts +2 -0
  71. package/lib/typescript/performance.d.ts +2 -0
  72. package/lib/typescript/plugins.d.ts +12 -0
  73. package/lib/typescript/react.d.ts +5 -0
  74. package/lib/typescript/release-profiler.d.ts +2 -0
  75. package/lib/typescript/sentry.d.ts +2 -0
  76. package/lib/typescript/src/client/TracingSink.d.ts +10 -0
  77. package/lib/typescript/src/client/createTraceClient.d.ts +82 -0
  78. package/lib/typescript/src/client/playground.d.ts +12 -0
  79. package/lib/typescript/src/expo/ExpoAdapter.d.ts +15 -0
  80. package/lib/typescript/src/expo/index.d.ts +2 -0
  81. package/lib/typescript/src/index.d.ts +6 -0
  82. package/lib/typescript/src/plugins/TracePlugin.d.ts +22 -0
  83. package/lib/typescript/src/plugins/errors.d.ts +15 -0
  84. package/lib/typescript/src/plugins/nativeMetrics.d.ts +13 -0
  85. package/lib/typescript/src/plugins/navigation.d.ts +29 -0
  86. package/lib/typescript/src/plugins/network.d.ts +23 -0
  87. package/lib/typescript/src/plugins/orderSpans.d.ts +3 -0
  88. package/lib/typescript/src/plugins/performance.d.ts +13 -0
  89. package/lib/typescript/src/plugins/releaseProfiler.d.ts +25 -0
  90. package/lib/typescript/src/plugins/runtimeMetrics.d.ts +17 -0
  91. package/lib/typescript/src/plugins/sentry.d.ts +15 -0
  92. package/lib/typescript/src/plugins/startPlugins.d.ts +4 -0
  93. package/lib/typescript/src/react/DetailView.d.ts +11 -0
  94. package/lib/typescript/src/react/ExplorerFilters.d.ts +9 -0
  95. package/lib/typescript/src/react/ExplorerView.d.ts +9 -0
  96. package/lib/typescript/src/react/InspectorControls.d.ts +229 -0
  97. package/lib/typescript/src/react/InspectorHeader.d.ts +8 -0
  98. package/lib/typescript/src/react/InspectorSheet.d.ts +16 -0
  99. package/lib/typescript/src/react/MetricsView.d.ts +7 -0
  100. package/lib/typescript/src/react/SummaryView.d.ts +11 -0
  101. package/lib/typescript/src/react/TimelineView.d.ts +8 -0
  102. package/lib/typescript/src/react/TraceInspector.d.ts +11 -0
  103. package/lib/typescript/src/react/TraceOverlay.d.ts +17 -0
  104. package/lib/typescript/src/react/explorerModel.d.ts +38 -0
  105. package/lib/typescript/src/react/labels.d.ts +203 -0
  106. package/lib/typescript/src/react/native.d.ts +42 -0
  107. package/lib/typescript/src/react/readSnapshot.d.ts +4 -0
  108. package/lib/typescript/src/react/store.d.ts +9 -0
  109. package/lib/typescript/src/react/topics.d.ts +80 -0
  110. package/lib/typescript/src/react/useAppReadyMetric.d.ts +3 -0
  111. package/lib/typescript/src/react/useLiveRecording.d.ts +4 -0
  112. package/lib/typescript/src/react/useTraceViewer.d.ts +140 -0
  113. package/lib/typescript/src/react/viewerModel.d.ts +59 -0
  114. package/lib/typescript/src/specs/Recording.nitro.d.ts +31 -0
  115. package/lib/typescript/src/specs/TraceSpan.nitro.d.ts +14 -0
  116. package/lib/typescript/src/specs/Tracing.nitro.d.ts +17 -0
  117. package/lib/typescript/src/types/CompletedSpanOptions.d.ts +15 -0
  118. package/lib/typescript/src/types/EventContext.d.ts +8 -0
  119. package/lib/typescript/src/types/MarkEvent.d.ts +4 -0
  120. package/lib/typescript/src/types/MarkOptions.d.ts +6 -0
  121. package/lib/typescript/src/types/MetricEvent.d.ts +8 -0
  122. package/lib/typescript/src/types/MetricOptions.d.ts +10 -0
  123. package/lib/typescript/src/types/NativeSamplingOptions.d.ts +7 -0
  124. package/lib/typescript/src/types/ReadOptions.d.ts +7 -0
  125. package/lib/typescript/src/types/RecordingOptions.d.ts +11 -0
  126. package/lib/typescript/src/types/RecordingStats.d.ts +25 -0
  127. package/lib/typescript/src/types/SpanEvent.d.ts +13 -0
  128. package/lib/typescript/src/types/SpanOptions.d.ts +6 -0
  129. package/lib/typescript/src/types/SpanOutcome.d.ts +2 -0
  130. package/lib/typescript/src/types/TraceAttribute.d.ts +7 -0
  131. package/lib/typescript/src/types/TraceAttributes.d.ts +3 -0
  132. package/lib/typescript/src/types/TraceContext.d.ts +10 -0
  133. package/lib/typescript/src/types/TracePage.d.ts +18 -0
  134. package/lib/typescript/src/types.d.ts +17 -0
  135. package/nitro.json +20 -0
  136. package/nitrogen/generated/.gitattributes +1 -0
  137. package/nitrogen/generated/android/NitroTracing+autolinking.cmake +83 -0
  138. package/nitrogen/generated/android/NitroTracing+autolinking.gradle +27 -0
  139. package/nitrogen/generated/android/NitroTracingOnLoad.cpp +49 -0
  140. package/nitrogen/generated/android/NitroTracingOnLoad.hpp +34 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrotracing/NitroTracingOnLoad.kt +35 -0
  142. package/nitrogen/generated/ios/NitroTracing+autolinking.rb +62 -0
  143. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.cpp +17 -0
  144. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.hpp +27 -0
  145. package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Umbrella.hpp +38 -0
  146. package/nitrogen/generated/ios/NitroTracingAutolinking.mm +35 -0
  147. package/nitrogen/generated/ios/NitroTracingAutolinking.swift +16 -0
  148. package/nitrogen/generated/shared/c++/CompletedSpanOptions.hpp +122 -0
  149. package/nitrogen/generated/shared/c++/HybridRecordingSpec.cpp +31 -0
  150. package/nitrogen/generated/shared/c++/HybridRecordingSpec.hpp +100 -0
  151. package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.cpp +23 -0
  152. package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.hpp +65 -0
  153. package/nitrogen/generated/shared/c++/HybridTracingSpec.cpp +22 -0
  154. package/nitrogen/generated/shared/c++/HybridTracingSpec.hpp +68 -0
  155. package/nitrogen/generated/shared/c++/MarkEvent.hpp +102 -0
  156. package/nitrogen/generated/shared/c++/MarkOptions.hpp +99 -0
  157. package/nitrogen/generated/shared/c++/MetricEvent.hpp +110 -0
  158. package/nitrogen/generated/shared/c++/MetricOptions.hpp +107 -0
  159. package/nitrogen/generated/shared/c++/NativeSamplingOptions.hpp +87 -0
  160. package/nitrogen/generated/shared/c++/ReadOptions.hpp +87 -0
  161. package/nitrogen/generated/shared/c++/RecordingOptions.hpp +95 -0
  162. package/nitrogen/generated/shared/c++/RecordingStats.hpp +123 -0
  163. package/nitrogen/generated/shared/c++/SpanEvent.hpp +121 -0
  164. package/nitrogen/generated/shared/c++/SpanOptions.hpp +99 -0
  165. package/nitrogen/generated/shared/c++/SpanOutcome.hpp +84 -0
  166. package/nitrogen/generated/shared/c++/TraceAttribute.hpp +87 -0
  167. package/nitrogen/generated/shared/c++/TracePage.hpp +111 -0
  168. package/package.json +236 -0
  169. package/performance.ts +2 -0
  170. package/plugins.ts +24 -0
  171. package/react-native.config.js +16 -0
  172. package/react.ts +10 -0
  173. package/release-profiler.ts +6 -0
  174. package/sentry.ts +2 -0
  175. package/src/client/TracingSink.ts +25 -0
  176. package/src/client/createTraceClient.ts +439 -0
  177. package/src/client/playground.ts +143 -0
  178. package/src/expo/ExpoAdapter.ts +71 -0
  179. package/src/expo/index.ts +2 -0
  180. package/src/index.ts +8 -0
  181. package/src/plugins/TracePlugin.ts +22 -0
  182. package/src/plugins/errors.ts +69 -0
  183. package/src/plugins/nativeMetrics.ts +26 -0
  184. package/src/plugins/navigation.ts +111 -0
  185. package/src/plugins/network.ts +178 -0
  186. package/src/plugins/orderSpans.ts +22 -0
  187. package/src/plugins/performance.ts +120 -0
  188. package/src/plugins/releaseProfiler.ts +123 -0
  189. package/src/plugins/runtimeMetrics.ts +131 -0
  190. package/src/plugins/sentry.ts +176 -0
  191. package/src/plugins/startPlugins.ts +58 -0
  192. package/src/react/DetailView.tsx +514 -0
  193. package/src/react/ExplorerFilters.tsx +151 -0
  194. package/src/react/ExplorerView.tsx +243 -0
  195. package/src/react/InspectorControls.tsx +651 -0
  196. package/src/react/InspectorHeader.tsx +171 -0
  197. package/src/react/InspectorSheet.tsx +89 -0
  198. package/src/react/MetricsView.tsx +182 -0
  199. package/src/react/SummaryView.tsx +121 -0
  200. package/src/react/TimelineView.tsx +87 -0
  201. package/src/react/TraceInspector.tsx +196 -0
  202. package/src/react/TraceOverlay.tsx +585 -0
  203. package/src/react/explorerModel.ts +285 -0
  204. package/src/react/labels.ts +234 -0
  205. package/src/react/native.tsx +159 -0
  206. package/src/react/readSnapshot.ts +58 -0
  207. package/src/react/store.ts +31 -0
  208. package/src/react/topics.ts +427 -0
  209. package/src/react/useAppReadyMetric.ts +10 -0
  210. package/src/react/useLiveRecording.ts +43 -0
  211. package/src/react/useTraceViewer.ts +333 -0
  212. package/src/react/viewerModel.ts +172 -0
  213. package/src/specs/Recording.nitro.ts +40 -0
  214. package/src/specs/TraceSpan.nitro.ts +14 -0
  215. package/src/specs/Tracing.nitro.ts +14 -0
  216. package/src/types/CompletedSpanOptions.ts +15 -0
  217. package/src/types/EventContext.ts +8 -0
  218. package/src/types/MarkEvent.ts +3 -0
  219. package/src/types/MarkOptions.ts +6 -0
  220. package/src/types/MetricEvent.ts +8 -0
  221. package/src/types/MetricOptions.ts +10 -0
  222. package/src/types/NativeSamplingOptions.ts +7 -0
  223. package/src/types/ReadOptions.ts +7 -0
  224. package/src/types/RecordingOptions.ts +11 -0
  225. package/src/types/RecordingStats.ts +25 -0
  226. package/src/types/SpanEvent.ts +13 -0
  227. package/src/types/SpanOptions.ts +6 -0
  228. package/src/types/SpanOutcome.ts +2 -0
  229. package/src/types/TraceAttribute.ts +7 -0
  230. package/src/types/TraceAttributes.ts +4 -0
  231. package/src/types/TraceContext.ts +10 -0
  232. package/src/types/TracePage.ts +18 -0
  233. package/src/types.ts +17 -0
@@ -0,0 +1,514 @@
1
+ import { LegendList } from '@legendapp/list/react-native'
2
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
3
+ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
4
+ import type { Metric, Viewer } from './useTraceViewer'
5
+ import type { InspectorTranslator } from './labels'
6
+ import { hierarchyRows, matchesSearch, sourceOf } from './explorerModel'
7
+ import {
8
+ apple,
9
+ palette,
10
+ Row,
11
+ SearchField,
12
+ Section,
13
+ Segmented,
14
+ TextButton,
15
+ } from './InspectorControls'
16
+ import { useStore } from './store'
17
+ import { formatMetric } from './viewerModel'
18
+
19
+ const fixed = (value: number) => value.toFixed(1)
20
+
21
+ export function MetricDetail({
22
+ series,
23
+ t,
24
+ }: {
25
+ series: Metric
26
+ t: InspectorTranslator
27
+ }) {
28
+ const stats = [
29
+ ['latest', series.latest],
30
+ ['median', series.median],
31
+ ['p95', series.p95],
32
+ ['min', series.min],
33
+ ['max', series.peak],
34
+ ] as const
35
+ return (
36
+ <>
37
+ <Section header={series.name} footer={t('metricsScope')}>
38
+ <View style={styles.chartCard}>
39
+ <View style={styles.chart}>
40
+ {series.samples.map((sample) => (
41
+ <View
42
+ key={sample.sequence}
43
+ accessibilityLabel={`${sample.value.toFixed(2)} ${sample.unit}`}
44
+ style={[
45
+ styles.bar,
46
+ {
47
+ height: `${Math.max(2, (Math.abs(sample.value) / series.max) * 100)}%`,
48
+ },
49
+ ]}
50
+ />
51
+ ))}
52
+ </View>
53
+ <Text style={styles.caption}>
54
+ {t('metricWindow', {
55
+ displayed: series.samples.length,
56
+ retained: series.retainedCount,
57
+ start: fixed(series.startMs),
58
+ end: fixed(series.endMs),
59
+ })}
60
+ </Text>
61
+ </View>
62
+ {stats.map(([key, value]) => (
63
+ <Row
64
+ key={key}
65
+ title={t(`stat_${key}`)}
66
+ value={formatMetric(value, series.unit)}
67
+ />
68
+ ))}
69
+ <Row title={t('samples')} value={String(series.retainedCount)} last />
70
+ </Section>
71
+ {!!series.samples.at(-1)?.attributes.length && (
72
+ <Section header={t('attributes')}>
73
+ {series.samples.at(-1)!.attributes.map((attribute, index, all) => (
74
+ <Row
75
+ key={attribute.key}
76
+ title={attribute.key}
77
+ value={attribute.value}
78
+ last={index === all.length - 1}
79
+ />
80
+ ))}
81
+ </Section>
82
+ )}
83
+ </>
84
+ )
85
+ }
86
+
87
+ export function DetailView({ v, t }: { v: Viewer; t: InspectorTranslator }) {
88
+ const detail = v.detail!
89
+ const key =
90
+ detail.kind +
91
+ ':' +
92
+ (detail.kind === 'trace'
93
+ ? detail.value.id
94
+ : detail.kind === 'span'
95
+ ? detail.value.spanId
96
+ : detail.kind === 'mark'
97
+ ? detail.value.sequence
98
+ : detail.value.name)
99
+ const saved = v.detailState.current[key]
100
+ const [collapsed, setCollapsed] = useState(
101
+ new Set<string>(saved?.collapsed ?? [])
102
+ )
103
+ const [attributes, setAttributes] = useState(saved?.attributes ?? '')
104
+ const [layout, setLayout] = useState<'waterfall' | 'list'>(
105
+ saved?.layout ?? 'waterfall'
106
+ )
107
+ const offset = useRef(saved?.offset ?? 0)
108
+ useEffect(() => {
109
+ if (Object.keys(v.detailState.current).length > 128)
110
+ delete v.detailState.current[Object.keys(v.detailState.current)[0]]
111
+ v.detailState.current[key] = {
112
+ collapsed: [...collapsed],
113
+ attributes,
114
+ layout,
115
+ offset: offset.current,
116
+ }
117
+ }, [collapsed, attributes, layout, key, v.detailState])
118
+ const scrollProps = {
119
+ contentOffset: { x: 0, y: offset.current },
120
+ scrollEventThrottle: 100,
121
+ onScroll: (event: { nativeEvent: { contentOffset: { y: number } } }) => {
122
+ offset.current = event.nativeEvent.contentOffset.y
123
+ if (v.detailState.current[key])
124
+ v.detailState.current[key].offset = offset.current
125
+ },
126
+ }
127
+ const items = useMemo(
128
+ () =>
129
+ detail.kind === 'trace'
130
+ ? [
131
+ ...hierarchyRows(detail.value.spans, collapsed).map((row) => ({
132
+ kind: 'span' as const,
133
+ row,
134
+ })),
135
+ ...detail.value.marks.map((mark) => ({
136
+ kind: 'mark' as const,
137
+ mark,
138
+ })),
139
+ ]
140
+ : [],
141
+ [detail, collapsed]
142
+ )
143
+ const traceIds = useMemo(
144
+ () =>
145
+ new Set(
146
+ detail.kind === 'trace' ? detail.value.spans.map((s) => s.spanId) : []
147
+ ),
148
+ [detail]
149
+ )
150
+ if (detail.kind === 'trace') {
151
+ const trace = detail.value
152
+ const toggle = (spanId: string) =>
153
+ setCollapsed((previous) => {
154
+ const next = new Set(previous)
155
+ if (next.has(spanId)) next.delete(spanId)
156
+ else next.add(spanId)
157
+ return next
158
+ })
159
+ return (
160
+ <LegendList
161
+ recycleItems
162
+ {...scrollProps}
163
+ style={styles.list}
164
+ initialScrollOffset={offset.current}
165
+ data={items}
166
+ estimatedItemSize={64}
167
+ keyExtractor={(item) =>
168
+ item.kind === 'span'
169
+ ? `span:${item.row.span.sequence}`
170
+ : `mark:${item.mark.sequence}`
171
+ }
172
+ ListHeaderComponent={
173
+ <View style={styles.traceHeader}>
174
+ <Text style={styles.title}>{trace.name}</Text>
175
+ <Text style={styles.subtitle}>
176
+ {t('traceSummary', {
177
+ duration: fixed(trace.duration),
178
+ events: trace.spans.length + trace.marks.length,
179
+ errors: trace.errors,
180
+ })}
181
+ </Text>
182
+ <Text selectable style={styles.caption}>
183
+ {trace.id} · {fixed(trace.start)}–
184
+ {fixed(trace.start + trace.duration)} ms
185
+ </Text>
186
+ <EvictionNotice v={v} t={t} />
187
+ <FrozenNotice v={v} t={t} />
188
+ <Segmented
189
+ options={[
190
+ { value: 'waterfall', label: t('waterfall') },
191
+ { value: 'list', label: t('spans') },
192
+ ]}
193
+ value={layout}
194
+ onChange={setLayout}
195
+ />
196
+ <View style={styles.headerActions}>
197
+ <TextButton
198
+ label={t('expandAll')}
199
+ onPress={() => setCollapsed(new Set())}
200
+ />
201
+ <TextButton
202
+ label={t('collapseAll')}
203
+ onPress={() =>
204
+ setCollapsed(new Set(trace.spans.map((span) => span.spanId)))
205
+ }
206
+ />
207
+ </View>
208
+ </View>
209
+ }
210
+ ListEmptyComponent={<Text style={styles.empty}>{t('noSpans')}</Text>}
211
+ renderItem={({ item }) => {
212
+ if (item.kind === 'mark')
213
+ return (
214
+ <Row
215
+ icon="◆"
216
+ iconColor={apple.secondary}
217
+ title={item.mark.name}
218
+ value={`${fixed(item.mark.timestampMs)} ms`}
219
+ onPress={() => v.open({ kind: 'mark', value: item.mark })}
220
+ />
221
+ )
222
+ const { row } = item
223
+ const error = row.span.outcome === 'error'
224
+ return (
225
+ <Pressable
226
+ accessibilityRole="button"
227
+ onPress={() => v.open({ kind: 'span', value: row.span })}
228
+ style={({ pressed }) => [
229
+ styles.spanRow,
230
+ pressed && styles.pressed,
231
+ ]}
232
+ >
233
+ <View
234
+ style={[
235
+ styles.spanLine,
236
+ { paddingLeft: Math.min(8, row.depth) * 12 },
237
+ ]}
238
+ >
239
+ {row.children > 0 ? (
240
+ <Pressable
241
+ accessibilityRole="button"
242
+ accessibilityLabel={`${collapsed.has(row.span.spanId) ? t('expandAll') : t('collapseAll')} ${row.span.name}`}
243
+ onPress={() => toggle(row.span.spanId)}
244
+ hitSlop={10}
245
+ >
246
+ <Text style={styles.disclosure}>
247
+ {collapsed.has(row.span.spanId) ? '▸' : '▾'}
248
+ </Text>
249
+ </Pressable>
250
+ ) : (
251
+ <Text style={styles.disclosure}> </Text>
252
+ )}
253
+ <Text
254
+ style={[styles.spanName, error && styles.error]}
255
+ numberOfLines={1}
256
+ >
257
+ {row.span.name}
258
+ {row.children > 0 && (
259
+ <Text style={styles.subtitle}> ({row.children})</Text>
260
+ )}
261
+ </Text>
262
+ <Text style={[styles.spanValue, error && styles.error]}>
263
+ {row.span.durationMs.toFixed(
264
+ row.span.durationMs < 10 ? 1 : 0
265
+ )}{' '}
266
+ ms
267
+ </Text>
268
+ </View>
269
+ {(v.query.search &&
270
+ matchesSearch(row.span, v.query.search.toLowerCase())) ||
271
+ (row.span.parentSpanId &&
272
+ !traceIds.has(row.span.parentSpanId)) ? (
273
+ <Text
274
+ style={[
275
+ styles.caption,
276
+ { paddingLeft: Math.min(8, row.depth) * 12 + 20 },
277
+ ]}
278
+ >
279
+ {v.query.search &&
280
+ matchesSearch(row.span, v.query.search.toLowerCase())
281
+ ? t('searchMatch')
282
+ : t('missingParent')}
283
+ </Text>
284
+ ) : null}
285
+ {layout === 'waterfall' && (
286
+ <View style={styles.track}>
287
+ <View
288
+ style={[
289
+ styles.segment,
290
+ {
291
+ marginLeft: `${row.offset * 100}%`,
292
+ width: `${Math.max(0.5, row.width * 100)}%`,
293
+ backgroundColor: error ? palette.error : palette.accent,
294
+ },
295
+ ]}
296
+ />
297
+ </View>
298
+ )}
299
+ </Pressable>
300
+ )
301
+ }}
302
+ />
303
+ )
304
+ }
305
+ if (detail.kind === 'metric')
306
+ return (
307
+ <ScrollView {...scrollProps} contentContainerStyle={styles.content}>
308
+ <EvictionNotice v={v} t={t} />
309
+ <MetricDetail series={detail.value} t={t} />
310
+ </ScrollView>
311
+ )
312
+ const event = detail.value
313
+ const span = detail.kind === 'span' ? detail.value : undefined
314
+ const parent = span
315
+ ? v.page.spans.find((s) => s.spanId === span.parentSpanId)
316
+ : undefined
317
+ const children = span
318
+ ? v.page.spans.filter((s) => s.parentSpanId === span.spanId)
319
+ : []
320
+ const trace = span ? v.traceFor(span) : undefined
321
+ const shownAttributes = event.attributes.filter((a) =>
322
+ `${a.key} ${a.value}`.toLowerCase().includes(attributes.toLowerCase())
323
+ )
324
+ return (
325
+ <ScrollView
326
+ {...scrollProps}
327
+ contentContainerStyle={styles.content}
328
+ keyboardShouldPersistTaps="handled"
329
+ >
330
+ <View style={styles.traceHeader}>
331
+ <Text style={styles.title} selectable>
332
+ {event.name}
333
+ </Text>
334
+ <EvictionNotice v={v} t={t} />
335
+ </View>
336
+ <Section>
337
+ {span && (
338
+ <Row
339
+ title={t('outcome')}
340
+ value={t(span.outcome)}
341
+ tint={span.outcome === 'error' ? palette.error : undefined}
342
+ />
343
+ )}
344
+ {span && (
345
+ <Row
346
+ title={t('duration')}
347
+ value={`${span.durationMs.toFixed(2)} ms`}
348
+ />
349
+ )}
350
+ <Row title={t('source')} value={sourceOf(event)} />
351
+ <Row
352
+ title={t('time')}
353
+ value={`${fixed(event.timestampMs)}–${fixed(event.timestampMs + (span?.durationMs ?? 0))} ms`}
354
+ />
355
+ <Row
356
+ title={t('correlationId')}
357
+ subtitle={event.correlationId || t('uncorrelated')}
358
+ last={!span}
359
+ />
360
+ {span && <Row title={t('spanId')} subtitle={span.spanId} last />}
361
+ </Section>
362
+ <Section
363
+ footer={span?.parentSpanId && !parent ? t('missingParent') : undefined}
364
+ >
365
+ {trace && (
366
+ <Row
367
+ icon="☰"
368
+ title={t('wholeTrace')}
369
+ onPress={() => v.open({ kind: 'trace', value: trace })}
370
+ />
371
+ )}
372
+ {parent && (
373
+ <Row
374
+ icon="↑"
375
+ title={t('parent')}
376
+ subtitle={parent.name}
377
+ onPress={() => v.open({ kind: 'span', value: parent })}
378
+ />
379
+ )}
380
+ <Row
381
+ icon="⌕"
382
+ title={t('similar')}
383
+ onPress={() =>
384
+ (detail.kind === 'mark' ? v.showMarks : v.showSpans)({
385
+ search: event.name,
386
+ })
387
+ }
388
+ last
389
+ />
390
+ </Section>
391
+ {event.attributes.length > 0 && (
392
+ <>
393
+ <Text style={styles.sectionTitle}>{t('attributes')}</Text>
394
+ {event.attributes.length > 6 && (
395
+ <SearchField
396
+ value={attributes}
397
+ onChange={setAttributes}
398
+ placeholder={t('attributeSearch')}
399
+ />
400
+ )}
401
+ <Section>
402
+ {shownAttributes.map((a, index) => (
403
+ <Row
404
+ key={a.key}
405
+ title={a.key}
406
+ subtitle={a.value}
407
+ last={index === shownAttributes.length - 1}
408
+ />
409
+ ))}
410
+ </Section>
411
+ </>
412
+ )}
413
+ {children.length > 0 && (
414
+ <Section header={t('children')}>
415
+ {children.map((child, index) => (
416
+ <Row
417
+ key={child.spanId}
418
+ title={child.name}
419
+ value={`${child.durationMs.toFixed(1)} ms`}
420
+ tint={child.outcome === 'error' ? palette.error : undefined}
421
+ onPress={() => v.open({ kind: 'span', value: child })}
422
+ last={index === children.length - 1}
423
+ />
424
+ ))}
425
+ </Section>
426
+ )}
427
+ </ScrollView>
428
+ )
429
+ }
430
+
431
+ function EvictionNotice({ v, t }: { v: Viewer; t: InspectorTranslator }) {
432
+ const earliest =
433
+ useStore(v.telemetry, (s) => s.pending?.earliestSequence) ??
434
+ v.page.earliestSequence
435
+ const detail = v.detail
436
+ if (!detail) return null
437
+ const events =
438
+ detail.kind === 'trace'
439
+ ? [...detail.value.spans, ...detail.value.marks]
440
+ : detail.kind === 'metric'
441
+ ? detail.value.samples
442
+ : [detail.value]
443
+ return events.some((event) => event.sequence < earliest) ? (
444
+ <Text style={[styles.caption, styles.error]}>{t('evicted')}</Text>
445
+ ) : null
446
+ }
447
+ function FrozenNotice({ v, t }: { v: Viewer; t: InspectorTranslator }) {
448
+ const pending = useStore(v.telemetry, (s) => s.pending)
449
+ return pending ? (
450
+ <Text style={styles.caption}>{t('detailFrozen')}</Text>
451
+ ) : null
452
+ }
453
+
454
+ const styles = StyleSheet.create({
455
+ list: { flex: 1 },
456
+ content: { paddingBottom: 32 },
457
+ traceHeader: { paddingHorizontal: 16, paddingTop: 16, gap: 4 },
458
+ title: { fontSize: 22, fontWeight: '700', color: palette.text },
459
+ subtitle: { fontSize: 15, color: apple.secondary },
460
+ caption: { fontSize: 12, color: apple.secondary },
461
+ error: { color: palette.error },
462
+ headerActions: {
463
+ flexDirection: 'row',
464
+ justifyContent: 'space-between',
465
+ marginTop: 4,
466
+ marginBottom: 8,
467
+ },
468
+ sectionTitle: {
469
+ fontSize: 13,
470
+ color: apple.secondary,
471
+ textTransform: 'uppercase',
472
+ marginLeft: 32,
473
+ marginTop: 20,
474
+ },
475
+ spanRow: {
476
+ backgroundColor: apple.card,
477
+ paddingHorizontal: 16,
478
+ paddingVertical: 8,
479
+ gap: 6,
480
+ borderBottomWidth: StyleSheet.hairlineWidth,
481
+ borderColor: apple.separator,
482
+ },
483
+ pressed: { backgroundColor: '#e5e5ea' },
484
+ spanLine: { flexDirection: 'row', alignItems: 'center', gap: 6 },
485
+ disclosure: { width: 14, fontSize: 13, color: apple.secondary },
486
+ spanName: { flex: 1, fontSize: 15, color: palette.text },
487
+ spanValue: {
488
+ fontSize: 13,
489
+ color: apple.secondary,
490
+ fontVariant: ['tabular-nums'],
491
+ },
492
+ track: {
493
+ height: 4,
494
+ borderRadius: 2,
495
+ backgroundColor: apple.fill,
496
+ overflow: 'hidden',
497
+ },
498
+ segment: { height: 4, borderRadius: 2 },
499
+ chartCard: { padding: 16, gap: 8, backgroundColor: apple.card },
500
+ chart: { height: 96, flexDirection: 'row', alignItems: 'flex-end', gap: 2 },
501
+ bar: {
502
+ flex: 1,
503
+ minHeight: 2,
504
+ borderTopLeftRadius: 2,
505
+ borderTopRightRadius: 2,
506
+ backgroundColor: palette.accent,
507
+ },
508
+ empty: {
509
+ fontSize: 15,
510
+ color: apple.secondary,
511
+ textAlign: 'center',
512
+ padding: 32,
513
+ },
514
+ })
@@ -0,0 +1,151 @@
1
+ import React, { useState } from 'react'
2
+ import { ScrollView, StyleSheet, Text, TextInput, View } from 'react-native'
3
+ import type { Viewer } from './useTraceViewer'
4
+ import type { InspectorTranslator } from './labels'
5
+ import { sourceOf, defaultQuery, type Query } from './explorerModel'
6
+ import { apple, palette, Row, Section } from './InspectorControls'
7
+ import { InspectorSheet } from './InspectorSheet'
8
+ const toggle = (values: string[], value: string) =>
9
+ values.includes(value)
10
+ ? values.filter((item) => item !== value)
11
+ : [...values, value]
12
+
13
+ /** Draft filters: Apply commits them, Cancel discards them. Sorting lives in its own menu. */
14
+ export function ExplorerFilters({
15
+ v,
16
+ t,
17
+ close,
18
+ }: {
19
+ v: Viewer
20
+ t: InspectorTranslator
21
+ close: () => void
22
+ }) {
23
+ const [draft, setDraft] = useState(v.query)
24
+ const update = (patch: Partial<Query>) =>
25
+ setDraft((previous) => ({ ...previous, ...patch }))
26
+ const sources = [
27
+ ...new Set([...v.page.spans, ...v.page.marks].map(sourceOf)),
28
+ ].sort()
29
+ const outcomes =
30
+ v.mode === 'traces'
31
+ ? ['error', 'success']
32
+ : ['success', 'error', 'cancelled', 'interrupted']
33
+ const invalid =
34
+ [draft.from, draft.to, draft.minDuration].some(
35
+ (value) =>
36
+ value !== '' && (!Number.isFinite(Number(value)) || Number(value) < 0)
37
+ ) ||
38
+ Boolean(draft.from && draft.to && Number(draft.from) > Number(draft.to))
39
+ const field = (
40
+ key: 'minDuration' | 'from' | 'to' | 'correlation',
41
+ numeric = true
42
+ ) => (
43
+ <View style={styles.field}>
44
+ <Text style={styles.fieldLabel}>{t(key)}</Text>
45
+ <TextInput
46
+ accessibilityLabel={t(key)}
47
+ value={draft[key]}
48
+ onChangeText={(value) => update({ [key]: value })}
49
+ placeholder="—"
50
+ placeholderTextColor={apple.secondary}
51
+ keyboardType={numeric ? 'decimal-pad' : 'default'}
52
+ autoCapitalize="none"
53
+ autoCorrect={false}
54
+ style={styles.fieldInput}
55
+ />
56
+ </View>
57
+ )
58
+ return (
59
+ <InspectorSheet
60
+ title={t('filters')}
61
+ close={close}
62
+ closeLabel={t('cancel')}
63
+ primary={{
64
+ label: t('applyFilters'),
65
+ disabled: invalid,
66
+ onPress: () => {
67
+ v.updateQuery(draft)
68
+ close()
69
+ },
70
+ }}
71
+ >
72
+ <ScrollView
73
+ keyboardShouldPersistTaps="handled"
74
+ contentContainerStyle={{ paddingBottom: 32 }}
75
+ >
76
+ {v.mode !== 'marks' && (
77
+ <Section header={t('outcome')}>
78
+ {outcomes.map((outcome, index) => (
79
+ <Row
80
+ key={outcome}
81
+ title={t(
82
+ outcome === 'success' && v.mode === 'traces'
83
+ ? 'noErrors'
84
+ : (outcome as 'success')
85
+ )}
86
+ checked={draft.outcomes.includes(outcome)}
87
+ onPress={() =>
88
+ update({ outcomes: toggle(draft.outcomes, outcome) })
89
+ }
90
+ last={index === outcomes.length - 1}
91
+ />
92
+ ))}
93
+ </Section>
94
+ )}
95
+ {sources.length > 0 && (
96
+ <Section header={t('source')}>
97
+ {sources.map((source, index) => (
98
+ <Row
99
+ key={source}
100
+ title={source}
101
+ checked={draft.sources.includes(source)}
102
+ onPress={() =>
103
+ update({ sources: toggle(draft.sources, source) })
104
+ }
105
+ last={index === sources.length - 1}
106
+ />
107
+ ))}
108
+ </Section>
109
+ )}
110
+ <Section
111
+ header={t('range')}
112
+ footer={invalid ? t('invalidRange') : undefined}
113
+ >
114
+ {v.mode !== 'marks' && field('minDuration')}
115
+ {field('from')}
116
+ {field('to')}
117
+ {field('correlation', false)}
118
+ </Section>
119
+ <Section>
120
+ <Row
121
+ title={t('reset')}
122
+ tint={palette.error}
123
+ onPress={() => setDraft(defaultQuery())}
124
+ last
125
+ />
126
+ </Section>
127
+ </ScrollView>
128
+ </InspectorSheet>
129
+ )
130
+ }
131
+
132
+ const styles = StyleSheet.create({
133
+ field: {
134
+ flexDirection: 'row',
135
+ alignItems: 'center',
136
+ minHeight: 44,
137
+ paddingHorizontal: 16,
138
+ backgroundColor: apple.card,
139
+ borderBottomWidth: StyleSheet.hairlineWidth,
140
+ borderColor: apple.separator,
141
+ gap: 12,
142
+ },
143
+ fieldLabel: { flex: 1, fontSize: 17, color: palette.text },
144
+ fieldInput: {
145
+ minWidth: 110,
146
+ fontSize: 17,
147
+ color: palette.text,
148
+ textAlign: 'right',
149
+ paddingVertical: 8,
150
+ },
151
+ })