@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
package/src/capture.ts DELETED
@@ -1,660 +0,0 @@
1
- import {
2
- type BeforeSendHook,
3
- type CaptureMessageOptions,
4
- hashIdentities,
5
- type LinkBy,
6
- logger,
7
- type MessageLevel,
8
- safeFn,
9
- sealTrail,
10
- shouldSample,
11
- } from '@goliapkg/sentori-core';
12
-
13
- import { base64Utf8 } from './base64';
14
- import {
15
- __peekBreadcrumbCount,
16
- addBreadcrumb,
17
- getBreadcrumbs,
18
- } from './breadcrumbs';
19
- import { getBundleInfo } from './bundle-info';
20
- import { getConfig, isInitialized } from './config';
21
- import { getFeatureFlagSnapshot } from './feature-flags';
22
- import { drainReplay, isReplayRunning } from './replay';
23
- import { clearStateSnapshots, getStateSnapshots } from './state-snapshots';
24
- import { symbolicateErrorViaMetro } from './handlers/dev-symbolicate';
25
- import { captureScreenshot } from './handlers/screenshot';
26
- import { markSessionErrored } from './session-tracker';
27
- import { parseStack } from './stack';
28
- import { getTrailBuffer } from './trail';
29
- import { enqueue, sendUserReport, uploadAttachment } from './transport';
30
- import { uuidV7 } from './uuid';
31
- import { peekInstallId } from './install-id';
32
- import { getCachedNetworkType } from './netinfo';
33
- import { getRecentNativeException } from './native';
34
- import type { App, AttachmentMeta, Device, Event, SentoriError, Tags, User } from './types';
35
-
36
- export { captureStep, __resetTrailForTests } from './trail';
37
-
38
- declare const __DEV__: boolean | undefined;
39
-
40
- let _user: User | null = null;
41
-
42
- /** v2.0 — global scope tags merged onto every captured event
43
- * (captureException + captureMessage). Set via `setTag` /
44
- * `setTags`; reset by passing `{}`. Per-call tags win on conflict. */
45
- let _scopeTags: Record<string, string> = {};
46
-
47
- /**
48
- * v2.0 — set a single scope tag merged onto every subsequent
49
- * capture. Per-call `extras.tags` / `opts.tags` win over scope tags.
50
- *
51
- * sentori.setTag('rollout', 'dark-mode-v2');
52
- * sentori.captureException(err); // event.tags carries rollout
53
- */
54
- export const setTag = (key: string, value: string): void => {
55
- _scopeTags[key] = String(value);
56
- };
57
-
58
- /**
59
- * v2.0 — bulk variant of setTag. Existing tags are merged with
60
- * the input record (`Object.assign` style).
61
- */
62
- export const setTags = (record: Record<string, string>): void => {
63
- for (const [k, v] of Object.entries(record)) _scopeTags[k] = String(v);
64
- };
65
-
66
- /** Internal — merge global scope tags into a per-call tag record. */
67
- function mergeScopeTags(perCall: Tags | undefined): Tags | undefined {
68
- if (Object.keys(_scopeTags).length === 0) return perCall;
69
- return { ..._scopeTags, ...(perCall ?? {}) };
70
- }
71
-
72
- export const __resetScopeForTests = (): void => {
73
- _user = null;
74
- _scopeTags = {};
75
- };
76
-
77
- // Phase 42 sub-D.08 — per-session screenshot quota. Defaults: 10 in
78
- // prod, unlimited (-1 sentinel) in dev so test loops + react-error-
79
- // overlay reruns don't run out partway through the session.
80
- const SCREENSHOT_PROD_LIMIT = 10;
81
- let _screenshotsTaken = 0;
82
-
83
- function screenshotBudget(): number {
84
- return typeof __DEV__ !== 'undefined' && __DEV__ ? -1 : SCREENSHOT_PROD_LIMIT;
85
- }
86
-
87
- export const __resetScreenshotBudgetForTests = (): void => {
88
- _screenshotsTaken = 0;
89
- };
90
-
91
- /**
92
- * Attach a stable user identifier to events captured after this call.
93
- *
94
- * PII policy (Phase 16 sub-D): the User shape is intentionally limited
95
- * to `{ id?, anonymous? }` — no email, name, IP, or other identifying
96
- * fields. Use a hashed / pseudonymous id (e.g. uuid v4 stored in
97
- * AsyncStorage on first launch). The server schema enforces the same
98
- * shape, so any extra fields you tack on at the JS layer would be
99
- * rejected with `validationFailed` and never persisted.
100
- *
101
- * Pass `null` to clear (e.g. on sign-out).
102
- */
103
- /**
104
- * Public user identity API.
105
- *
106
- * sentori.setUser({ id: 'usr_123', name: 'Lihao', linkBy: {
107
- * email: 'lihao@example.com',
108
- * googleSub: '108293…',
109
- * } })
110
- *
111
- * The `linkBy` map is for **cross-project lookup** (see v2.3 design,
112
- * §5 in docs/design/sdk-v2.3-redesign.md). Each value gets hashed
113
- * client-side via `crypto.subtle.digest('SHA-256', …)` BEFORE
114
- * leaving the device. Raw email / phone / sub **never** reach
115
- * Sentori; the server can't recover them.
116
- *
117
- * Hashing is async (WebCrypto is async). `setUser` itself returns
118
- * void synchronously so host code stays one-line; the hash work
119
- * runs in the background and is committed to scope when ready. If
120
- * a `captureException` fires BEFORE the hash settles, the event
121
- * carries `id` + `name` only (no linkHashes that cycle) — the next
122
- * event picks them up. This is fine in practice because the host
123
- * sets the user once near startup, well before any captures.
124
- *
125
- * If `crypto.subtle` is unavailable in the runtime, the hash
126
- * promise rejects and `linkBy` is silently dropped (NEVER rule —
127
- * SDK failure must not propagate to host code).
128
- */
129
- type SetUserInput = (User & { linkBy?: LinkBy }) | null;
130
-
131
- export const setUser = (input: SetUserInput): void => {
132
- if (input == null) {
133
- // `null` (explicit clear) or `undefined` (callers occasionally
134
- // pass through optional state). Both clear the scope user.
135
- _user = null;
136
- return;
137
- }
138
- // Stage 1: commit id + name + anonymous immediately so the next
139
- // captured event picks them up even if hashing is still in flight.
140
- const { linkBy: rawLinkBy, ...stable } = input;
141
- _user = { ...stable };
142
-
143
- // Stage 2: hash any linkBy values async; commit to scope when done.
144
- if (rawLinkBy && Object.keys(rawLinkBy).length > 0) {
145
- void hashIdentities(rawLinkBy)
146
- .then((linkHashes) => {
147
- // Re-merge to guard against setUser being called again
148
- // between stage 1 and stage 2 — if the user changed, drop
149
- // the now-stale hash.
150
- if (_user && _user.id === stable.id) {
151
- _user = { ..._user, linkHashes };
152
- }
153
- })
154
- .catch((e) => {
155
- logger.warn('identity', 'linkBy hash failed; identities dropped:', e);
156
- });
157
- }
158
- };
159
-
160
- export const getUser = (): User | null => _user;
161
-
162
- /** v1.1 +S7 升级 — read just the current user id for the control
163
- * channel poll. Returns `undefined` until `setUser({ id })` runs. */
164
- export const getCurrentUserId = (): string | undefined => _user?.id ?? undefined;
165
-
166
- export type CaptureExtras = {
167
- tags?: Tags;
168
- user?: User;
169
- fingerprint?: string[];
170
- /** Phase 42 sub-D.07: per-call screenshot override. `false` skips
171
- * screenshot capture even when `init({ capture: { screenshot:
172
- * true } })` is on — handy for sensitive screens. Defaults to
173
- * whatever `config.screenshotsEnabled` says. */
174
- screenshot?: boolean;
175
- };
176
-
177
- /**
178
- * v0.8.2 — submit an end-user-supplied bug report. Use this when the
179
- * host app surfaces a "Report a problem" form. Pass `eventId` if the
180
- * user is reporting a specific crash they just saw — the server links
181
- * the report to that event's issue automatically.
182
- *
183
- * Returns `{ id, issueId }` on success or `null` on any failure
184
- * (network down, ingest token revoked, validation rejected). Doesn't
185
- * throw.
186
- */
187
- export const sendUserFeedback = async (input: {
188
- body: string;
189
- email?: string;
190
- eventId?: string;
191
- name?: string;
192
- title: string;
193
- }): Promise<null | { id: string; issueId: null | string }> => {
194
- if (!isInitialized()) return null;
195
- const config = getConfig();
196
- if (!config) return null;
197
- return sendUserReport(config.ingestUrl, config.token, input);
198
- };
199
-
200
- export const captureError = (error: Error, extras?: CaptureExtras): void => {
201
- if (!isInitialized()) return;
202
- const config = getConfig();
203
- if (!config) return;
204
-
205
- // Phase 44 sub-B: client-side sampling. Skip the whole pipeline
206
- // (no screenshot capture either) when the sample dice come up
207
- // wrong. Default rate = null = keep, so existing callers unaffected.
208
- if (!shouldSample(config.errorSampleRate)) {
209
- addBreadcrumb({ type: 'custom', data: { reason: 'sampled-out', kind: 'error' } });
210
- return;
211
- }
212
-
213
- const flags = getFeatureFlagSnapshot();
214
- const bundle = getBundleInfo();
215
- const crumbs = getBreadcrumbs();
216
- const event: Event = {
217
- id: uuidV7(),
218
- timestamp: new Date().toISOString(),
219
- kind: 'error',
220
- platform: 'javascript',
221
- release: config.release,
222
- environment: config.environment,
223
- device: collectDevice(),
224
- app: collectApp(config.release),
225
- user: extras?.user ?? _user,
226
- tags: mergeScopeTags(extras?.tags),
227
- ...(flags ? { flags } : {}),
228
- ...(bundle ? { bundle } : {}),
229
- breadcrumbs: crumbs,
230
- error: errorToObject(error),
231
- fingerprint: extras?.fingerprint,
232
- };
233
- // v0.9.8 — dev-only diagnostic. Insight saw `breadcrumbs: []` on
234
- // every event in 0.9.7 despite handlers being installed; this line
235
- // makes it visible in Metro that the snapshot at captureException
236
- // time really is empty (no breadcrumb events fired yet) vs. having
237
- // been silently dropped on the wire. Production builds gate out.
238
- logger.debug('capture', 'captureException',
239
- 'eventId=', event.id,
240
- 'breadcrumbs=', crumbs.length,
241
- 'wantScreenshot=', config.screenshotsEnabled && extras?.screenshot !== false,
242
- 'wantSessionTrail=', config.sessionTrailEnabled,
243
- 'wantReplay=', isReplayRunning(),
244
- );
245
-
246
- // Phase 26 sub-B: a captured error promotes the current session to
247
- // `errored` so the next AppState=background ping reports unhealthy.
248
- markSessionErrored();
249
-
250
- // Phase 42 sub-D.07: opt-in screenshot. Default off; per-call
251
- // `extras.screenshot: false` always wins so callers can mute it
252
- // on a sensitive flow even when init has it on globally.
253
- const wantScreenshot =
254
- config.screenshotsEnabled && extras?.screenshot !== false && allowScreenshot();
255
-
256
- // Phase 40 sub-E: in dev there's no uploaded source map, so ask
257
- // Metro to symbolicate the stack before we send it (best-effort,
258
- // short timeout). Release builds skip straight to enqueue and let
259
- // the server symbolicate at ingest against the uploaded map.
260
- const pipeline = async (): Promise<void> => {
261
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
262
- await symbolicateErrorViaMetro(event.error!).catch(() => {});
263
- }
264
- if (wantScreenshot) {
265
- await captureAndAttachScreenshot(event);
266
- }
267
- const trail = getTrailBuffer();
268
- if (config.sessionTrailEnabled && trail.size() > 0) {
269
- await captureAndAttachSessionTrail(event);
270
- }
271
- // v0.9.2 +S2 — state time-travel attachment. Only if anything has
272
- // been bound or recorded; cleared on success so the next crash's
273
- // ring doesn't carry stale entries.
274
- const stateSnapshots = getStateSnapshots();
275
- if (stateSnapshots.length > 0) {
276
- await captureAndAttachStateSnapshots(event, stateSnapshots);
277
- clearStateSnapshots();
278
- }
279
- // v0.9.6 #2 — wireframe replay attachment. drainReplay clears the
280
- // ring as a side effect so next session's replay starts fresh.
281
- const replayNdjson = drainReplay();
282
- if (replayNdjson.length > 0) {
283
- await captureAndAttachReplay(event, replayNdjson);
284
- } else if (typeof __DEV__ !== 'undefined' && __DEV__ && isReplayRunning()) {
285
- // rc.4 — explicit "replay was on but ring drained empty" signal.
286
- // Without this, "kinds=screenshot,sessionTrail" looks
287
- // indistinguishable from `replay: off` even though the ticks
288
- // were healthy upstream. Insight 2026-05-18 verify shape made
289
- // this gap painful to triage.
290
- logger.debug('capture', 'replay drain empty (no frames buffered at captureException)',
291
- 'eventId=', event.id,
292
- );
293
- }
294
- logger.debug('capture', 'enqueue',
295
- 'eventId=', event.id,
296
- 'attachments=', event.attachments?.length ?? 0,
297
- 'kinds=', (event.attachments ?? []).map((a) => a.kind).join(',') || '(none)',
298
- 'breadcrumbsAtEnqueue=', __peekBreadcrumbCount(),
299
- );
300
- // v2.3 — host beforeSend hook. Sync; drops on null, falls back
301
- // to unmutated event on throw or non-event return.
302
- const finalEvent = applyBeforeSend(event, config.beforeSend);
303
- if (finalEvent === null) return;
304
- enqueue(finalEvent);
305
- };
306
- void pipeline();
307
- };
308
-
309
- /**
310
- * v2.3 — invoke the host's `beforeSend` hook (if any) under the
311
- * NEVER rule. Returns the (possibly mutated) event, or null to
312
- * drop. A throw / non-event return is treated as a no-op
313
- * (one-shot warn + send unmodified) so a buggy host hook can't
314
- * stall captures.
315
- */
316
- let _beforeSendThrewWarned = false;
317
- function applyBeforeSend(event: Event, hook: BeforeSendHook | undefined): Event | null {
318
- if (!hook) return event;
319
- try {
320
- const result = hook(event);
321
- if (result === null) {
322
- logger.debug('capture', 'beforeSend dropped event', 'eventId=', event.id);
323
- return null;
324
- }
325
- if (typeof result !== 'object' || !result || typeof (result as Event).id !== 'string') {
326
- // Host returned a non-event shape; fall back.
327
- if (!_beforeSendThrewWarned) {
328
- _beforeSendThrewWarned = true;
329
- logger.warn('capture', 'beforeSend returned non-event shape; falling back to unmodified event');
330
- }
331
- return event;
332
- }
333
- return result;
334
- } catch (e) {
335
- if (!_beforeSendThrewWarned) {
336
- _beforeSendThrewWarned = true;
337
- logger.warn('capture', 'beforeSend threw; falling back to unmodified event', e);
338
- }
339
- return event;
340
- }
341
- }
342
-
343
- /** v0.9.6 #2 — upload the wireframe replay ring as a `replay`
344
- * attachment. Plain NDJSON (one snapshot per line) — server may
345
- * gzip on storage; the network upload is base64.
346
- *
347
- * rc.4: route through `base64Utf8` so non-Latin-1 text inside any
348
- * walked TextView (Japanese / Chinese / em-dash etc.) doesn't blow
349
- * up the Hermes-spec `btoa`. The pre-rc.4 inline `btoa(ndjson)` path
350
- * threw `InvalidCharacterError` on those code points, the
351
- * surrounding catch swallowed it silently, and the replay
352
- * attachment never landed. Insight 2026-05-18 verify caught it
353
- * after rc.3's walker fix surfaced deep TextView content. Dev
354
- * logs replace the silent catch so the next failure shape is
355
- * visible. */
356
- async function captureAndAttachReplay(event: Event, ndjson: string): Promise<void> {
357
- try {
358
- const base64 = base64Utf8(ndjson);
359
- const meta = await uploadAttachment(
360
- event.id,
361
- 'replay',
362
- { base64, mediaType: 'application/x-ndjson' },
363
- { source: 'js' },
364
- );
365
- if (!meta) {
366
- logger.debug('capture', 'replay upload returned null',
367
- 'eventId=', event.id,
368
- 'ndjsonBytes=', ndjson.length,
369
- );
370
- return;
371
- }
372
- if (!event.attachments) event.attachments = [];
373
- event.attachments.push(meta);
374
- } catch (e) {
375
- logger.debug('capture', 'replay attachment threw',
376
- 'eventId=', event.id,
377
- 'ndjsonBytes=', ndjson.length,
378
- e,
379
- );
380
- }
381
- }
382
-
383
- /** v0.9.2 +S2 — upload the rolling state-snapshot ring as a
384
- * `stateSnapshot` attachment so the dashboard time-travel viewer can
385
- * scrub through diffs alongside the breadcrumb timeline. */
386
- async function captureAndAttachStateSnapshots(
387
- event: Event,
388
- snapshots: ReturnType<typeof getStateSnapshots>,
389
- ): Promise<void> {
390
- try {
391
- const payload = JSON.stringify({ snapshots });
392
- const base64 = base64Utf8(payload);
393
- const meta = await uploadAttachment(
394
- event.id,
395
- 'stateSnapshot',
396
- { base64, mediaType: 'application/json' },
397
- { source: 'js' },
398
- );
399
- if (meta) {
400
- if (!event.attachments) event.attachments = [];
401
- event.attachments.push(meta);
402
- }
403
- } catch {
404
- // best-effort
405
- }
406
- }
407
-
408
- /**
409
- * Phase 46 — seal the trail buffer, upload it as a `sessionTrail`
410
- * attachment, attach the ref. Best-effort: any failure leaves a
411
- * breadcrumb and lets the event ship without the trail.
412
- *
413
- * The trail is **always cleared** after `captureException`, even if
414
- * upload fails — we don't want a stale 30-step buffer leaking into
415
- * the next crash's trail.
416
- */
417
- async function captureAndAttachSessionTrail(event: Event): Promise<void> {
418
- const trail = getTrailBuffer();
419
- const payload = sealTrail(trail);
420
- trail.clear();
421
- const json = JSON.stringify(payload);
422
- const base64 = base64Utf8(json);
423
- const attachment = await uploadAttachment(
424
- event.id,
425
- 'sessionTrail',
426
- { base64, mediaType: 'application/json' },
427
- { source: 'js' },
428
- );
429
- if (!attachment) {
430
- addBreadcrumb({ type: 'custom', data: { reason: 'session-trail-upload-failed' } });
431
- return;
432
- }
433
- if (!event.attachments) event.attachments = [];
434
- event.attachments.push(attachment);
435
- }
436
-
437
- export const captureException = captureError;
438
-
439
- const DEFAULT_MESSAGE_LEVEL: MessageLevel = 'info';
440
-
441
- /**
442
- * Manually report an issue without an Error instance.
443
- *
444
- * Routes to the dashboard Issues module — distinct from `track`
445
- * (analytics) and `recordMetric` (numeric). Use for "operator should
446
- * look at this" signals: a fallback that fired, an unexpected state,
447
- * a feature flag rollout that crossed a threshold.
448
- *
449
- * sentori.captureMessage('Payment provider returned 500, used fallback')
450
- * sentori.captureMessage('Detected impossible state in session reducer', {
451
- * level: 'error',
452
- * tags: { reducer: 'session' },
453
- * })
454
- *
455
- * Wrapped in `safeFn` per the NEVER rule — any internal failure is
456
- * swallowed and (optionally) self-reported; the host app never sees
457
- * a thrown error.
458
- */
459
- export const captureMessage = safeFn(
460
- 'captureMessage',
461
- (message: string, opts: CaptureMessageOptions = {}): void => {
462
- if (!isInitialized()) return;
463
- if (typeof message !== 'string' || message.length === 0) return;
464
- const config = getConfig();
465
- if (!config) return;
466
-
467
- if (!shouldSample(config.messageSampleRate)) {
468
- addBreadcrumb({ type: 'custom', data: { reason: 'sampled-out', kind: 'message' } });
469
- return;
470
- }
471
-
472
- const flags = getFeatureFlagSnapshot();
473
- const bundle = getBundleInfo();
474
- const crumbs = opts.breadcrumbs ?? getBreadcrumbs();
475
-
476
- const event: Event = {
477
- id: uuidV7(),
478
- timestamp: new Date().toISOString(),
479
- kind: 'message',
480
- level: opts.level ?? DEFAULT_MESSAGE_LEVEL,
481
- message,
482
- platform: 'javascript',
483
- release: config.release,
484
- environment: config.environment,
485
- device: collectDevice(),
486
- app: collectApp(config.release),
487
- user: opts.user ?? _user,
488
- tags: mergeScopeTags(opts.tags),
489
- ...(flags ? { flags } : {}),
490
- ...(bundle ? { bundle } : {}),
491
- breadcrumbs: crumbs,
492
- };
493
-
494
- const finalEvent = applyBeforeSend(event, config.beforeSend);
495
- if (finalEvent === null) return;
496
- enqueue(finalEvent);
497
- },
498
- );
499
-
500
- /** rc.4 — test hook. The real replay attach path is internal so we
501
- * don't bloat the public surface, but the encoding bug Insight hit
502
- * on 2026-05-18 needs a behaviour-level test that exercises the
503
- * same code path captureException runs in production. */
504
- export const __captureAndAttachReplayForTests = captureAndAttachReplay;
505
-
506
- /** v2.3 — test hook for the beforeSend dispatcher. The dispatcher
507
- * is internal so the unit test reaches it directly without
508
- * needing to stub the whole transport pipeline. */
509
- export const __applyBeforeSendForTests = applyBeforeSend;
510
-
511
- /** Phase 42 sub-D.08: per-session screenshot quota gate. */
512
- function allowScreenshot(): boolean {
513
- const budget = screenshotBudget();
514
- if (budget < 0) return true; // dev: unlimited
515
- if (_screenshotsTaken >= budget) return false;
516
- _screenshotsTaken += 1;
517
- return true;
518
- }
519
-
520
- /**
521
- * Phase 42 sub-D.06/07: take a screenshot, upload it, push the
522
- * server-issued ref into `event.attachments`. Every step is
523
- * best-effort — on any failure we leave a breadcrumb and let the
524
- * event ship without a thumbnail.
525
- */
526
- async function captureAndAttachScreenshot(event: Event): Promise<void> {
527
- let blob: Awaited<ReturnType<typeof captureScreenshot>> = null;
528
- try {
529
- blob = await captureScreenshot();
530
- } catch (e) {
531
- logger.debug('capture', 'screenshot capture threw', e);
532
- }
533
- if (!blob) {
534
- logger.debug('capture', 'screenshot blob null — native module missing or capture returned null',
535
- 'eventId=', event.id,
536
- );
537
- addBreadcrumb({ type: 'custom', data: { reason: 'screenshot-capture-failed' } });
538
- return;
539
- }
540
- logger.debug('capture', 'screenshot blob ok, uploading',
541
- 'eventId=', event.id,
542
- 'mediaType=', blob.mediaType,
543
- 'base64Bytes=', blob.base64.length,
544
- );
545
- const attachment: AttachmentMeta | null = await uploadAttachment(
546
- event.id,
547
- 'screenshot',
548
- blob,
549
- { source: 'js' },
550
- );
551
- if (!attachment) {
552
- logger.debug('capture', 'screenshot upload returned null', 'eventId=', event.id);
553
- addBreadcrumb({ type: 'custom', data: { reason: 'screenshot-upload-failed' } });
554
- return;
555
- }
556
- if (!event.attachments) event.attachments = [];
557
- event.attachments.push(attachment);
558
- }
559
-
560
- const errorToObject = (error: Error): SentoriError => {
561
- const causeRaw = (error as { cause?: unknown }).cause;
562
- let cause: SentoriError | null = null;
563
- if (causeRaw instanceof Error) {
564
- cause = errorToObject(causeRaw);
565
- }
566
-
567
- // v0.9.5 #8 — TurboModule swallowed-exception bridge. If the host
568
- // wrapped a native call with `@try @catch + recordException`, the
569
- // native ring may hold a fresh entry (< 1 s old). Synthesize that
570
- // as a `cause` so the JS event includes the original native stack.
571
- if (cause === null) {
572
- const recent = getRecentNativeException();
573
- if (recent && recent.ageMs <= 1500) {
574
- cause = {
575
- type: recent.name || 'NativeException',
576
- message: recent.reason,
577
- stack: recent.stack.map((line, i) => ({
578
- function: line.trim(),
579
- file: '<native>',
580
- inApp: false,
581
- line: i + 1,
582
- })),
583
- cause: null,
584
- };
585
- }
586
- }
587
-
588
- return {
589
- type: error.name || 'Error',
590
- message: error.message,
591
- stack: parseStack(error.stack),
592
- cause,
593
- };
594
- };
595
-
596
- const collectDevice = (): Device => {
597
- let os: Device['os'] = 'other';
598
- let osVersion = '0';
599
- let locale: string | undefined;
600
- const networkType = getCachedNetworkType();
601
- try {
602
- const RN = require('react-native') as {
603
- NativeModules: {
604
- I18nManager?: { localeIdentifier?: string };
605
- SettingsManager?: {
606
- settings?: { AppleLanguages?: string[]; AppleLocale?: string };
607
- };
608
- };
609
- Platform: { OS: string; Version: string | number };
610
- };
611
- const rnOS = RN.Platform.OS;
612
- os = rnOS === 'ios' || rnOS === 'android' || rnOS === 'web' ? rnOS : 'other';
613
- osVersion = String(RN.Platform.Version);
614
- // v0.8.0-a — RN reads user locale through native modules. These
615
- // are stable RN-internal modules (SettingsManager since 0.4,
616
- // I18nManager since 0.16) so we can read them directly without
617
- // an extra peer dep. iOS returns e.g. "en_US"; Android returns
618
- // e.g. "en_US" via `getDefault().toString()`. `AppleLocale` is
619
- // the format the user picked in Settings; `AppleLanguages[0]`
620
- // is the resolved language priority — prefer the former.
621
- if (rnOS === 'ios') {
622
- const s = RN.NativeModules.SettingsManager?.settings;
623
- locale = s?.AppleLocale ?? s?.AppleLanguages?.[0];
624
- } else if (rnOS === 'android') {
625
- locale = RN.NativeModules.I18nManager?.localeIdentifier;
626
- }
627
- } catch {
628
- // not in RN runtime (jest, bun test)
629
- }
630
- const device: Device = { os, osVersion };
631
- if (locale) device.locale = locale;
632
- if (networkType) device.networkType = networkType;
633
- // v1.1 chunk S1 — stable per-install id. Read sync from the
634
- // in-memory cache populated by the install-id module's first
635
- // resolve (kicked off from init()). `null` before init resolves —
636
- // the field is omitted in that case rather than blocking event
637
- // assembly, which sits on the JS thread.
638
- const installId = peekInstallId();
639
- if (installId) device.installId = installId;
640
- return device;
641
- };
642
-
643
- const collectApp = (release: string): App => {
644
- const m = /^(?:[^@]+@)?([^+]+)(?:\+(.+))?$/.exec(release);
645
- const version = m?.[1] ?? '0.0.0';
646
- const build = m?.[2];
647
-
648
- let rnVersion = 'unknown';
649
- try {
650
- rnVersion = (require('react-native/package.json') as { version: string }).version;
651
- } catch {
652
- // not in RN runtime
653
- }
654
-
655
- return {
656
- version,
657
- build,
658
- framework: { name: 'react-native', version: rnVersion },
659
- };
660
- };