@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
package/src/init.ts CHANGED
@@ -1,472 +1,138 @@
1
- import { setLogLevel } from '@goliapkg/sentori-core';
1
+ // sentori.init(config) — the single configuration entry point
2
+ // (design.md §4). Synchronous, never throws; a bad config degrades
3
+ // every verb to a no-op with one console.warn, never a crash
4
+ // (failure-isolation iron rule).
2
5
 
3
- import { type ReadyInfo, setConfig } from './config';
6
+ import { safeFn, setLogLevel } from '@goliapkg/sentori-core';
7
+ import type { InitConfig } from '@goliapkg/sentori-core';
8
+
9
+ import { setConfig } from './config';
10
+ import { registerEmitHook } from './emit-hooks';
4
11
  import { installGlobalHandler } from './handlers/global';
5
12
  import { installLifecycleHandler } from './handlers/lifecycle';
6
- import { installPromiseHandler } from './handlers/promise';
7
13
  import { installNetworkHandler } from './handlers/network';
8
- import { getBundleInfo } from './bundle-info';
9
- import {
10
- markLaunchCompleted,
11
- runLaunchCrashGuard,
12
- } from './launch-crash-guard';
13
- import { getInstallId } from './install-id';
14
- import { startMetricsTimer } from './metrics';
15
- import {
16
- emitColdStart,
17
- markColdStartT0,
18
- startRuntimeMetricsTimer,
19
- } from './runtime-metrics';
20
- import { startFpsInstrument } from './runtime-metrics-fps';
21
- import { startHeapInstrument } from './runtime-metrics-heap';
22
- import { startNetworkBytesInstrument } from './runtime-metrics-network';
23
- import { startTrackTimer } from './track';
24
- import { drainNativePending, markNativeJsBridgeReady, setNativeConfig } from './native';
25
- import { getColdStartMs } from './mobile-vitals';
26
- import { startHeartbeat, type HeartbeatOptions } from './heartbeat';
27
- import { startSpan } from '@goliapkg/sentori-core';
28
- import { startControlChannel } from './control-channel';
14
+ import { installPromiseHandler } from './handlers/promise';
29
15
  import { startLongTaskMonitor } from './long-task-monitor';
30
- import { startNetworkTypeWatch } from './netinfo';
31
- import { startPreCrashSentinel, type PreCrashChannel } from './pre-crash-sentinel';
32
- import { startReplay } from './replay';
33
- import { startSampleProfiler } from './sample-profiler';
34
- import { startSession } from './session-tracker';
35
- import {
36
- drainOfflineQueue,
37
- enqueue,
38
- startTransport,
39
- uploadAttachment,
40
- } from './transport';
41
- import type { AttachmentKind, AttachmentMeta, AttachmentSource, Event } from './types';
42
-
43
- declare const __DEV__: boolean | undefined;
44
-
45
- export type InitOptions = {
46
- /** Project token starting with `st_pk_`. Required. */
47
- token: string;
48
- /** Release identifier, e.g. `myapp@1.2.3+456`. Required. */
49
- release: string;
50
- /** Environment label. Defaults to `dev` if `__DEV__`, else `prod`. */
51
- environment?: string;
52
- /** Override ingestion URL (self-hosted). Default: https://ingest.sentori.golia.jp */
53
- ingestUrl?: string;
54
- /** Toggle individual capture sources. All enabled by default. */
55
- capture?: {
56
- globalErrors?: boolean;
57
- promiseRejections?: boolean;
58
- network?:
59
- | boolean
60
- | {
61
- /** v0.9.0 #11 — auto-extract GraphQL `operationName` from
62
- * POST request bodies and use it as the breadcrumb / span
63
- * name (instead of `POST /graphql`). Default `true`. */
64
- graphql?: boolean;
65
- };
66
- /** Session tracking: opens a session on init and on each
67
- * foreground (`AppState` → `active`), ends it on background.
68
- * Drives crash-free rate. Set `false` to opt out. */
69
- sessions?: boolean;
70
- /** Capture a screenshot of the current screen on
71
- * `captureException`. Opt-in. The capture runs through the
72
- * bundled native module — no extra peer dep required since
73
- * v0.7.3. To redact PII regions, register a mask query via
74
- * `sentori.registerMaskQuery(() => string[])` and put
75
- * `nativeID="..."` on the `<View>`s the SDK should black out.
76
- * The image is webp q=70 / jpeg q=70 at 480 px max, < 100 KB
77
- * typical. */
78
- screenshot?: boolean;
79
- /** Phase 46: record the last N steps (route changes, custom
80
- * breadcrumbs) leading up to a crash. On `captureException`
81
- * the buffer is sealed and uploaded as a `sessionTrail`
82
- * attachment. Defaults to false. */
83
- sessionTrail?: boolean;
84
- /** v2.0 W3 — when `true`, every `sentori.track(name, props)`
85
- * also pushes a `{ type: 'track', data: { name, props } }`
86
- * breadcrumb so a subsequent `captureException` /
87
- * `captureMessage` carries the customer journey leading up to
88
- * the failure. Defaults to `false` to preserve v1 customer
89
- * breadcrumb shape on upgrade; recommended `true` for new
90
- * integrations. See `docs/recipes/track-and-metrics.md`. */
91
- trackAutoBreadcrumb?: boolean;
92
- /** v2.1 W2 — auto-instrument runtime metrics (FPS, JS heap,
93
- * cold-start, route nav timing, network bytes). Drains the
94
- * shared `@goliapkg/sentori-core` ring to
95
- * `/v1/runtime-metrics:batch` every 30 s. Defaults to `true`
96
- * in W2 part 2 — cold-start only, ~6 emits per session
97
- * per device, ~zero main-thread cost. Higher-cost instruments
98
- * (FPS / route-nav) land in W2 part 3 with per-tick perf
99
- * budget tests as stop-ship gates per
100
- * `.claude/CLAUDE.md` performance bedrock. */
101
- runtimeMetrics?: boolean;
102
- /** v0.9.1 +S4 — pre-crash sentinel. Subscribes to JS-thread
103
- * frame timing; when ≥ 50% of a 60-frame window misses the
104
- * budget (default 32 ms / < 30 fps), emits a `kind: nearCrash`
105
- * event proactively so dashboards see the "about-to-die"
106
- * signal before an actual crash. */
107
- preCrashSentinel?: boolean;
108
- sentinelChannels?: PreCrashChannel[];
109
- /** v0.9.6 #4 — JS-thread long-task monitor. setInterval(50ms)
110
- * tick detects JS thread stalls ≥ 200ms (configurable) and
111
- * emits a `sentori.longtask` span. Pairs with
112
- * `preCrashSentinel` (slow frames < 32ms) to cover the
113
- * "JS thread is stuck" spectrum. */
114
- longTaskMonitor?: boolean | { thresholdMs?: number };
115
- /** v0.9.6 #2 — wireframe session replay. Native walks the iOS
116
- * UIView / Android View hierarchy at 1 Hz and serializes
117
- * visible nodes; captureException flushes the last 60 s as a
118
- * `replay` attachment. Set to `'wireframe'` to enable. */
119
- replay?: 'off' | 'wireframe' | { hz?: number; mode: 'off' | 'wireframe' };
120
- /** v1.1 #4 升级 — JS sample profiler. setInterval(50ms) idle-tick
121
- * sampler aggregates frame counts → emits sentori.profile span
122
- * every 60s with `flameData` (frame → tick count). Pairs with
123
- * longTaskMonitor (≥200ms outliers) — sample profiler 看 idle
124
- * 分布、long-task 看 outliers。 */
125
- sampleProfiler?: boolean | { flushMs?: number; sampleMs?: number };
126
- /** Analytics v1 — live-presence heartbeat. Foreground 1/min
127
- * default; opt out with `false`, or pass an options object
128
- * to tune. Powers the Audience > Live dashboard.
129
- * Iron-rule budget: < 1 KB / min, < 1 ms / call. */
130
- heartbeat?: boolean | HeartbeatOptions;
131
- /** v0.9.0 #3 — launch-crash loop guard. When two consecutive
132
- * launches don't reach `markLaunchCompleted()` (typical of an
133
- * OTA update with a fatal bug), invoke the host callback with
134
- * a 200 ms timeout to decide rollback / reset / continue. */
135
- launchCrashGuard?: {
136
- enabled: boolean;
137
- onLaunchCrashDetected?: (
138
- info: import('./launch-crash-guard').LaunchCrashInfo,
139
- ) =>
140
- | import('./launch-crash-guard').LaunchCrashAction
141
- | Promise<import('./launch-crash-guard').LaunchCrashAction>;
142
- threshold?: number;
143
- timeoutMs?: number;
144
- };
145
- };
146
- /** Phase 44 sub-B: client-side sampling. Each rate is `[0, 1]`;
147
- * absent / null keeps everything. Defaults to 1.0 for both
148
- * (no drop). Set traces to e.g. 0.1 once the app's at user
149
- * volume to keep ingest budget under control without changing
150
- * the server-side quota. Decisions are made per-event for
151
- * errors and per-trace (all spans together) for traces. */
152
- sampling?: {
153
- errors?: null | number;
154
- traces?: null | number;
155
- /** v2.0 — sampling rate for `kind: 'message'` events emitted
156
- * via `sentori.captureMessage()`. `null` / absent → keep all. */
157
- messages?: null | number;
158
- };
159
- /** v2.3 — canonical sampling field (renamed from `sampling`).
160
- * Same shape; if both are passed, `sample` wins. The older
161
- * `sampling` stays accepted indefinitely as a back-compat alias —
162
- * this is the kind of rename that's only worth doing if it's
163
- * also zero-cost for existing callers. */
164
- sample?: {
165
- errors?: null | number;
166
- traces?: null | number;
167
- messages?: null | number;
168
- };
169
- /** v2.3 — Sentori SDK's own console output gate. Default `'warn'`:
170
- * SDK is silent unless something is genuinely broken (transport
171
- * sustained failure, native module not found, internal SDK
172
- * exception). Set to `'silent'` for absolute silence; bump to
173
- * `'info'` / `'debug'` when debugging Sentori itself. */
174
- logLevel?: import('@goliapkg/sentori-core').LogLevel;
175
- /** v2.3 — fires once after init completes. Use this to know the
176
- * SDK is live instead of scanning the console. The `ReadyInfo`
177
- * carries native-module bind status + cold-start timing. */
178
- onReady?: (info: ReadyInfo) => void;
179
- /** v2.3 — mutate-or-drop hook on each outbound event (sync).
180
- * Return the event to ship it (possibly mutated), or `null` to
181
- * drop. See `BeforeSendHook` for the throwing / non-event
182
- * fallback policy. Used for host-side PII scrubbing the SDK
183
- * can't do automatically; server-side privacy_lab still runs
184
- * regardless. */
185
- beforeSend?: import('@goliapkg/sentori-core').BeforeSendHook;
186
- };
187
-
188
- const DEFAULT_INGEST_URL = 'https://ingest.sentori.golia.jp';
189
-
190
- export const init = (options: InitOptions): void => {
191
- if (!options.token || !options.token.startsWith('st_pk_')) {
192
- throw new Error("Sentori: token is required and must start with 'st_pk_'");
16
+ import { checkColdStart } from './mobile-vitals';
17
+ import { markNativeJsBridgeReady, setNativeConfig } from './native';
18
+ import { shipNativePending } from './native-pending';
19
+ import { drainReplay, startReplay } from './replay';
20
+ import { drainScreenReplay, startScreenReplay } from './replay-screens';
21
+ import { drainOfflineQueue, startTransport, uploadAttachment } from './transport';
22
+
23
+ let _initialized = false;
24
+
25
+ export const init = safeFn('init', (config: InitConfig): void => {
26
+ if (_initialized) return;
27
+
28
+ if (!config || typeof config.token !== 'string' || config.token.length === 0) {
29
+ // eslint-disable-next-line no-console
30
+ console.warn('[sentori] init skipped: token missing — SDK is a no-op');
31
+ return;
193
32
  }
194
- if (!options.release) {
195
- throw new Error('Sentori: release is required');
33
+ if (typeof config.ingestUrl !== 'string' || config.ingestUrl.length === 0) {
34
+ // eslint-disable-next-line no-console
35
+ console.warn('[sentori] init skipped: ingestUrl missing — SDK is a no-op');
36
+ return;
196
37
  }
197
38
 
198
- // v2.3 — set log level FIRST so any startup-time logger calls
199
- // are gated correctly. Default 'warn' from logger.ts; an explicit
200
- // host setting overrides.
201
- setLogLevel(options.logLevel);
202
-
203
- const env =
204
- options.environment ??
205
- (typeof __DEV__ !== 'undefined' && __DEV__ ? 'dev' : 'prod');
206
-
207
- // v0.9.0 #3 — launch-crash guard. Fires *before* any other setup so
208
- // a known-bad bundle can roll back instead of running JS that's
209
- // about to die again. AsyncStorage-backed; if the host doesn't have
210
- // it the guard is a no-op.
211
- const lcg = options.capture?.launchCrashGuard;
212
- if (lcg?.enabled) {
213
- void runLaunchCrashGuard(
214
- lcg,
215
- options.release,
216
- getBundleInfo()?.id ?? null,
217
- );
218
- }
39
+ _initialized = true;
219
40
 
220
41
  setConfig({
221
- token: options.token,
222
- release: options.release,
223
- environment: env,
224
- ingestUrl: options.ingestUrl ?? DEFAULT_INGEST_URL,
42
+ token: config.token,
43
+ ingestUrl: config.ingestUrl.replace(/\/+$/, ''),
44
+ release: config.release ?? '',
45
+ environment: config.environment ?? 'production',
225
46
  enabled: true,
226
- screenshotsEnabled: options.capture?.screenshot === true,
227
- // v2.3 — `sample` is the canonical name; `sampling` is the
228
- // back-compat alias. If both are supplied, `sample` wins.
229
- errorSampleRate: options.sample?.errors ?? options.sampling?.errors ?? null,
230
- traceSampleRate: options.sample?.traces ?? options.sampling?.traces ?? null,
231
- messageSampleRate: options.sample?.messages ?? options.sampling?.messages ?? null,
232
- sessionTrailEnabled: options.capture?.sessionTrail === true,
233
- // v2.0 W3 — when true, every `track()` also pushes a
234
- // `type: 'track'` breadcrumb so a subsequent captureException
235
- // carries the customer journey. Defaults false to preserve v1
236
- // breadcrumb shape on upgrade.
237
- trackAutoBreadcrumb: options.capture?.trackAutoBreadcrumb === true,
238
- // v2.3 — host-side beforeSend hook (sync). Stored on Config so
239
- // capture.ts can pull it without re-resolving the InitOptions.
240
- beforeSend: options.beforeSend,
47
+ detect: {
48
+ rageTap: config.detect?.rageTap ?? true,
49
+ longFreeze: config.detect?.longFreeze ?? true,
50
+ slowColdStart: config.detect?.slowColdStart ?? true,
51
+ slowApi: config.detect?.slowApi ?? false,
52
+ },
53
+ replaySeconds: config.replaySeconds ?? 30,
54
+ replayScreens: config.replayScreens ?? false,
55
+ beforeSend: config.beforeSend,
241
56
  });
57
+ setLogLevel(config.logLevel ?? 'warn');
58
+
59
+ // JS-side error capture + the signal-ring feeders.
60
+ installGlobalHandler();
61
+ installPromiseHandler();
62
+ installNetworkHandler();
63
+ installLifecycleHandler();
64
+
65
+ // Warn-scenario detectors (design.md §3 minimum set). rage_tap
66
+ // rides the RageTapCapture component; the rest start here.
67
+ if (config.detect?.longFreeze !== false) startLongTaskMonitor();
68
+
69
+ // B-type replay: a rolling in-memory wireframe ring; an error/warn
70
+ // going out drains it into a replay attachment on that event.
71
+ const replaySeconds = config.replaySeconds ?? 30;
72
+ if (replaySeconds > 0) {
73
+ startReplay({ mode: 'wireframe' });
74
+ // Visual ring is opt-in: screenshots can carry user content.
75
+ if (config.replayScreens === true) startScreenReplay(replaySeconds);
76
+ registerEmitHook((event) => {
77
+ if (event.kind !== 'error' && event.kind !== 'warn') return;
78
+ if (!event.id) return;
79
+ const lines = drainReplay();
80
+ if (lines) {
81
+ const base64 = base64Encode(lines);
82
+ if (base64) {
83
+ void uploadAttachment(
84
+ event.id,
85
+ 'replay',
86
+ { base64, mediaType: 'application/x-sentori-replay' },
87
+ { source: 'js' },
88
+ );
89
+ }
90
+ }
91
+ const frames = drainScreenReplay();
92
+ if (frames) {
93
+ const base64 = base64Encode(frames);
94
+ if (base64) {
95
+ void uploadAttachment(
96
+ event.id,
97
+ 'screens',
98
+ { base64, mediaType: 'application/x-sentori-screens' },
99
+ { source: 'js' },
100
+ );
101
+ }
102
+ }
103
+ });
104
+ }
242
105
 
243
- // Tell the native crash handler about the config so the JSON it writes
244
- // on the next NSException / Java uncaught carries release + env.
106
+ // Native side: hand over release/environment for the crash-file
107
+ // writer, mark the bridge live, then drain crashes from previous
108
+ // launches. All fire-and-forget — init stays synchronous and fast
109
+ // (< 50 ms budget; the work below happens off the critical path).
245
110
  setNativeConfig({
246
- token: options.token,
247
- release: options.release,
248
- environment: env,
111
+ environment: config.environment ?? 'production',
112
+ release: config.release ?? '',
113
+ token: config.token,
249
114
  });
250
- // v0.9.4 #1 — finalize cold-start measurement. iOS uses the
251
- // delta from `applicationDidFinishLaunching` to this call;
252
- // Android uses Process.getStartElapsedRealtime() so the value is
253
- // computed at this point and cached.
254
115
  markNativeJsBridgeReady();
255
- // Emit a one-off cold-start span. Server aggregates these per
256
- // release for the Mobile Vitals dashboard. No-op when native
257
- // module isn't linked.
258
- const coldMs = getColdStartMs();
259
- if (coldMs !== null && coldMs > 0 && coldMs < 60_000) {
260
- const span = startSpan('sentori.cold_start', {
261
- name: 'cold-start',
262
- parent: null,
263
- startNowMs: Date.now() - coldMs,
264
- tags: { 'vital.kind': 'cold_start' },
265
- });
266
- span.finish({ status: 'ok' });
267
- }
268
116
 
269
117
  startTransport();
270
- // v1.1 +S7 升级 — control channel poll for live-debug flag.
271
- startControlChannel();
272
- // v0.8.0-c — start watching network class. No-op if NetInfo isn't
273
- // installed; events just won't carry `device.networkType` in that
274
- // case.
275
- startNetworkTypeWatch();
276
- // v0.8.3 — drain custom-metric ring every 30 s.
277
- startMetricsTimer();
278
- // v1.1 chunk B — drain `sentori.track()` ring every 30 s.
279
- startTrackTimer();
280
- // v2.1 W2 — runtime metrics auto-instrument. Defaults on; host
281
- // can opt out with `capture: { runtimeMetrics: false }`. The
282
- // ring + emit live in `@goliapkg/sentori-core`; we own the
283
- // 30 s flusher + the cold-start one-shot here. FPS / heap /
284
- // route-nav / network instruments ship in W2 part 3 (each
285
- // gated on its own per-tick perf budget CI test).
286
- if (options.capture?.runtimeMetrics !== false) {
287
- markColdStartT0();
288
- startRuntimeMetricsTimer();
289
- // Defer the emit one tick so React's first paint settles
290
- // before we stamp "cold-start ended". 0-delay setTimeout puts
291
- // the call after the current microtask queue drains.
292
- setTimeout(emitColdStart, 0);
293
- // FPS via rAF (per-tick budget < 0.5 ms — see
294
- // runtime-metrics-fps.ts header). Heap is a 30 s polling
295
- // tick; no-op when performance.memory isn't exposed (most
296
- // RN engines today; we ship the wiring anyway so the same
297
- // SDK works on web targets via @goliapkg/sentori-javascript).
298
- startFpsInstrument();
299
- startHeapInstrument();
300
- // Network bytes counters drain every 30 s. The counters
301
- // themselves are incremented inline by handlers/network.ts
302
- // on every fetch round-trip — see the recordNetworkBytes
303
- // call site there. No-op when `capture.network: false` is
304
- // set (no fetch patch → no counter increments).
305
- startNetworkBytesInstrument();
306
- // Route-nav dwell timing emits inline from `navigation.ts`'s
307
- // useTraceNavigation state listener — no extra start call
308
- // needed; the host already mounts that hook for tracing.
309
- }
310
- // v1.1 chunk S1 — warm the install-id cache. Fire-and-forget;
311
- // any event captured before the first resolve simply omits
312
- // `device.installId`. Subsequent captures pick it up via the
313
- // sync `peekInstallId()` read in collectDevice().
314
- void getInstallId();
315
- // v0.9.1 +S4 — pre-crash sentinel. Off by default; opt-in via
316
- // `capture.preCrashSentinel: true`.
317
- if (options.capture?.preCrashSentinel === true) {
318
- startPreCrashSentinel({
319
- enabled: true,
320
- channels: options.capture.sentinelChannels,
321
- });
322
- }
323
- // v0.9.6 #4 — long-task monitor. Off by default.
324
- const lt = options.capture?.longTaskMonitor;
325
- if (lt) {
326
- startLongTaskMonitor({
327
- enabled: true,
328
- thresholdMs: typeof lt === 'object' ? lt.thresholdMs : undefined,
329
- });
330
- }
331
- // v0.9.6 #2 — wireframe replay. Off by default.
332
- const rp = options.capture?.replay;
333
- if (rp === 'wireframe') {
334
- startReplay({ mode: 'wireframe' });
335
- } else if (rp && typeof rp === 'object' && rp.mode === 'wireframe') {
336
- startReplay({ hz: rp.hz, mode: 'wireframe' });
337
- }
338
- // v1.1 #4 升级 — JS sample profiler. Off by default.
339
- const sp = options.capture?.sampleProfiler;
340
- if (sp) {
341
- startSampleProfiler({
342
- enabled: true,
343
- flushMs: typeof sp === 'object' ? sp.flushMs : undefined,
344
- sampleMs: typeof sp === 'object' ? sp.sampleMs : undefined,
345
- });
346
- }
347
-
348
- const capture = options.capture ?? {};
349
- if (capture.globalErrors !== false) installGlobalHandler();
350
- if (capture.promiseRejections !== false) installPromiseHandler();
351
- if (capture.network !== false) {
352
- const netOpts = typeof capture.network === 'object' ? capture.network : undefined;
353
- installNetworkHandler({ graphql: netOpts?.graphql });
354
- }
355
- if (capture.sessions !== false) {
356
- // Open the cold-start session now (RN doesn't fire an AppState
357
- // `change` for the initial `active` state), then bind AppState so
358
- // background ends it and the next foreground opens a fresh one.
359
- startSession();
360
- installLifecycleHandler();
361
- }
362
- // Analytics v1 — live-presence heartbeat. Foreground 1/min by
363
- // default; pass `capture.heartbeat = false` to opt out, or
364
- // `{ intervalMs: 30_000 }` to override. The default budget is well
365
- // under the "几乎不能造成性能抖动" rule (CLAUDE.md).
366
- const heartbeatOpt = capture.heartbeat;
367
- if (heartbeatOpt !== false) {
368
- startHeartbeat(typeof heartbeatOpt === 'object' ? heartbeatOpt : {});
369
- }
370
-
371
- // Drain events persisted from previous session (best-effort):
372
- // - native crashes from <Documents>/sentori/pending/*.json
373
- // - JS transport offline queue from AsyncStorage
374
- drainNativePending()
375
- .then(async (items) => {
376
- for (const json of items) {
377
- try {
378
- const event = JSON.parse(json) as Event & {
379
- _pendingAttachments?: PendingAttachment[];
380
- };
381
- // Phase 42 sub-E.05 / F.09: the native crash handler couldn't
382
- // upload attachments at crash time (the app was dying); it
383
- // base64-encoded them into `_pendingAttachments` instead.
384
- // On next launch we upload each before enqueueing the event,
385
- // so the dashboard sees the refs in `event.attachments[]`.
386
- if (event._pendingAttachments && event._pendingAttachments.length > 0) {
387
- for (const p of event._pendingAttachments) {
388
- const meta = await uploadAttachment(
389
- event.id,
390
- p.kind,
391
- { base64: p.base64, mediaType: p.mediaType },
392
- { source: p.source },
393
- );
394
- if (meta) {
395
- if (!event.attachments) event.attachments = [];
396
- event.attachments.push(meta);
397
- }
398
- }
399
- delete event._pendingAttachments;
400
- }
401
- enqueue(event);
402
- } catch {
403
- // skip malformed
404
- }
405
- }
406
- })
407
- .catch(() => {});
408
- drainOfflineQueue().catch(() => {});
409
-
410
- // v0.9.0 #3 — init reached the end without throwing. Schedule the
411
- // "launch completed" marker after one tick so any synchronous user
412
- // code right after `init()` gets to run first; we want the marker to
413
- // confirm the JS bridge stayed alive, not just that `init()` returned.
414
- if (lcg?.enabled) {
415
- setTimeout(() => {
416
- void markLaunchCompleted(getBundleInfo()?.id ?? null);
417
- }, 2_000);
418
- }
419
-
420
- // v2.3 — onReady callback. Fires after setConfig + native bind +
421
- // transport start are all settled. The drain-pending work is
422
- // still in flight (it's async) but the SDK is ready to accept
423
- // new captures. Host wires this to know the SDK is live without
424
- // scanning the console. Wrapped in try/catch — host callback
425
- // throwing must not propagate (NEVER rule).
426
- if (options.onReady) {
427
- const nativeMod = (() => {
428
- try {
429
- // eslint-disable-next-line @typescript-eslint/no-require-imports
430
- const { native } = require('./native');
431
- const n = native?.();
432
- return n ?? null;
433
- } catch {
434
- return null;
435
- }
436
- })();
437
- const info: ReadyInfo = {
438
- sdkVersion: SDK_VERSION,
439
- coldStartMs: coldMs ?? undefined,
440
- native: {
441
- bound: !!nativeMod,
442
- methods: nativeMod ? Object.keys(nativeMod).sort() : [],
443
- },
118
+ checkColdStart(config.detect?.slowColdStart !== false);
119
+ void shipNativePending().catch(() => undefined);
120
+ void drainOfflineQueue();
121
+ });
122
+
123
+ /** RN's Hermes has no btoa in older releases; go through base64.ts. */
124
+ const base64Encode = (text: string): string | null => {
125
+ try {
126
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
127
+ const { encodeBase64 } = require('./base64') as {
128
+ encodeBase64: (s: string) => string;
444
129
  };
445
- try {
446
- options.onReady(info);
447
- } catch {
448
- // Host's onReady threw. NEVER rule — swallow.
449
- }
130
+ return encodeBase64(text);
131
+ } catch {
132
+ return null;
450
133
  }
451
134
  };
452
135
 
453
- // Bumped on each SDK release; surfaced in onReady payload + the
454
- // future Sentry-compat layer's identification string.
455
- const SDK_VERSION = '2.3.0';
456
-
457
- /**
458
- * Phase 42 sub-E.05: shape of each entry in the native crash JSON's
459
- * `_pendingAttachments` array. Mirrors what
460
- * `SentoriCrashHandler.write` writes on iOS and (sub-F) what
461
- * `SentoriCrashWriter` writes on Android.
462
- */
463
- type PendingAttachment = {
464
- base64: string;
465
- kind: AttachmentKind;
466
- mediaType: string;
467
- source: AttachmentSource;
136
+ export const __resetForTests = (): void => {
137
+ _initialized = false;
468
138
  };
469
-
470
- // Keep AttachmentMeta in the imports — it's part of the public type
471
- // surface re-exported from this module's bundle.
472
- export type { AttachmentMeta };