@goliapkg/sentori-react-native 4.0.0 → 5.0.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 (284) hide show
  1. package/android/build.gradle +21 -0
  2. package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
  3. package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
  4. package/android/src/test/resources/robolectric.properties +10 -0
  5. package/lib/config.d.ts +16 -47
  6. package/lib/config.d.ts.map +1 -1
  7. package/lib/config.js +3 -0
  8. package/lib/config.js.map +1 -1
  9. package/lib/device.d.ts +5 -0
  10. package/lib/device.d.ts.map +1 -0
  11. package/lib/device.js +29 -0
  12. package/lib/device.js.map +1 -0
  13. package/lib/emit-hooks.d.ts +7 -0
  14. package/lib/emit-hooks.d.ts.map +1 -0
  15. package/lib/emit-hooks.js +24 -0
  16. package/lib/emit-hooks.js.map +1 -0
  17. package/lib/error-boundary.d.ts.map +1 -1
  18. package/lib/error-boundary.js +1 -1
  19. package/lib/error-boundary.js.map +1 -1
  20. package/lib/feature-flags.d.ts.map +1 -1
  21. package/lib/handlers/dev-symbolicate.d.ts +1 -1
  22. package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
  23. package/lib/handlers/dev-symbolicate.js +2 -0
  24. package/lib/handlers/dev-symbolicate.js.map +1 -1
  25. package/lib/handlers/global.js +1 -1
  26. package/lib/handlers/global.js.map +1 -1
  27. package/lib/handlers/lifecycle.js +3 -3
  28. package/lib/handlers/lifecycle.js.map +1 -1
  29. package/lib/handlers/network.d.ts +2 -2
  30. package/lib/handlers/network.d.ts.map +1 -1
  31. package/lib/handlers/network.js +69 -187
  32. package/lib/handlers/network.js.map +1 -1
  33. package/lib/handlers/promise.js +1 -1
  34. package/lib/handlers/promise.js.map +1 -1
  35. package/lib/handlers/screenshot.d.ts.map +1 -1
  36. package/lib/handlers/screenshot.js +3 -12
  37. package/lib/handlers/screenshot.js.map +1 -1
  38. package/lib/index.d.ts +31 -109
  39. package/lib/index.d.ts.map +1 -1
  40. package/lib/index.js +51 -103
  41. package/lib/index.js.map +1 -1
  42. package/lib/init.d.ts +3 -150
  43. package/lib/init.d.ts.map +1 -1
  44. package/lib/init.js +88 -261
  45. package/lib/init.js.map +1 -1
  46. package/lib/long-task-monitor.d.ts +1 -7
  47. package/lib/long-task-monitor.d.ts.map +1 -1
  48. package/lib/long-task-monitor.js +37 -58
  49. package/lib/long-task-monitor.js.map +1 -1
  50. package/lib/mobile-vitals.d.ts +3 -31
  51. package/lib/mobile-vitals.d.ts.map +1 -1
  52. package/lib/mobile-vitals.js +22 -61
  53. package/lib/mobile-vitals.js.map +1 -1
  54. package/lib/native-pending.d.ts +3 -0
  55. package/lib/native-pending.d.ts.map +1 -0
  56. package/lib/native-pending.js +64 -0
  57. package/lib/native-pending.js.map +1 -0
  58. package/lib/navigation.d.ts +3 -17
  59. package/lib/navigation.d.ts.map +1 -1
  60. package/lib/navigation.js +32 -133
  61. package/lib/navigation.js.map +1 -1
  62. package/lib/netinfo.d.ts +1 -1
  63. package/lib/netinfo.d.ts.map +1 -1
  64. package/lib/netinfo.js +2 -2
  65. package/lib/netinfo.js.map +1 -1
  66. package/lib/push.d.ts.map +1 -1
  67. package/lib/push.js +2 -4
  68. package/lib/push.js.map +1 -1
  69. package/lib/rage-tap.d.ts.map +1 -1
  70. package/lib/rage-tap.js +28 -23
  71. package/lib/rage-tap.js.map +1 -1
  72. package/lib/replay.d.ts.map +1 -1
  73. package/lib/replay.js +3 -24
  74. package/lib/replay.js.map +1 -1
  75. package/lib/scope.d.ts +7 -0
  76. package/lib/scope.d.ts.map +1 -0
  77. package/lib/scope.js +41 -0
  78. package/lib/scope.js.map +1 -0
  79. package/lib/transport.d.ts +19 -92
  80. package/lib/transport.d.ts.map +1 -1
  81. package/lib/transport.js +75 -288
  82. package/lib/transport.js.map +1 -1
  83. package/lib/verbs.d.ts +16 -0
  84. package/lib/verbs.d.ts.map +1 -0
  85. package/lib/verbs.js +153 -0
  86. package/lib/verbs.js.map +1 -0
  87. package/package.json +3 -3
  88. package/src/__tests__/iron-rule.test.ts +128 -0
  89. package/src/__tests__/screenshot.test.ts +0 -2
  90. package/src/__tests__/transport.test.ts +66 -213
  91. package/src/__tests__/verbs.test.ts +91 -0
  92. package/src/config.ts +19 -47
  93. package/src/device.ts +36 -0
  94. package/src/emit-hooks.ts +30 -0
  95. package/src/error-boundary.tsx +1 -1
  96. package/src/handlers/dev-symbolicate.ts +2 -1
  97. package/src/handlers/global.ts +1 -1
  98. package/src/handlers/lifecycle.ts +3 -3
  99. package/src/handlers/network.ts +95 -211
  100. package/src/handlers/promise.ts +1 -1
  101. package/src/handlers/screenshot.ts +3 -11
  102. package/src/index.ts +81 -215
  103. package/src/init.ts +94 -446
  104. package/src/long-task-monitor.ts +42 -64
  105. package/src/mobile-vitals.ts +22 -82
  106. package/src/native-pending.ts +92 -0
  107. package/src/navigation.ts +34 -142
  108. package/src/netinfo.ts +3 -3
  109. package/src/push.ts +2 -5
  110. package/src/rage-tap.tsx +29 -26
  111. package/src/replay.ts +3 -20
  112. package/src/scope.ts +50 -0
  113. package/src/transport.ts +84 -371
  114. package/src/verbs.ts +198 -0
  115. package/lib/breadcrumbs.d.ts +0 -18
  116. package/lib/breadcrumbs.d.ts.map +0 -1
  117. package/lib/breadcrumbs.js +0 -42
  118. package/lib/breadcrumbs.js.map +0 -1
  119. package/lib/bundle-info.d.ts +0 -12
  120. package/lib/bundle-info.d.ts.map +0 -1
  121. package/lib/bundle-info.js +0 -73
  122. package/lib/bundle-info.js.map +0 -1
  123. package/lib/capture.d.ts +0 -140
  124. package/lib/capture.d.ts.map +0 -1
  125. package/lib/capture.js +0 -511
  126. package/lib/capture.js.map +0 -1
  127. package/lib/compat/sentry.d.ts +0 -126
  128. package/lib/compat/sentry.d.ts.map +0 -1
  129. package/lib/compat/sentry.js +0 -332
  130. package/lib/compat/sentry.js.map +0 -1
  131. package/lib/control-channel.d.ts +0 -6
  132. package/lib/control-channel.d.ts.map +0 -1
  133. package/lib/control-channel.js +0 -96
  134. package/lib/control-channel.js.map +0 -1
  135. package/lib/expo-compat.d.ts +0 -270
  136. package/lib/expo-compat.d.ts.map +0 -1
  137. package/lib/expo-compat.js +0 -500
  138. package/lib/expo-compat.js.map +0 -1
  139. package/lib/feedback-widget.d.ts +0 -35
  140. package/lib/feedback-widget.d.ts.map +0 -1
  141. package/lib/feedback-widget.js +0 -193
  142. package/lib/feedback-widget.js.map +0 -1
  143. package/lib/feedback.d.ts +0 -2
  144. package/lib/feedback.d.ts.map +0 -1
  145. package/lib/feedback.js +0 -17
  146. package/lib/feedback.js.map +0 -1
  147. package/lib/heartbeat.d.ts +0 -9
  148. package/lib/heartbeat.d.ts.map +0 -1
  149. package/lib/heartbeat.js +0 -138
  150. package/lib/heartbeat.js.map +0 -1
  151. package/lib/index.js.bak +0 -64
  152. package/lib/install-id.d.ts +0 -17
  153. package/lib/install-id.d.ts.map +0 -1
  154. package/lib/install-id.js +0 -125
  155. package/lib/install-id.js.map +0 -1
  156. package/lib/lifecycle.d.ts +0 -28
  157. package/lib/lifecycle.d.ts.map +0 -1
  158. package/lib/lifecycle.js +0 -73
  159. package/lib/lifecycle.js.map +0 -1
  160. package/lib/mask.d.ts +0 -16
  161. package/lib/mask.d.ts.map +0 -1
  162. package/lib/mask.js +0 -36
  163. package/lib/mask.js.map +0 -1
  164. package/lib/measure.d.ts +0 -4
  165. package/lib/measure.d.ts.map +0 -1
  166. package/lib/measure.js +0 -25
  167. package/lib/measure.js.map +0 -1
  168. package/lib/metrics.d.ts +0 -24
  169. package/lib/metrics.d.ts.map +0 -1
  170. package/lib/metrics.js +0 -81
  171. package/lib/metrics.js.map +0 -1
  172. package/lib/pre-crash-sentinel.d.ts +0 -12
  173. package/lib/pre-crash-sentinel.d.ts.map +0 -1
  174. package/lib/pre-crash-sentinel.js +0 -116
  175. package/lib/pre-crash-sentinel.js.map +0 -1
  176. package/lib/report-security.d.ts +0 -40
  177. package/lib/report-security.d.ts.map +0 -1
  178. package/lib/report-security.js +0 -159
  179. package/lib/report-security.js.map +0 -1
  180. package/lib/runtime-metrics-fps.d.ts +0 -19
  181. package/lib/runtime-metrics-fps.d.ts.map +0 -1
  182. package/lib/runtime-metrics-fps.js +0 -128
  183. package/lib/runtime-metrics-fps.js.map +0 -1
  184. package/lib/runtime-metrics-heap.d.ts +0 -13
  185. package/lib/runtime-metrics-heap.d.ts.map +0 -1
  186. package/lib/runtime-metrics-heap.js +0 -67
  187. package/lib/runtime-metrics-heap.js.map +0 -1
  188. package/lib/runtime-metrics-network.d.ts +0 -23
  189. package/lib/runtime-metrics-network.d.ts.map +0 -1
  190. package/lib/runtime-metrics-network.js +0 -99
  191. package/lib/runtime-metrics-network.js.map +0 -1
  192. package/lib/runtime-metrics.d.ts +0 -43
  193. package/lib/runtime-metrics.d.ts.map +0 -1
  194. package/lib/runtime-metrics.js +0 -115
  195. package/lib/runtime-metrics.js.map +0 -1
  196. package/lib/sample-profiler.d.ts +0 -21
  197. package/lib/sample-profiler.d.ts.map +0 -1
  198. package/lib/sample-profiler.js +0 -152
  199. package/lib/sample-profiler.js.map +0 -1
  200. package/lib/sentinel-context.d.ts +0 -4
  201. package/lib/sentinel-context.d.ts.map +0 -1
  202. package/lib/sentinel-context.js +0 -52
  203. package/lib/sentinel-context.js.map +0 -1
  204. package/lib/session-tracker.d.ts +0 -6
  205. package/lib/session-tracker.d.ts.map +0 -1
  206. package/lib/session-tracker.js +0 -50
  207. package/lib/session-tracker.js.map +0 -1
  208. package/lib/stack.d.ts +0 -3
  209. package/lib/stack.d.ts.map +0 -1
  210. package/lib/stack.js +0 -7
  211. package/lib/stack.js.map +0 -1
  212. package/lib/state-snapshots.d.ts +0 -33
  213. package/lib/state-snapshots.d.ts.map +0 -1
  214. package/lib/state-snapshots.js +0 -109
  215. package/lib/state-snapshots.js.map +0 -1
  216. package/lib/track.d.ts +0 -34
  217. package/lib/track.d.ts.map +0 -1
  218. package/lib/track.js +0 -107
  219. package/lib/track.js.map +0 -1
  220. package/lib/trail.d.ts +0 -12
  221. package/lib/trail.d.ts.map +0 -1
  222. package/lib/trail.js +0 -28
  223. package/lib/trail.js.map +0 -1
  224. package/lib/trust-score.d.ts +0 -20
  225. package/lib/trust-score.d.ts.map +0 -1
  226. package/lib/trust-score.js +0 -151
  227. package/lib/trust-score.js.map +0 -1
  228. package/lib/types.d.ts +0 -2
  229. package/lib/types.d.ts.map +0 -1
  230. package/lib/types.js +0 -4
  231. package/lib/types.js.map +0 -1
  232. package/lib/uuid.d.ts +0 -2
  233. package/lib/uuid.d.ts.map +0 -1
  234. package/lib/uuid.js +0 -3
  235. package/lib/uuid.js.map +0 -1
  236. package/src/__tests__/before-send.test.ts +0 -72
  237. package/src/__tests__/breadcrumbs.test.ts +0 -44
  238. package/src/__tests__/capture-replay.test.ts +0 -150
  239. package/src/__tests__/compat-sentry.test.ts +0 -121
  240. package/src/__tests__/control-channel.test.ts +0 -111
  241. package/src/__tests__/install-id.test.ts +0 -60
  242. package/src/__tests__/long-task-monitor.test.ts +0 -59
  243. package/src/__tests__/measure.test.ts +0 -45
  244. package/src/__tests__/navigation.test.ts +0 -175
  245. package/src/__tests__/push.test.ts +0 -238
  246. package/src/__tests__/report-security.test.ts +0 -106
  247. package/src/__tests__/runtime-metrics-instruments.test.ts +0 -109
  248. package/src/__tests__/runtime-metrics-network.test.ts +0 -78
  249. package/src/__tests__/sample-profiler.test.ts +0 -59
  250. package/src/__tests__/stack.test.ts +0 -45
  251. package/src/__tests__/state-snapshots.test.ts +0 -119
  252. package/src/__tests__/tracing.test.ts +0 -201
  253. package/src/__tests__/track.test.ts +0 -91
  254. package/src/__tests__/uuid.test.ts +0 -41
  255. package/src/breadcrumbs.ts +0 -57
  256. package/src/bundle-info.ts +0 -95
  257. package/src/capture.ts +0 -660
  258. package/src/compat/sentry.ts +0 -490
  259. package/src/control-channel.ts +0 -98
  260. package/src/expo-compat.ts +0 -698
  261. package/src/feedback-widget.tsx +0 -316
  262. package/src/feedback.ts +0 -21
  263. package/src/heartbeat.ts +0 -157
  264. package/src/install-id.ts +0 -146
  265. package/src/lifecycle.ts +0 -76
  266. package/src/mask.ts +0 -41
  267. package/src/measure.ts +0 -28
  268. package/src/metrics.ts +0 -94
  269. package/src/pre-crash-sentinel.ts +0 -140
  270. package/src/report-security.ts +0 -165
  271. package/src/runtime-metrics-fps.ts +0 -137
  272. package/src/runtime-metrics-heap.ts +0 -78
  273. package/src/runtime-metrics-network.ts +0 -103
  274. package/src/runtime-metrics.ts +0 -122
  275. package/src/sample-profiler.ts +0 -169
  276. package/src/sentinel-context.ts +0 -60
  277. package/src/session-tracker.ts +0 -53
  278. package/src/stack.ts +0 -10
  279. package/src/state-snapshots.ts +0 -133
  280. package/src/track.ts +0 -123
  281. package/src/trail.ts +0 -32
  282. package/src/trust-score.ts +0 -176
  283. package/src/types.ts +0 -21
  284. package/src/uuid.ts +0 -2
@@ -1,57 +1,39 @@
1
- // v0.9.6 #4 — JS thread long-task monitor.
1
+ // long_freeze — detected warn scenario (design.md §3, category B:
2
+ // 「app 卡住了 / 死了几秒」), JS-thread side.
2
3
  //
3
- // What it does: a setInterval(50ms) tick checks how much wall-clock
4
- // passed vs the requested interval. Excess = the JS thread was busy
5
- // doing something else for at least that much. When the excess
6
- // crosses 200ms (a "long task" per Chrome's PerformanceObserver
7
- // threshold) we emit a `sentori.longtask` span so the dashboard's
8
- // trace waterfall shows where the JS thread stalled.
4
+ // Mini-spec: a setInterval(50 ms) tick measures wall-clock drift.
5
+ // Drift ≤ 2 s ⇒ a `freeze` signal in the ring (context only —
6
+ // brief jank is not worth an event). Drift > 2 s ⇒ one `warn`
7
+ // event, scenario `long_freeze`, surface = current screen, with the
8
+ // blocked duration. Rate-limited to 6 events/minute so a freeze
9
+ // storm cannot flood ingest.
9
10
  //
10
- // What it does NOT do: capture the stack DURING the long task. JS
11
- // is single-threaded — by the time our tick runs, the busy code is
12
- // already gone. The span carries the duration + nearest navigation
13
- // context (via active span) so triage still has a route to blame.
14
- //
15
- // Why this and not a real Hermes sampler: Hermes ships a sampling
16
- // profiler but accessing it from JS requires RN-internal bridges
17
- // that vary per RN minor. We could swizzle / vendor headers but
18
- // that's an Insight-build-config burden. The long-task monitor
19
- // gets ~80% of the practical value (find slow renders, expensive
20
- // reducers, accidental sync work in render path) with zero native
21
- // code + zero RN-version sensitivity.
22
- //
23
- // Pairs naturally with +S4 (pre-crash sentinel, RAF frame budget,
24
- // fires at the slow-frame threshold) — long-task monitor fires
25
- // further down the slowness scale.
11
+ // What it cannot do: capture the stack DURING the block — JS is
12
+ // single-threaded; by the time our tick runs the busy code is gone.
13
+ // The native watchdogs (HangWatchdog / AnrWatchdog) cover the
14
+ // UI-thread side of the same scenario.
15
+
16
+ import { pushSignal } from '@goliapkg/sentori-core';
26
17
 
27
- import { startSpan } from '@goliapkg/sentori-core';
18
+ import { currentScreen } from './navigation';
19
+ import { warnDetected } from './verbs';
28
20
 
29
21
  const TICK_INTERVAL_MS = 50;
30
- const LONGTASK_THRESHOLD_MS = 200; // > 200ms blocking = a longtask
31
- const MAX_EMITS_PER_MIN = 60;
22
+ const SIGNAL_THRESHOLD_MS = 200; // ring entry: noticeable jank
23
+ const WARN_THRESHOLD_MS = 2_000; // warn event: a real freeze
24
+ const MAX_WARNS_PER_MIN = 6;
32
25
 
33
26
  let _timer: ReturnType<typeof setInterval> | null = null;
34
27
  let _lastTick = 0;
35
- let _emitWindowStart = 0;
36
- let _emitsThisWindow = 0;
28
+ let _warnWindowStart = 0;
29
+ let _warnsThisWindow = 0;
37
30
 
38
- export type LongTaskMonitorOptions = {
39
- enabled: boolean;
40
- /** Threshold ms above which a tick lag becomes a longtask span.
41
- * Default 200ms. Lower → noisier. */
42
- thresholdMs?: number;
43
- };
44
-
45
- export function startLongTaskMonitor(opts: LongTaskMonitorOptions): void {
31
+ export function startLongTaskMonitor(): void {
46
32
  if (_timer !== null) return;
47
- if (!opts.enabled) return;
48
- const threshold = opts.thresholdMs ?? LONGTASK_THRESHOLD_MS;
49
33
  _lastTick = Date.now();
50
- _emitWindowStart = _lastTick;
51
- _emitsThisWindow = 0;
52
- _timer = setInterval(() => {
53
- tick(threshold);
54
- }, TICK_INTERVAL_MS);
34
+ _warnWindowStart = _lastTick;
35
+ _warnsThisWindow = 0;
36
+ _timer = setInterval(tick, TICK_INTERVAL_MS);
55
37
  (_timer as unknown as { unref?: () => void }).unref?.();
56
38
  }
57
39
 
@@ -62,38 +44,34 @@ export function stopLongTaskMonitor(): void {
62
44
  }
63
45
  }
64
46
 
65
- function tick(threshold: number): void {
47
+ function tick(): void {
66
48
  const now = Date.now();
67
49
  const elapsed = now - _lastTick;
68
50
  _lastTick = now;
69
51
  const lag = elapsed - TICK_INTERVAL_MS;
70
- if (lag <= threshold) return;
52
+ if (lag <= SIGNAL_THRESHOLD_MS) return;
53
+
54
+ pushSignal('freeze', { blockedMs: Math.round(lag) });
55
+ if (lag <= WARN_THRESHOLD_MS) return;
71
56
 
72
- // Rate-limit emits: at most MAX_EMITS_PER_MIN per minute so a
73
- // pathological scroll-block-storm doesn't generate 1000 spans.
74
- if (now - _emitWindowStart >= 60_000) {
75
- _emitWindowStart = now;
76
- _emitsThisWindow = 0;
57
+ if (now - _warnWindowStart >= 60_000) {
58
+ _warnWindowStart = now;
59
+ _warnsThisWindow = 0;
77
60
  }
78
- if (_emitsThisWindow >= MAX_EMITS_PER_MIN) return;
79
- _emitsThisWindow += 1;
61
+ if (_warnsThisWindow >= MAX_WARNS_PER_MIN) return;
62
+ _warnsThisWindow += 1;
80
63
 
81
- const span = startSpan('sentori.longtask', {
82
- name: 'js.longtask',
83
- startNowMs: now - lag,
84
- tags: {
85
- 'profile.kind': 'longtask',
86
- 'profile.duration_ms': String(Math.round(lag)),
87
- 'profile.tick_interval_ms': String(TICK_INTERVAL_MS),
88
- },
89
- });
90
- span.finish({ endNowMs: now, status: 'ok' });
64
+ warnDetected(
65
+ 'long_freeze',
66
+ { screen: currentScreen() },
67
+ { blockedMs: Math.round(lag), thread: 'js' },
68
+ );
91
69
  }
92
70
 
93
71
  /** Test-only. */
94
72
  export function __resetLongTaskMonitorForTests(): void {
95
73
  stopLongTaskMonitor();
96
74
  _lastTick = 0;
97
- _emitWindowStart = 0;
98
- _emitsThisWindow = 0;
75
+ _warnWindowStart = 0;
76
+ _warnsThisWindow = 0;
99
77
  }
@@ -1,34 +1,25 @@
1
- // v0.9.4 #1 — Mobile Vitals.
1
+ // slow_cold_start — detected warn scenario (design.md §3, category
2
+ // C: 「启动过慢」).
2
3
  //
3
- // Three measurements, three call paths, one server schema:
4
- //
5
- // • Cold start: native side measures at app launch (iOS
6
- // mach_absolute_time / Android Process.getStartElapsedRealtime)
7
- // and exposes via the bundled native module — read once on JS
8
- // side at init() and ride along on the first event.
9
- // • TTID (Time-To-Initial-Display): automatic via
10
- // useTraceNavigation extension — span from navigation.dispatch
11
- // to first frame after route mount.
12
- // • TTFD (Time-To-Full-Display): manual. Host calls
13
- // sentori.markTimeToFullDisplay('Home').end() when the screen's
14
- // data has loaded.
15
- // • Slow / frozen frame counts: native side hooks CADisplayLink /
16
- // Choreographer.FrameCallback; counters flush per navigation
17
- // span.
18
- //
19
- // JS-side first: TTFD API + bundle-level vital captures. Native
20
- // pieces ship as a separate native module method `getColdStartMs`
21
- // + `getFrameCounts()` — graceful no-op if not linked.
4
+ // Mini-spec: the native side measures launch → JS-ready (iOS
5
+ // mach_absolute_time / Android Process.getStartElapsedRealtime);
6
+ // init() calls checkColdStart() once. > 3 s ⇒ one `warn` event,
7
+ // scenario `slow_cold_start`, with the measured ms. Always pushes a
8
+ // `lifecycle` signal so even a fast start shows on the timeline.
9
+ // Graceful no-op when the native module isn't linked (Expo Go,
10
+ // tests).
11
+
12
+ import { pushSignal } from '@goliapkg/sentori-core';
22
13
 
23
- import { startSpan } from '@goliapkg/sentori-core';
14
+ import { getNativeColdStartMs } from './native';
15
+ import { warnDetected } from './verbs';
24
16
 
25
- import { getNativeColdStartMs, getNativeFrameCounters } from './native';
17
+ const SLOW_COLD_START_MS = 3_000;
26
18
 
27
19
  let _coldStartMs: null | number = null;
28
20
  let _coldStartCaptured = false;
29
21
 
30
- /** Read the native-side cold start measurement once. Cached. Returns
31
- * null when the native module isn't linked (Expo Go / tests). */
22
+ /** Read the native-side cold start measurement once. Cached. */
32
23
  export function getColdStartMs(): null | number {
33
24
  if (_coldStartCaptured) return _coldStartMs;
34
25
  _coldStartCaptured = true;
@@ -40,69 +31,18 @@ export function getColdStartMs(): null | number {
40
31
  return _coldStartMs;
41
32
  }
42
33
 
43
- /**
44
- * v0.9.4 #1 — Time-To-Full-Display marker. Host calls this at the
45
- * point the screen is functionally "ready" (data fetched, images
46
- * loaded, etc.) so the dashboard can show real perceived load time
47
- * vs auto-detected TTID.
48
- *
49
- * const h = sentori.markTimeToFullDisplay('Home');
50
- * // ...data fetched, images rendered...
51
- * h.end();
52
- *
53
- * If `.end()` is never called, the handle's span is finished as
54
- * `cancelled` at the next `markTimeToFullDisplay` call (one TTFD in
55
- * flight at a time is the typical case).
56
- */
57
- export type TimeToFullDisplayHandle = {
58
- end: (opts?: { status?: 'cancelled' | 'error' | 'ok' }) => void;
59
- cancel: () => void;
60
- };
61
-
62
- let _activeTtfd: null | {
63
- finish: (status: 'cancelled' | 'error' | 'ok') => void;
64
- route: string;
65
- } = null;
66
-
67
- export function markTimeToFullDisplay(route: string): TimeToFullDisplayHandle {
68
- if (_activeTtfd && _activeTtfd.route !== route) {
69
- _activeTtfd.finish('cancelled');
70
- }
71
- const span = startSpan('react.navigation.ttfd', {
72
- name: route,
73
- tags: { 'nav.route': route, 'vital.kind': 'ttfd' },
74
- });
75
- let finished = false;
76
- const finish = (status: 'cancelled' | 'error' | 'ok'): void => {
77
- if (finished) return;
78
- finished = true;
79
- span.finish({ status });
80
- if (_activeTtfd && _activeTtfd.route === route) _activeTtfd = null;
81
- };
82
- _activeTtfd = { finish, route };
83
- return {
84
- cancel: () => finish('cancelled'),
85
- end: (opts?: { status?: 'cancelled' | 'error' | 'ok' }) =>
86
- finish(opts?.status ?? 'ok'),
87
- };
88
- }
89
-
90
- /** v0.9.4 #1 — read the per-screen slow/frozen frame counts since
91
- * the most recent navigation transition. Native module reads
92
- * CADisplayLink / Choreographer counters; returns null when not
93
- * linked. */
94
- export function getFrameCounters(): null | { slow: number; frozen: number } {
95
- try {
96
- return getNativeFrameCounters();
97
- } catch {
98
- return null;
34
+ /** Called once from init(): signal always, warn when slow. */
35
+ export function checkColdStart(detectEnabled: boolean): void {
36
+ const ms = getColdStartMs();
37
+ if (ms === null) return;
38
+ pushSignal('lifecycle', { phase: 'cold_start', ms });
39
+ if (detectEnabled && ms > SLOW_COLD_START_MS) {
40
+ warnDetected('slow_cold_start', {}, { coldStartMs: ms, thresholdMs: SLOW_COLD_START_MS });
99
41
  }
100
42
  }
101
43
 
102
44
  /** Test-only. */
103
45
  export function __resetMobileVitalsForTests(): void {
104
- if (_activeTtfd) _activeTtfd.finish('cancelled');
105
- _activeTtfd = null;
106
46
  _coldStartCaptured = false;
107
47
  _coldStartMs = null;
108
48
  }
@@ -0,0 +1,92 @@
1
+ // Native pending-crash drain: the crash handlers (Swift/Kotlin)
2
+ // write one JSON file per crash in the legacy event shape; this
3
+ // module converts them to the v1 wire and enqueues.
4
+ //
5
+ // The converter lives on the JS side so the native writers stay
6
+ // untouched — native code doesn't compile in preflight/CI, and a
7
+ // silent .kt/.swift break ships to npm (the v3.0.0 K2 lesson).
8
+
9
+ import { uuidV7 } from '@goliapkg/sentori-core';
10
+ import type { SentoriError, WireEvent } from '@goliapkg/sentori-core';
11
+
12
+ import { drainNativePending } from './native';
13
+ import { enqueue, flush, uploadAttachment } from './transport';
14
+
15
+ type LegacyNativeEvent = {
16
+ id?: string;
17
+ timestamp?: string;
18
+ platform?: string;
19
+ release?: string;
20
+ environment?: string;
21
+ device?: Record<string, unknown>;
22
+ app?: Record<string, unknown>;
23
+ error?: {
24
+ type?: string;
25
+ message?: string;
26
+ stack?: Array<Record<string, unknown>>;
27
+ };
28
+ _pendingAttachments?: Array<{
29
+ kind?: string;
30
+ base64?: string;
31
+ mediaType?: string;
32
+ source?: string;
33
+ }>;
34
+ };
35
+
36
+ const toWire = (raw: LegacyNativeEvent): WireEvent => {
37
+ const platform = raw.platform === 'android' ? 'android' : 'ios';
38
+ const error: SentoriError = {
39
+ type: raw.error?.type ?? 'NativeCrash',
40
+ message: raw.error?.message ?? '',
41
+ stack: (raw.error?.stack ?? []).map((f) => ({
42
+ file: typeof f.file === 'string' ? f.file : undefined,
43
+ function: typeof f.function === 'string' ? f.function : undefined,
44
+ line: typeof f.line === 'number' ? f.line : undefined,
45
+ inApp: typeof f.inApp === 'boolean' ? f.inApp : undefined,
46
+ })),
47
+ cause: null,
48
+ };
49
+ return {
50
+ id: raw.id ?? uuidV7(),
51
+ kind: 'error',
52
+ occurredAt: raw.timestamp ?? new Date().toISOString(),
53
+ platform,
54
+ release: raw.release ?? '',
55
+ environment: raw.environment ?? '',
56
+ payload: {
57
+ error,
58
+ device: raw.device as WireEvent['payload']['device'],
59
+ app: raw.app as WireEvent['payload']['app'],
60
+ nativeCrash: true,
61
+ },
62
+ };
63
+ };
64
+
65
+ /** Drain, convert, enqueue, and ship pre-death attachments. */
66
+ export const shipNativePending = async (): Promise<void> => {
67
+ const files = await drainNativePending();
68
+ for (const text of files) {
69
+ try {
70
+ const raw = JSON.parse(text) as LegacyNativeEvent;
71
+ const pending = raw._pendingAttachments;
72
+ delete raw._pendingAttachments;
73
+ const wire = toWire(raw);
74
+ enqueue(wire);
75
+ if (pending && wire.id) {
76
+ for (const att of pending) {
77
+ if (att.base64 && att.kind) {
78
+ void uploadAttachment(
79
+ wire.id,
80
+ att.kind as import('@goliapkg/sentori-core').AttachmentKind,
81
+ { base64: att.base64, mediaType: att.mediaType ?? 'application/octet-stream' },
82
+ { source: (att.source as 'android' | 'ios' | 'js') ?? 'ios' },
83
+ );
84
+ }
85
+ }
86
+ }
87
+ } catch {
88
+ // One corrupt crash file must not block the rest.
89
+ }
90
+ }
91
+ if (files.length > 0) await flush();
92
+ };
package/src/navigation.ts CHANGED
@@ -1,35 +1,17 @@
1
- // Phase 35 sub-C: react-navigation auto-instrumentation.
2
- // Phase 39 sub-B: the open `react.navigation` span is also made the
3
- // active span for that screen's lifetime, so the screen's
4
- // `http.client` spans (and any other startSpan() calls) attach to it
5
- // as children — one trace per screen instead of one per request.
1
+ // react-navigation auto-instrumentation — v1 role: feed the signal
2
+ // ring (`nav` signals) and remember the current screen so warn
3
+ // surfaces can name where they happened. The span/trace/metric
4
+ // machinery this file used to drive is gone with the APM vocabulary.
6
5
  //
7
6
  // Mount `useTraceNavigation(navigationRef)` next to your
8
- // `<NavigationContainer ref={navigationRef}>`. Span names are
9
- // `<from> → <to>` (or just the route name for the first screen) so
10
- // the trace list reads as a navigation log.
11
- //
12
- // react-navigation is an OPTIONAL peer dependency — apps that don't
13
- // use it never have to install it. The hook doesn't import from
14
- // @react-navigation/native; consumers pass in the ref they already
15
- // have, and we read its state via the public `getCurrentRoute()`.
16
- //
17
- // Caveat (active-span on RN is a module variable): requests fired
18
- // from a `setTimeout` / background poll / detached promise after the
19
- // screen settled may not see the nav span as active. If you want such
20
- // a request parented to the current screen, pass it explicitly:
21
- // `startSpan(op, { parent: activeSpan() })`.
7
+ // `<NavigationContainer ref={navigationRef}>`. react-navigation
8
+ // stays an OPTIONAL peer dependency: the hook never imports from
9
+ // @react-navigation/native — consumers pass the ref they already
10
+ // have and we read `getCurrentRoute()`.
22
11
 
23
12
  import { useEffect, useRef } from 'react';
24
13
 
25
- import { emitMetric, setActiveSpan, startSpan, type SpanHandle } from '@goliapkg/sentori-core';
26
-
27
- import {
28
- getNativeFrameCounters,
29
- resetNativeFrameCounters,
30
- } from './native';
31
- import { track } from './track';
32
- import { captureStep } from './trail';
14
+ import { pushSignal } from '@goliapkg/sentori-core';
33
15
 
34
16
  /** Minimal contract: anything with `addListener('state', cb)` and
35
17
  * `getCurrentRoute()` works. The real @react-navigation/native
@@ -39,150 +21,60 @@ export type NavigationRefLike = {
39
21
  getCurrentRoute: () => { name: string } | undefined;
40
22
  };
41
23
 
42
- /** Process-global last-known route — populated by the navigation
43
- * span path below, read by the analytics heartbeat so its per-tick
44
- * payload includes the screen the user is on right now. Initial null
45
- * when no nav has happened yet (app just launched, dev-launcher,
46
- * splash). */
24
+ /** Process-global last-known route — surfaces on detected warns
25
+ * (`surface.screen`) and rides nav signals. Null before the first
26
+ * navigation (splash, dev launcher). */
47
27
  let _lastRoute: null | string = null;
48
28
 
29
+ export function currentScreen(): string | undefined {
30
+ return _lastRoute ?? undefined;
31
+ }
32
+
33
+ /** Kept for compat with earlier integrations. */
49
34
  export function getLastRoute(): null | string {
50
35
  return _lastRoute;
51
36
  }
52
37
 
53
- /**
54
- * Subscribe to react-navigation state changes and emit a
55
- * `react.navigation` span per screen (including the initial one),
56
- * each a fresh trace root. The span is kept active while the screen
57
- * is current; child spans created in that window attribute up to it.
58
- *
59
- * import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'
60
- * import { useTraceNavigation } from '@goliapkg/sentori-react-native'
61
- *
62
- * function App() {
63
- * const navigationRef = useNavigationContainerRef()
64
- * useTraceNavigation(navigationRef)
65
- * return <NavigationContainer ref={navigationRef}>{...}</NavigationContainer>
66
- * }
67
- *
68
- * Each span carries `{ nav.from, nav.to }` tags.
69
- */
70
38
  export function useTraceNavigation(navigationRef: NavigationRefLike): void {
71
- // Latest route name we've observed.
72
39
  const lastRouteRef = useRef<null | string>(null);
73
- // Wall-clock ms when the last screen was entered. Drives dwell time
74
- // attached to the leaving span + the next captureStep breadcrumb.
75
- const lastRouteEnteredAtRef = useRef<null | number>(null);
76
- // Span for the screen the user is currently on. Finished when the
77
- // next screen is entered (or on unmount).
78
- const openSpanRef = useRef<null | SpanHandle>(null);
40
+ const enteredAtRef = useRef<null | number>(null);
79
41
 
80
42
  useEffect(() => {
81
43
  if (typeof navigationRef.addListener !== 'function') return;
82
44
  if (typeof navigationRef.getCurrentRoute !== 'function') return;
83
45
 
84
- // Each screen gets its own trace root — detach from whatever the
85
- // previous screen's span was (we keep it active, so without
86
- // `parent: null` the new one would nest under it).
87
- const openScreenSpan = (
88
- from: null | string,
89
- to: string,
90
- prevDwellMs: null | number,
91
- ) => {
92
- const span = startSpan('react.navigation', {
93
- name: from ? `${from} → ${to}` : to,
94
- parent: null,
95
- tags: { 'nav.from': from ?? '', 'nav.to': to },
96
- });
97
- openSpanRef.current = span;
98
- setActiveSpan(span);
46
+ const noteScreen = (from: null | string, to: string) => {
47
+ const dwellMs =
48
+ enteredAtRef.current !== null
49
+ ? Math.max(0, Date.now() - enteredAtRef.current)
50
+ : undefined;
99
51
  lastRouteRef.current = to;
100
52
  _lastRoute = to;
101
- lastRouteEnteredAtRef.current = Date.now();
102
- // v0.8.0-b — dwell on the previous screen surfaces in the
103
- // session trail. The leaving span's `durationMs` already
104
- // carries the same number, but the trail is the most-glanced
105
- // surface so we duplicate it as breadcrumb data. No bytes wasted:
106
- // a breadcrumb without sessionTrail enabled is a no-op.
107
- captureStep(`screen:${to}`, {
108
- breadcrumb: {
109
- data: prevDwellMs !== null ? { dwellMsPrev: prevDwellMs } : undefined,
110
- message: from ? `${from} → ${to}` : to,
111
- type: 'navigation',
112
- },
113
- });
114
- // v1.1 chunk B — auto-pageview. Pushed into the track ring
115
- // alongside the navigation span so the analytics path sees
116
- // every screen entry without app code needing to wire its own
117
- // pageview calls. `from`/`dwellMsPrev` ride in props so the
118
- // Behavior view (chunk D) can reconstruct the user journey
119
- // even when only the track stream is queried.
120
- track(
121
- '$pageview',
122
- prevDwellMs !== null
123
- ? { from: from ?? null, dwellMsPrev: prevDwellMs }
124
- : { from: from ?? null },
53
+ enteredAtRef.current = Date.now();
54
+ pushSignal('nav', {
55
+ from: from ?? undefined,
125
56
  to,
126
- );
127
- };
128
-
129
- const finishOpenSpanWithDwell = (): null | number => {
130
- const span = openSpanRef.current;
131
- const enteredAt = lastRouteEnteredAtRef.current;
132
- if (!span) return null;
133
- const dwellMs = enteredAt !== null ? Math.max(0, Date.now() - enteredAt) : null;
134
- // v0.9.4 #1 — drain native frame counters at screen-leave.
135
- // Empty/null when native module not linked; tags omitted.
136
- const fc = getNativeFrameCounters();
137
- const finishTags: Record<string, string> = {};
138
- if (dwellMs !== null) finishTags['nav.dwell_ms'] = String(dwellMs);
139
- if (fc) {
140
- finishTags['vital.slow_frames'] = String(fc.slow);
141
- finishTags['vital.frozen_frames'] = String(fc.frozen);
142
- }
143
- span.finish({
144
- status: 'ok',
145
- tags: Object.keys(finishTags).length > 0 ? finishTags : undefined,
57
+ dwellMsPrev: dwellMs,
146
58
  });
147
- // Reset counters for the next screen.
148
- resetNativeFrameCounters();
149
- return dwellMs;
150
59
  };
151
60
 
152
- // Open a span for the initial screen so its requests are grouped
153
- // too (auth / config / first data load are usually the busiest
154
- // screen of a session).
155
61
  const initial = navigationRef.getCurrentRoute()?.name ?? null;
156
- if (initial !== null) openScreenSpan(null, initial, null);
157
- else lastRouteRef.current = null;
62
+ if (initial !== null) noteScreen(null, initial);
158
63
 
159
64
  const unsubscribe = navigationRef.addListener('state', () => {
160
65
  const next = navigationRef.getCurrentRoute()?.name ?? null;
161
66
  const prev = lastRouteRef.current;
162
67
  if (next === null || next === prev) return;
163
- const dwellMs = finishOpenSpanWithDwell();
164
- // v2.1 W2 — emit dwell as a runtime metric so the BI panel
165
- // can rank screens by how long users sat on them. Tagged
166
- // with `from` + `to` so a release-over-release diff can spot
167
- // a 2x dwell regression on Onboarding etc. emit is cheap +
168
- // bounded by the core ring cap; if `capture.runtimeMetrics:
169
- // false` the flush is off but the ring still records up to
170
- // the cap before dropping oldest.
171
- if (typeof dwellMs === 'number' && dwellMs >= 0) {
172
- emitMetric('runtime.route_nav_ms', dwellMs, {
173
- from: prev ?? '',
174
- to: next ?? '',
175
- });
176
- }
177
- openScreenSpan(prev, next, dwellMs);
68
+ noteScreen(prev, next);
178
69
  });
179
70
 
180
71
  return () => {
181
72
  unsubscribe();
182
- finishOpenSpanWithDwell();
183
- openSpanRef.current = null;
184
- lastRouteEnteredAtRef.current = null;
185
- setActiveSpan(null);
73
+ enteredAtRef.current = null;
186
74
  };
187
75
  }, [navigationRef]);
188
76
  }
77
+
78
+ export const __resetForTests = (): void => {
79
+ _lastRoute = null;
80
+ };
package/src/netinfo.ts CHANGED
@@ -5,11 +5,11 @@
5
5
  // standard library), we subscribe at SDK init time and cache the
6
6
  // latest network state. `collectDevice()` reads the cache
7
7
  // synchronously at capture time. If the peer isn't installed, the
8
- // cache stays `undefined` and `device.networkType` is omitted —
8
+ // cache stays `undefined` and `device.network` is omitted —
9
9
  // no warning, no crash.
10
10
  //
11
11
  // We collapse NetInfo's enum into the smaller set the protocol
12
- // allows (see `Device.networkType` in `sdk/core/src/types.ts`):
12
+ // allows (see `Device.network` in `sdk/core/src/types.ts`):
13
13
  // `wifi`, `2g/3g/4g/slow-2g`, `offline`, `unknown`. 5G collapses
14
14
  // into `4g` because the schema doesn't have a 5g slot yet; the
15
15
  // information loss is acceptable for an analytics dimension.
@@ -18,7 +18,7 @@ import type { Device } from '@goliapkg/sentori-core';
18
18
 
19
19
  import { isNativeModuleLinked } from './native-loader';
20
20
 
21
- type NetworkType = Device['networkType'];
21
+ type NetworkType = Device['network'];
22
22
 
23
23
  type NetInfoState = {
24
24
  details?: { cellularGeneration?: null | string };
package/src/push.ts CHANGED
@@ -21,9 +21,8 @@
21
21
  // buffered events while the app is foreground. Pauses on
22
22
  // background, resumes on active, per the perf iron rule.
23
23
 
24
- import { addBreadcrumb, logger } from '@goliapkg/sentori-core'
24
+ import { logger, pushSignal } from '@goliapkg/sentori-core'
25
25
 
26
- import { track } from './track.js'
27
26
  // AppState is RN-only; we treat it dynamically so the SDK keeps
28
27
  // importing cleanly under Bun / web.
29
28
  type AppStateModule = {
@@ -318,13 +317,11 @@ function autoCorrelate(
318
317
  // Breadcrumb buffer: O(1) in-memory push. Tag both event types
319
318
  // ('received' vs 'opened') so a later captureException shows
320
319
  // whether the user actually saw the push.
321
- addBreadcrumb('push', { body, msgId, opened: eventType === 'opened', provider, title })
320
+ pushSignal('push', { body: undefined, msgId, opened: eventType === 'opened', provider, title })
322
321
 
323
322
  // Track event: reuses the existing SDK event pipeline. Two
324
323
  // distinct names so dashboards can separate delivery from open.
325
324
  const trackName = eventType === 'opened' ? 'sentori.push.opened' : 'sentori.push.received'
326
- track(trackName, { msgId, provider })
327
-
328
325
  // Enqueue ack — batched, see drainAckQueue.
329
326
  enqueueAck(msgId)
330
327
  }