@goliapkg/sentori-react-native 4.0.0 → 5.1.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 (308) hide show
  1. package/MIGRATION.md +131 -0
  2. package/README.md +40 -30
  3. package/android/build.gradle +21 -0
  4. package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
  5. package/android/src/main/java/com/sentori/SentoriModule.kt +8 -0
  6. package/android/src/main/java/com/sentori/SentoriScreenshotCapture.kt +36 -4
  7. package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
  8. package/android/src/test/resources/robolectric.properties +10 -0
  9. package/ios/SentoriCrashHandler.swift +48 -4
  10. package/ios/SentoriModule.swift +12 -0
  11. package/ios/SentoriScreenshotCapture.swift +34 -3
  12. package/lib/config.d.ts +17 -47
  13. package/lib/config.d.ts.map +1 -1
  14. package/lib/config.js +3 -0
  15. package/lib/config.js.map +1 -1
  16. package/lib/device.d.ts +5 -0
  17. package/lib/device.d.ts.map +1 -0
  18. package/lib/device.js +29 -0
  19. package/lib/device.js.map +1 -0
  20. package/lib/emit-hooks.d.ts +7 -0
  21. package/lib/emit-hooks.d.ts.map +1 -0
  22. package/lib/emit-hooks.js +24 -0
  23. package/lib/emit-hooks.js.map +1 -0
  24. package/lib/error-boundary.d.ts.map +1 -1
  25. package/lib/error-boundary.js +1 -1
  26. package/lib/error-boundary.js.map +1 -1
  27. package/lib/feature-flags.d.ts.map +1 -1
  28. package/lib/handlers/dev-symbolicate.d.ts +1 -1
  29. package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
  30. package/lib/handlers/dev-symbolicate.js +2 -0
  31. package/lib/handlers/dev-symbolicate.js.map +1 -1
  32. package/lib/handlers/global.js +1 -1
  33. package/lib/handlers/global.js.map +1 -1
  34. package/lib/handlers/lifecycle.js +3 -3
  35. package/lib/handlers/lifecycle.js.map +1 -1
  36. package/lib/handlers/network.d.ts +2 -2
  37. package/lib/handlers/network.d.ts.map +1 -1
  38. package/lib/handlers/network.js +69 -187
  39. package/lib/handlers/network.js.map +1 -1
  40. package/lib/handlers/promise.js +1 -1
  41. package/lib/handlers/promise.js.map +1 -1
  42. package/lib/handlers/screenshot.d.ts.map +1 -1
  43. package/lib/handlers/screenshot.js +3 -12
  44. package/lib/handlers/screenshot.js.map +1 -1
  45. package/lib/index.d.ts +32 -109
  46. package/lib/index.d.ts.map +1 -1
  47. package/lib/index.js +54 -103
  48. package/lib/index.js.map +1 -1
  49. package/lib/init.d.ts +3 -150
  50. package/lib/init.d.ts.map +1 -1
  51. package/lib/init.js +100 -261
  52. package/lib/init.js.map +1 -1
  53. package/lib/long-task-monitor.d.ts +1 -7
  54. package/lib/long-task-monitor.d.ts.map +1 -1
  55. package/lib/long-task-monitor.js +37 -58
  56. package/lib/long-task-monitor.js.map +1 -1
  57. package/lib/mask.d.ts +5 -13
  58. package/lib/mask.d.ts.map +1 -1
  59. package/lib/mask.js +29 -30
  60. package/lib/mask.js.map +1 -1
  61. package/lib/mobile-vitals.d.ts +3 -31
  62. package/lib/mobile-vitals.d.ts.map +1 -1
  63. package/lib/mobile-vitals.js +22 -61
  64. package/lib/mobile-vitals.js.map +1 -1
  65. package/lib/native-pending.d.ts +3 -0
  66. package/lib/native-pending.d.ts.map +1 -0
  67. package/lib/native-pending.js +64 -0
  68. package/lib/native-pending.js.map +1 -0
  69. package/lib/native.d.ts +4 -11
  70. package/lib/native.d.ts.map +1 -1
  71. package/lib/native.js +23 -0
  72. package/lib/native.js.map +1 -1
  73. package/lib/navigation.d.ts +3 -17
  74. package/lib/navigation.d.ts.map +1 -1
  75. package/lib/navigation.js +32 -133
  76. package/lib/navigation.js.map +1 -1
  77. package/lib/netinfo.d.ts +1 -1
  78. package/lib/netinfo.d.ts.map +1 -1
  79. package/lib/netinfo.js +2 -2
  80. package/lib/netinfo.js.map +1 -1
  81. package/lib/push.d.ts.map +1 -1
  82. package/lib/push.js +2 -4
  83. package/lib/push.js.map +1 -1
  84. package/lib/rage-tap.d.ts.map +1 -1
  85. package/lib/rage-tap.js +56 -23
  86. package/lib/rage-tap.js.map +1 -1
  87. package/lib/replay-screens.d.ts +15 -0
  88. package/lib/replay-screens.d.ts.map +1 -0
  89. package/lib/replay-screens.js +87 -0
  90. package/lib/replay-screens.js.map +1 -0
  91. package/lib/replay.d.ts.map +1 -1
  92. package/lib/replay.js +3 -24
  93. package/lib/replay.js.map +1 -1
  94. package/lib/responsiveness-detector.d.ts +18 -0
  95. package/lib/responsiveness-detector.d.ts.map +1 -0
  96. package/lib/responsiveness-detector.js +55 -0
  97. package/lib/responsiveness-detector.js.map +1 -0
  98. package/lib/scope.d.ts +7 -0
  99. package/lib/scope.d.ts.map +1 -0
  100. package/lib/scope.js +41 -0
  101. package/lib/scope.js.map +1 -0
  102. package/lib/transport.d.ts +19 -92
  103. package/lib/transport.d.ts.map +1 -1
  104. package/lib/transport.js +75 -288
  105. package/lib/transport.js.map +1 -1
  106. package/lib/verbs.d.ts +16 -0
  107. package/lib/verbs.d.ts.map +1 -0
  108. package/lib/verbs.js +153 -0
  109. package/lib/verbs.js.map +1 -0
  110. package/package.json +4 -3
  111. package/src/__tests__/iron-rule.test.ts +128 -0
  112. package/src/__tests__/replay-screens.test.ts +63 -0
  113. package/src/__tests__/responsiveness-detector.test.ts +74 -0
  114. package/src/__tests__/screenshot.test.ts +0 -2
  115. package/src/__tests__/transport.test.ts +66 -213
  116. package/src/__tests__/verbs.test.ts +91 -0
  117. package/src/config.ts +20 -47
  118. package/src/device.ts +36 -0
  119. package/src/emit-hooks.ts +30 -0
  120. package/src/error-boundary.tsx +1 -1
  121. package/src/handlers/dev-symbolicate.ts +2 -1
  122. package/src/handlers/global.ts +1 -1
  123. package/src/handlers/lifecycle.ts +3 -3
  124. package/src/handlers/network.ts +95 -211
  125. package/src/handlers/promise.ts +1 -1
  126. package/src/handlers/screenshot.ts +3 -11
  127. package/src/index.ts +84 -215
  128. package/src/init.ts +115 -449
  129. package/src/long-task-monitor.ts +42 -64
  130. package/src/mask.ts +29 -31
  131. package/src/mobile-vitals.ts +22 -82
  132. package/src/native-pending.ts +92 -0
  133. package/src/native.ts +33 -0
  134. package/src/navigation.ts +34 -142
  135. package/src/netinfo.ts +3 -3
  136. package/src/push.ts +2 -5
  137. package/src/rage-tap.tsx +72 -25
  138. package/src/replay-screens.ts +95 -0
  139. package/src/replay.ts +3 -20
  140. package/src/responsiveness-detector.ts +66 -0
  141. package/src/scope.ts +50 -0
  142. package/src/transport.ts +84 -371
  143. package/src/verbs.ts +198 -0
  144. package/lib/breadcrumbs.d.ts +0 -18
  145. package/lib/breadcrumbs.d.ts.map +0 -1
  146. package/lib/breadcrumbs.js +0 -42
  147. package/lib/breadcrumbs.js.map +0 -1
  148. package/lib/bundle-info.d.ts +0 -12
  149. package/lib/bundle-info.d.ts.map +0 -1
  150. package/lib/bundle-info.js +0 -73
  151. package/lib/bundle-info.js.map +0 -1
  152. package/lib/capture.d.ts +0 -140
  153. package/lib/capture.d.ts.map +0 -1
  154. package/lib/capture.js +0 -511
  155. package/lib/capture.js.map +0 -1
  156. package/lib/compat/sentry.d.ts +0 -126
  157. package/lib/compat/sentry.d.ts.map +0 -1
  158. package/lib/compat/sentry.js +0 -332
  159. package/lib/compat/sentry.js.map +0 -1
  160. package/lib/control-channel.d.ts +0 -6
  161. package/lib/control-channel.d.ts.map +0 -1
  162. package/lib/control-channel.js +0 -96
  163. package/lib/control-channel.js.map +0 -1
  164. package/lib/expo-compat.d.ts +0 -270
  165. package/lib/expo-compat.d.ts.map +0 -1
  166. package/lib/expo-compat.js +0 -500
  167. package/lib/expo-compat.js.map +0 -1
  168. package/lib/feedback-widget.d.ts +0 -35
  169. package/lib/feedback-widget.d.ts.map +0 -1
  170. package/lib/feedback-widget.js +0 -193
  171. package/lib/feedback-widget.js.map +0 -1
  172. package/lib/feedback.d.ts +0 -2
  173. package/lib/feedback.d.ts.map +0 -1
  174. package/lib/feedback.js +0 -17
  175. package/lib/feedback.js.map +0 -1
  176. package/lib/heartbeat.d.ts +0 -9
  177. package/lib/heartbeat.d.ts.map +0 -1
  178. package/lib/heartbeat.js +0 -138
  179. package/lib/heartbeat.js.map +0 -1
  180. package/lib/index.js.bak +0 -64
  181. package/lib/install-id.d.ts +0 -17
  182. package/lib/install-id.d.ts.map +0 -1
  183. package/lib/install-id.js +0 -125
  184. package/lib/install-id.js.map +0 -1
  185. package/lib/lifecycle.d.ts +0 -28
  186. package/lib/lifecycle.d.ts.map +0 -1
  187. package/lib/lifecycle.js +0 -73
  188. package/lib/lifecycle.js.map +0 -1
  189. package/lib/measure.d.ts +0 -4
  190. package/lib/measure.d.ts.map +0 -1
  191. package/lib/measure.js +0 -25
  192. package/lib/measure.js.map +0 -1
  193. package/lib/metrics.d.ts +0 -24
  194. package/lib/metrics.d.ts.map +0 -1
  195. package/lib/metrics.js +0 -81
  196. package/lib/metrics.js.map +0 -1
  197. package/lib/pre-crash-sentinel.d.ts +0 -12
  198. package/lib/pre-crash-sentinel.d.ts.map +0 -1
  199. package/lib/pre-crash-sentinel.js +0 -116
  200. package/lib/pre-crash-sentinel.js.map +0 -1
  201. package/lib/report-security.d.ts +0 -40
  202. package/lib/report-security.d.ts.map +0 -1
  203. package/lib/report-security.js +0 -159
  204. package/lib/report-security.js.map +0 -1
  205. package/lib/runtime-metrics-fps.d.ts +0 -19
  206. package/lib/runtime-metrics-fps.d.ts.map +0 -1
  207. package/lib/runtime-metrics-fps.js +0 -128
  208. package/lib/runtime-metrics-fps.js.map +0 -1
  209. package/lib/runtime-metrics-heap.d.ts +0 -13
  210. package/lib/runtime-metrics-heap.d.ts.map +0 -1
  211. package/lib/runtime-metrics-heap.js +0 -67
  212. package/lib/runtime-metrics-heap.js.map +0 -1
  213. package/lib/runtime-metrics-network.d.ts +0 -23
  214. package/lib/runtime-metrics-network.d.ts.map +0 -1
  215. package/lib/runtime-metrics-network.js +0 -99
  216. package/lib/runtime-metrics-network.js.map +0 -1
  217. package/lib/runtime-metrics.d.ts +0 -43
  218. package/lib/runtime-metrics.d.ts.map +0 -1
  219. package/lib/runtime-metrics.js +0 -115
  220. package/lib/runtime-metrics.js.map +0 -1
  221. package/lib/sample-profiler.d.ts +0 -21
  222. package/lib/sample-profiler.d.ts.map +0 -1
  223. package/lib/sample-profiler.js +0 -152
  224. package/lib/sample-profiler.js.map +0 -1
  225. package/lib/sentinel-context.d.ts +0 -4
  226. package/lib/sentinel-context.d.ts.map +0 -1
  227. package/lib/sentinel-context.js +0 -52
  228. package/lib/sentinel-context.js.map +0 -1
  229. package/lib/session-tracker.d.ts +0 -6
  230. package/lib/session-tracker.d.ts.map +0 -1
  231. package/lib/session-tracker.js +0 -50
  232. package/lib/session-tracker.js.map +0 -1
  233. package/lib/stack.d.ts +0 -3
  234. package/lib/stack.d.ts.map +0 -1
  235. package/lib/stack.js +0 -7
  236. package/lib/stack.js.map +0 -1
  237. package/lib/state-snapshots.d.ts +0 -33
  238. package/lib/state-snapshots.d.ts.map +0 -1
  239. package/lib/state-snapshots.js +0 -109
  240. package/lib/state-snapshots.js.map +0 -1
  241. package/lib/track.d.ts +0 -34
  242. package/lib/track.d.ts.map +0 -1
  243. package/lib/track.js +0 -107
  244. package/lib/track.js.map +0 -1
  245. package/lib/trail.d.ts +0 -12
  246. package/lib/trail.d.ts.map +0 -1
  247. package/lib/trail.js +0 -28
  248. package/lib/trail.js.map +0 -1
  249. package/lib/trust-score.d.ts +0 -20
  250. package/lib/trust-score.d.ts.map +0 -1
  251. package/lib/trust-score.js +0 -151
  252. package/lib/trust-score.js.map +0 -1
  253. package/lib/types.d.ts +0 -2
  254. package/lib/types.d.ts.map +0 -1
  255. package/lib/types.js +0 -4
  256. package/lib/types.js.map +0 -1
  257. package/lib/uuid.d.ts +0 -2
  258. package/lib/uuid.d.ts.map +0 -1
  259. package/lib/uuid.js +0 -3
  260. package/lib/uuid.js.map +0 -1
  261. package/src/__tests__/before-send.test.ts +0 -72
  262. package/src/__tests__/breadcrumbs.test.ts +0 -44
  263. package/src/__tests__/capture-replay.test.ts +0 -150
  264. package/src/__tests__/compat-sentry.test.ts +0 -121
  265. package/src/__tests__/control-channel.test.ts +0 -111
  266. package/src/__tests__/install-id.test.ts +0 -60
  267. package/src/__tests__/long-task-monitor.test.ts +0 -59
  268. package/src/__tests__/measure.test.ts +0 -45
  269. package/src/__tests__/navigation.test.ts +0 -175
  270. package/src/__tests__/push.test.ts +0 -238
  271. package/src/__tests__/report-security.test.ts +0 -106
  272. package/src/__tests__/runtime-metrics-instruments.test.ts +0 -109
  273. package/src/__tests__/runtime-metrics-network.test.ts +0 -78
  274. package/src/__tests__/sample-profiler.test.ts +0 -59
  275. package/src/__tests__/stack.test.ts +0 -45
  276. package/src/__tests__/state-snapshots.test.ts +0 -119
  277. package/src/__tests__/tracing.test.ts +0 -201
  278. package/src/__tests__/track.test.ts +0 -91
  279. package/src/__tests__/uuid.test.ts +0 -41
  280. package/src/breadcrumbs.ts +0 -57
  281. package/src/bundle-info.ts +0 -95
  282. package/src/capture.ts +0 -660
  283. package/src/compat/sentry.ts +0 -490
  284. package/src/control-channel.ts +0 -98
  285. package/src/expo-compat.ts +0 -698
  286. package/src/feedback-widget.tsx +0 -316
  287. package/src/feedback.ts +0 -21
  288. package/src/heartbeat.ts +0 -157
  289. package/src/install-id.ts +0 -146
  290. package/src/lifecycle.ts +0 -76
  291. package/src/measure.ts +0 -28
  292. package/src/metrics.ts +0 -94
  293. package/src/pre-crash-sentinel.ts +0 -140
  294. package/src/report-security.ts +0 -165
  295. package/src/runtime-metrics-fps.ts +0 -137
  296. package/src/runtime-metrics-heap.ts +0 -78
  297. package/src/runtime-metrics-network.ts +0 -103
  298. package/src/runtime-metrics.ts +0 -122
  299. package/src/sample-profiler.ts +0 -169
  300. package/src/sentinel-context.ts +0 -60
  301. package/src/session-tracker.ts +0 -53
  302. package/src/stack.ts +0 -10
  303. package/src/state-snapshots.ts +0 -133
  304. package/src/track.ts +0 -123
  305. package/src/trail.ts +0 -32
  306. package/src/trust-score.ts +0 -176
  307. package/src/types.ts +0 -21
  308. 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
  }
package/src/mask.ts CHANGED
@@ -1,41 +1,39 @@
1
- // v0.7.3 — mask discovery via consumer-supplied query callback.
1
+ // Screen masking — the privacy half of visual replay.
2
2
  //
3
- // The SDK does not own a registry of masked regions. The host app
4
- // keeps its own (e.g. a `Set<string>` updated as `<Maskable>`
5
- // components mount/unmount) and hands the SDK a thunk that returns
6
- // the current list of native-IDs to redact. The SDK calls the thunk
7
- // once per screenshot capture — cheap, called rarely, only on error.
3
+ // The host registers a query returning the `nativeID`s of views
4
+ // that must never appear in a screenshot (camera feeds, user
5
+ // identity, payment fields). Native paints black rectangles over
6
+ // those subtrees in the same render pass, so the pixels never
7
+ // leave the device.
8
8
  //
9
- // Why this shape: a logging SDK should never live on the render
10
- // path. Earlier iterations exported `<MaskRegion>` (React component)
11
- // and `setMaskedNode` (imperative ref helper), which forced every
12
- // PII-bearing UI file to import from the SDK and put SDK bugs in
13
- // the user's render tree. This module is JS-only — no React, no JSX,
14
- // no native module touch — so swapping or removing the SDK doesn't
15
- // affect rendering.
9
+ // The query runs on every frame tick; keep it cheap (return a
10
+ // cached array). A throwing query is swallowed and masks NOTHING
11
+ // that tick — so a broken query fails visible-in-review rather
12
+ // than silently, and can never take the capture path down.
13
+
14
+ import { reportInternal } from '@goliapkg/sentori-core';
16
15
 
17
16
  type MaskQuery = () => string[];
18
17
 
19
18
  let _query: MaskQuery | null = null;
20
19
 
21
- /**
22
- * Register a callback the SDK calls right before each screenshot
23
- * capture. Return the native-IDs (the `nativeID` prop on the RN
24
- * `<View>`) that should be blacked-out in the captured image.
25
- *
26
- * Idempotent: a second call replaces the first. Pass `null` (or
27
- * call `clearMaskQuery`) to detach.
28
- */
29
- export function registerMaskQuery(query: MaskQuery): void {
20
+ /** Register (or with `null`, clear) the mask query. */
21
+ export const registerMaskQuery = (query: MaskQuery | null): void => {
30
22
  _query = query;
31
- }
23
+ };
32
24
 
33
- /** Unregister. Mostly for tests / teardown. */
34
- export function clearMaskQuery(): void {
35
- _query = null;
36
- }
25
+ /** The current mask list; empty when unregistered or throwing. */
26
+ export const maskedNativeIds = (): string[] => {
27
+ if (!_query) return [];
28
+ try {
29
+ const ids = _query();
30
+ return Array.isArray(ids) ? ids.filter((x) => typeof x === 'string') : [];
31
+ } catch (e) {
32
+ reportInternal('mask-query', e);
33
+ return [];
34
+ }
35
+ };
37
36
 
38
- /** Internal — read by `handlers/screenshot.ts` at capture time. */
39
- export function getRegisteredMaskQuery(): MaskQuery | null {
40
- return _query;
41
- }
37
+ export const __resetForTests = (): void => {
38
+ _query = null;
39
+ };
@@ -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/native.ts CHANGED
@@ -69,6 +69,13 @@ type SentoriNativeModule = {
69
69
  * snapshot string or null on failure.
70
70
  */
71
71
  captureWireframe?: (maskedIds: string[]) => null | string
72
+
73
+ /** v5.1 — low-bitrate replay frame; absent on older native builds. */
74
+ captureReplayFrame?: (
75
+ maskedIds: string[],
76
+ longEdgePx: number,
77
+ quality: number,
78
+ ) => Promise<null | { base64: string; mediaType: string }>
72
79
  /**
73
80
  * v0.9.12 — diagnostic readout for the wireframe path. Cheap
74
81
  * synchronous call that returns the path the last `captureWireframe`
@@ -387,6 +394,32 @@ export function getRecentNativeException(): null | {
387
394
  * Callers must treat `null` as "no screenshot this round" — the
388
395
  * error event still ships, just without a thumbnail.
389
396
  */
397
+ /** v5.1 — one low-bitrate frame for the screens replay ring.
398
+ * Null on every failure mode (module unbound, method missing on an
399
+ * older native build, capture failed); the ring just skips a beat. */
400
+ let warnedNoReplayFrame = false
401
+ export async function captureNativeReplayFrame(
402
+ maskedIds: string[],
403
+ longEdgePx: number,
404
+ quality: number,
405
+ ): Promise<null | { base64: string; mediaType: string }> {
406
+ const n = native()
407
+ if (!n) return null
408
+ if (!n.captureReplayFrame) {
409
+ if (!warnedNoReplayFrame) {
410
+ warnedNoReplayFrame = true
411
+ logger.warn('native', 'captureReplayFrame missing — rebuild the native app for visual replay')
412
+ }
413
+ return null
414
+ }
415
+ try {
416
+ return (await n.captureReplayFrame(maskedIds, longEdgePx, quality)) ?? null
417
+ } catch (e) {
418
+ logger.warn('native', 'captureReplayFrame threw', e)
419
+ return null
420
+ }
421
+ }
422
+
390
423
  export async function captureNativeScreenshotWithMask(
391
424
  maskedIds: string[],
392
425
  ): Promise<null | { base64: string; mediaType: string }> {