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,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
+ }