@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/lifecycle.ts DELETED
@@ -1,76 +0,0 @@
1
- // v2.0 W3 — top-level lifecycle: flush / close.
2
- //
3
- // Sentry / OTel parity: a single `flush(timeoutMs?)` that drains
4
- // every in-flight buffer (events / metrics / track), gated by a
5
- // timeout so a slow network doesn't hang the host's shutdown path.
6
- //
7
- // Use before short-lived process exit (CLI / serverless function /
8
- // fixture cleanup) to ensure pending captures land before the
9
- // process dies.
10
-
11
- import { flush as flushTransport } from './transport';
12
- import { flushMetrics } from './metrics';
13
- import { flushTrack } from './track';
14
- import { reportInternal } from '@goliapkg/sentori-core';
15
-
16
- let _closed = false;
17
-
18
- /**
19
- * Force-flush every pending Sentori buffer (events, metrics,
20
- * track). Returns when the flush settles or the timeout fires —
21
- * whichever happens first.
22
- *
23
- * Never rejects: per the NEVER rule, individual flush failures
24
- * are silently absorbed (and self-reported via the internal
25
- * circuit-breaker), and the resolved promise's value is undefined.
26
- *
27
- * await sentori.flush(5_000) // wait up to 5 s, then move on
28
- * process.exit(0)
29
- */
30
- export async function flush(timeoutMs: number = 5_000): Promise<void> {
31
- if (_closed) return;
32
- try {
33
- const drainAll = Promise.all([
34
- flushTransport().catch((err) => {
35
- reportInternal('flush.transport', err);
36
- }),
37
- flushMetrics().catch((err) => {
38
- reportInternal('flush.metrics', err);
39
- }),
40
- flushTrack().catch((err) => {
41
- reportInternal('flush.track', err);
42
- }),
43
- ]).then(() => undefined);
44
- const timer = new Promise<void>((resolve) => setTimeout(resolve, timeoutMs));
45
- await Promise.race([drainAll, timer]);
46
- } catch (err) {
47
- // Belt-and-braces: even though each branch already has its own
48
- // catch, the wrapping race shouldn't be able to surface a
49
- // failure to the host either. NEVER rule.
50
- reportInternal('flush', err);
51
- }
52
- }
53
-
54
- /**
55
- * Flush + shut down. After `close()`, further capture* calls remain
56
- * silent no-ops via `_closed` gate. Idempotent — re-calling is safe.
57
- *
58
- * await sentori.close()
59
- * // SDK is asleep; no more events go out.
60
- */
61
- export async function close(timeoutMs?: number): Promise<void> {
62
- await flush(timeoutMs);
63
- _closed = true;
64
- }
65
-
66
- /** Test hook — reset the shutdown latch so unit tests don't have to
67
- * re-init the whole SDK between cases. */
68
- export function __resetLifecycleForTests(): void {
69
- _closed = false;
70
- }
71
-
72
- /** Internal — capture* implementations check this to short-circuit
73
- * after `close()`. */
74
- export function isClosed(): boolean {
75
- return _closed;
76
- }
package/src/measure.ts DELETED
@@ -1,28 +0,0 @@
1
- // v0.9.0 #14 — `sentori.measureFn(name, fn)`. Profile-lite. Wrap an
2
- // async (or sync) function call in a span so it shows on the issue
3
- // detail trace waterfall without writing the boilerplate every time.
4
- // The full Hermes-sampler profiler (#4) is the deep version of this
5
- // idea; `measureFn` is the cheap version that doesn't need a native
6
- // module.
7
-
8
- import { startSpan } from '@goliapkg/sentori-core';
9
-
10
- export async function measureFn<T>(
11
- name: string,
12
- fn: () => Promise<T> | T,
13
- opts?: { tags?: Record<string, string> },
14
- ): Promise<T> {
15
- const span = startSpan('sentori.measureFn', {
16
- name,
17
- tags: opts?.tags ?? {},
18
- });
19
- try {
20
- const result = await fn();
21
- span.finish({ status: 'ok' });
22
- return result;
23
- } catch (e) {
24
- if (e instanceof Error) span.setTag('error.message', e.message);
25
- span.finish({ status: 'error' });
26
- throw e;
27
- }
28
- }
package/src/metrics.ts DELETED
@@ -1,94 +0,0 @@
1
- // v0.8.3 — custom metrics buffer.
2
- //
3
- // `recordMetric(name, value, tags?)` pushes a point into a fixed-size
4
- // ring. A timer flushes the ring every 30 s (or when the buffer is
5
- // full); captureException also forces a flush so the metrics line up
6
- // with the error event in the dashboard. Best-effort: a flush failure
7
- // drops the batch on the floor — metrics aren't critical telemetry.
8
- //
9
- // Why not one fetch per point: noisy loops (`recordMetric('frame', 1)`
10
- // in a render hook) would burn the JS thread + saturate the device's
11
- // outgoing connection pool. Batching makes the SDK safe to use as a
12
- // cheap counter primitive.
13
-
14
- import type { SpanContextLike } from '@goliapkg/sentori-core';
15
-
16
- import { getConfig, isInitialized } from './config';
17
- import { sendMetricsBatch } from './transport';
18
-
19
- type Point = {
20
- name: string;
21
- tags?: Record<string, string>;
22
- ts: string;
23
- value: number;
24
- };
25
-
26
- const MAX_BUFFER = 500;
27
- const FLUSH_INTERVAL_MS = 30_000;
28
-
29
- let _buf: Point[] = [];
30
- let _timer: ReturnType<typeof setInterval> | null = null;
31
-
32
- /**
33
- * Record a numeric metric point. Cheap to call from a render hook —
34
- * pushes into a 500-slot ring drained every 30 s (or on overflow).
35
- *
36
- * v2.0 — accepts an optional `parent: SpanContextLike` so a metric
37
- * can be correlated to a span. The dashboard span-detail view joins
38
- * metric points by `tags.span_id` / `tags.trace_id`.
39
- *
40
- * const span = sentori.startSpan({ name: 'db.query users' })
41
- * sentori.recordMetric('db.query.duration_ms', 42, undefined, { parent: span })
42
- * span.end({ status: 'ok' })
43
- */
44
- export function recordMetric(
45
- name: string,
46
- value: number,
47
- tags?: Record<string, string>,
48
- opts?: { parent?: SpanContextLike },
49
- ): void {
50
- if (!isInitialized()) return;
51
- if (typeof name !== 'string' || name.length === 0 || name.length > 200) return;
52
- if (typeof value !== 'number' || !Number.isFinite(value)) return;
53
- if (tags && Object.keys(tags).length > 20) return;
54
- // Merge span-context tags so the dashboard can join metric points
55
- // to the span that produced them without a separate schema column.
56
- const finalTags: Record<string, string> | undefined = opts?.parent
57
- ? { ...(tags ?? {}), span_id: opts.parent.spanId, trace_id: opts.parent.traceId }
58
- : tags;
59
- _buf.push({ name, tags: finalTags, ts: new Date().toISOString(), value });
60
- if (_buf.length >= MAX_BUFFER) {
61
- void flushMetrics();
62
- }
63
- }
64
-
65
- export async function flushMetrics(): Promise<void> {
66
- if (_buf.length === 0) return;
67
- const config = getConfig();
68
- if (!config) return;
69
- const batch = _buf;
70
- _buf = [];
71
- await sendMetricsBatch(config.ingestUrl, config.token, batch);
72
- }
73
-
74
- /**
75
- * Start the 30 s flush timer. Called once from `init()`. Idempotent.
76
- * `clearMetricsTimer` is exposed for tests / teardown.
77
- */
78
- export function startMetricsTimer(): void {
79
- if (_timer !== null) return;
80
- _timer = setInterval(() => {
81
- void flushMetrics();
82
- }, FLUSH_INTERVAL_MS);
83
- // Don't keep the process alive solely for this timer (Node would).
84
- // In RN setInterval is a NoopRef so this is harmless there.
85
- (_timer as unknown as { unref?: () => void }).unref?.();
86
- }
87
-
88
- export function __resetMetricsForTests(): void {
89
- if (_timer !== null) {
90
- clearInterval(_timer);
91
- _timer = null;
92
- }
93
- _buf = [];
94
- }
@@ -1,140 +0,0 @@
1
- // v0.9.1 +S4 — pre-crash sentinel.
2
- //
3
- // Predictive (vs reactive) telemetry. Subscribes to JS-thread frame
4
- // timing via requestAnimationFrame and, when a rolling 60-frame
5
- // window has ≥ 50% of frames slower than 32 ms (i.e. < 30 fps for
6
- // half the window), emits an `event.kind = nearCrash` to the server.
7
- // Backend stores it in the same events stream so the dashboard shows
8
- // "X user sessions had near-crash signals 4 minutes before the
9
- // actual NSException".
10
- //
11
- // Memory pressure / OOM / storage low signals will need native
12
- // system observers and ship in v1.0. v0.9.1 covers the most common
13
- // runaway-render-loop case purely from JS.
14
-
15
- import { startSpan } from '@goliapkg/sentori-core';
16
-
17
- import { getBundleInfo } from './bundle-info';
18
- import { collectDeviceForSentinel, getAppForSentinel } from './sentinel-context';
19
- import { getConfig, isInitialized } from './config';
20
- import { enqueue } from './transport';
21
- import { uuidV7 } from './uuid';
22
- import type { Event } from './types';
23
-
24
- const FRAME_BUDGET_MS = 32; // < 30 fps
25
- const WINDOW_FRAMES = 60; // ~1 s at 60 fps
26
- const TRIP_RATIO = 0.5;
27
- const COOLDOWN_MS = 60_000; // don't spam: one nearCrash event per minute
28
-
29
- let _running = false;
30
- let _lastFrameAt = 0;
31
- let _slowFrames = 0;
32
- let _totalFrames = 0;
33
- let _lastEmitAt = 0;
34
- let _channels: Set<string> = new Set();
35
-
36
- export type PreCrashChannel =
37
- | 'frame-budget-overrun'
38
- | 'memory-pressure' // native, v1.0
39
- | 'oom-warning' // native, v1.0
40
- | 'storage-low'; // native, v1.0
41
-
42
- export type PreCrashSentinelOptions = {
43
- enabled: boolean;
44
- channels?: PreCrashChannel[];
45
- /** Lower → more sensitive. Default 32 ms (< 30 fps). */
46
- frameBudgetMs?: number;
47
- /** Fraction of frames in the window that must miss budget. Default 0.5. */
48
- tripRatio?: number;
49
- };
50
-
51
- export function startPreCrashSentinel(opts: PreCrashSentinelOptions): void {
52
- if (!opts.enabled || _running) return;
53
- _running = true;
54
- _channels = new Set(opts.channels ?? ['frame-budget-overrun']);
55
-
56
- if (_channels.has('frame-budget-overrun')) {
57
- startFrameBudgetWatch(opts.frameBudgetMs ?? FRAME_BUDGET_MS, opts.tripRatio ?? TRIP_RATIO);
58
- }
59
- // Native channels (memory-pressure, oom-warning, storage-low) hook
60
- // through a TODO native module in v1.0.
61
- }
62
-
63
- export function stopPreCrashSentinel(): void {
64
- _running = false;
65
- _slowFrames = 0;
66
- _totalFrames = 0;
67
- _channels.clear();
68
- }
69
-
70
- function startFrameBudgetWatch(budgetMs: number, tripRatio: number): void {
71
- if (typeof requestAnimationFrame !== 'function') return;
72
- _lastFrameAt = Date.now();
73
- function tick() {
74
- if (!_running) return;
75
- const now = Date.now();
76
- const delta = now - _lastFrameAt;
77
- _lastFrameAt = now;
78
- if (delta >= budgetMs) _slowFrames++;
79
- _totalFrames++;
80
- if (_totalFrames >= WINDOW_FRAMES) {
81
- const ratio = _slowFrames / _totalFrames;
82
- if (ratio >= tripRatio && now - _lastEmitAt > COOLDOWN_MS) {
83
- _lastEmitAt = now;
84
- emitNearCrash({
85
- slowFrames: _slowFrames,
86
- totalFrames: _totalFrames,
87
- ratio,
88
- windowMs: WINDOW_FRAMES * (budgetMs / 2), // approximate
89
- channel: 'frame-budget-overrun',
90
- });
91
- }
92
- _slowFrames = 0;
93
- _totalFrames = 0;
94
- }
95
- requestAnimationFrame(tick);
96
- }
97
- requestAnimationFrame(tick);
98
- }
99
-
100
- function emitNearCrash(data: {
101
- slowFrames: number;
102
- totalFrames: number;
103
- ratio: number;
104
- windowMs: number;
105
- channel: PreCrashChannel;
106
- }): void {
107
- if (!isInitialized()) return;
108
- const config = getConfig();
109
- if (!config) return;
110
- const span = startSpan('sentori.nearCrash', {
111
- name: data.channel,
112
- tags: {
113
- 'nearCrash.channel': data.channel,
114
- 'nearCrash.ratio': data.ratio.toFixed(3),
115
- 'nearCrash.slow_frames': String(data.slowFrames),
116
- 'nearCrash.total_frames': String(data.totalFrames),
117
- },
118
- });
119
- span.finish({ status: 'ok' });
120
- const event: Event = {
121
- id: uuidV7(),
122
- timestamp: new Date().toISOString(),
123
- kind: 'nearCrash',
124
- platform: 'javascript',
125
- release: config.release,
126
- environment: config.environment,
127
- device: collectDeviceForSentinel(),
128
- app: getAppForSentinel(config.release),
129
- ...(getBundleInfo() ? { bundle: getBundleInfo() as { id: string } } : {}),
130
- error: {
131
- type: 'NearCrash',
132
- message: `frame budget overrun: ${(data.ratio * 100).toFixed(0)}% of last ${data.totalFrames} frames slow`,
133
- stack: [],
134
- },
135
- tags: {
136
- 'nearCrash.channel': data.channel,
137
- },
138
- };
139
- enqueue(event);
140
- }
@@ -1,165 +0,0 @@
1
- // v1.1 chunk S2 — security event reporting.
2
- //
3
- // `sentori.reportSecurity(kind, data)` POSTs a single security event
4
- // to `/v1/security:report`. Helpers wrap common kinds with the right
5
- // payload shape so dashboards can rely on it without coordinating
6
- // schemas with every host app.
7
- //
8
- // Why a separate API from `captureException` / `track`: security
9
- // reports have different retention + access patterns (the trust
10
- // scoring engine in S3 reads them on a hot path) and conflating
11
- // them with errors would pollute issue grouping. Single endpoint,
12
- // no batching: pin mismatches and root-detection signals are
13
- // low-volume by nature (one per app-lifetime in most cases).
14
-
15
- import { getConfig, isInitialized } from './config';
16
- import { peekInstallId } from './install-id';
17
- import { getCurrentUserId } from './capture';
18
-
19
- const SDK_VERSION = '0.0.0';
20
-
21
- export type SecurityReportData = Record<string, unknown>;
22
-
23
- /**
24
- * Report an arbitrary security signal. Fire-and-forget; resolves
25
- * with the server-assigned event id on success or `null` on any
26
- * failure (network down, server unhappy, SDK not initialised).
27
- *
28
- * Use the dedicated helpers (`reportPinMismatch`, future
29
- * `reportRootDetected`, …) when their shape applies — the dashboard
30
- * renders kind-specific panels off the well-known kinds.
31
- */
32
- export async function reportSecurity(
33
- kind: string,
34
- data: SecurityReportData = {},
35
- ): Promise<null | string> {
36
- if (!isInitialized()) return null;
37
- if (typeof kind !== 'string' || kind.length === 0 || kind.length > 100) {
38
- return null;
39
- }
40
- if (Object.keys(data).length > 40) return null;
41
-
42
- const config = getConfig();
43
- if (!config) return null;
44
- const body = {
45
- kind,
46
- data,
47
- ts: new Date().toISOString(),
48
- userId: getCurrentUserId(),
49
- installId: peekInstallId() ?? undefined,
50
- release: config.release,
51
- environment: config.environment,
52
- };
53
- try {
54
- const resp = await fetch(`${config.ingestUrl}/v1/security:report`, {
55
- body: JSON.stringify(body),
56
- headers: {
57
- Authorization: `Bearer ${config.token}`,
58
- 'Content-Type': 'application/json',
59
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
60
- },
61
- method: 'POST',
62
- });
63
- if (!resp.ok) return null;
64
- const parsed = (await resp.json().catch(() => null)) as null | { id?: string };
65
- return parsed?.id ?? null;
66
- } catch {
67
- return null;
68
- }
69
- }
70
-
71
- /**
72
- * v1.1 chunk S4 — link the current device's user / install to a
73
- * federated identity (e.g. a Google sub or Apple `sub`). Idempotent;
74
- * call on every sign-in. Posts to `/v1/security/link`. The dashboard
75
- * uses this to stitch the same user across projects in the Posture
76
- * cross-project view.
77
- *
78
- * Privacy: only the opaque OAuth `subject` value travels. Never pass
79
- * the email, display name, avatar, or any other identity attribute.
80
- */
81
- export async function linkFederatedIdentity(args: {
82
- provider: string;
83
- subject: string;
84
- userId?: string;
85
- }): Promise<boolean> {
86
- if (!args || typeof args.provider !== 'string' || args.provider.length === 0) return false;
87
- if (typeof args.subject !== 'string' || args.subject.length === 0) return false;
88
- if (!isInitialized()) return false;
89
- const config = getConfig();
90
- if (!config) return false;
91
- const installId = peekInstallId() ?? undefined;
92
- const body = {
93
- provider: args.provider,
94
- subject: args.subject,
95
- userId: args.userId ?? getCurrentUserId(),
96
- installId,
97
- };
98
- try {
99
- const resp = await fetch(`${config.ingestUrl}/v1/security/link`, {
100
- body: JSON.stringify(body),
101
- headers: {
102
- Authorization: `Bearer ${config.token}`,
103
- 'Content-Type': 'application/json',
104
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
105
- },
106
- method: 'POST',
107
- });
108
- return resp.ok;
109
- } catch {
110
- return false;
111
- }
112
- }
113
-
114
- /**
115
- * TLS certificate pin mismatch — caller observed a server cert that
116
- * didn't match the configured pin set. Posts `kind = 'pin.mismatch'`
117
- * with the expected + observed pin (or hash) so the dashboard's
118
- * Pin anomaly panel can cluster reports by server.
119
- */
120
- export async function reportPinMismatch(args: {
121
- expected: string;
122
- observed: string;
123
- /** Hostname the SDK was connecting to. Used by the dashboard to
124
- * cluster reports per server. */
125
- serverName: string;
126
- }): Promise<null | string> {
127
- if (!args || typeof args.serverName !== 'string' || args.serverName.length === 0) {
128
- return null;
129
- }
130
- // The serverName rides on the top-level envelope (column-typed on
131
- // the server) so dashboard queries don't need to crack the data
132
- // JSONB. We still echo it inside `data` for self-contained payloads.
133
- if (!isInitialized()) return null;
134
- const config = getConfig();
135
- if (!config) return null;
136
- const body = {
137
- kind: 'pin.mismatch',
138
- serverName: args.serverName,
139
- ts: new Date().toISOString(),
140
- userId: getCurrentUserId(),
141
- installId: peekInstallId() ?? undefined,
142
- release: config.release,
143
- environment: config.environment,
144
- data: {
145
- expected: args.expected,
146
- observed: args.observed,
147
- },
148
- };
149
- try {
150
- const resp = await fetch(`${config.ingestUrl}/v1/security:report`, {
151
- body: JSON.stringify(body),
152
- headers: {
153
- Authorization: `Bearer ${config.token}`,
154
- 'Content-Type': 'application/json',
155
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
156
- },
157
- method: 'POST',
158
- });
159
- if (!resp.ok) return null;
160
- const parsed = (await resp.json().catch(() => null)) as null | { id?: string };
161
- return parsed?.id ?? null;
162
- } catch {
163
- return null;
164
- }
165
- }
@@ -1,137 +0,0 @@
1
- // v2.1 W2 part 3 — FPS auto-instrument.
2
- //
3
- // Rolling 5 s window of inter-frame Δt measured via rAF; emit
4
- // runtime.fps.p50 + runtime.fps.p95 every 5 s.
5
- //
6
- // Perf bedrock (.claude/CLAUDE.md):
7
- // • per-tick cost must be < 0.5 ms on a Pixel-5-equivalent
8
- // bench → stop-ship gate in W2 part 4 CI
9
- // • main-thread sustained < 1 %
10
- // • no allocation inside the rAF callback hot path
11
- //
12
- // Implementation choices:
13
- // • single-frame Δt = perf.now() - prev — cheap, no allocation
14
- // • rolling window stored in a fixed-size Float32Array (no
15
- // splice / shift — circular buffer write index)
16
- // • percentile computed by copying-and-sorting only at emit
17
- // time (every 5 s, ~300 floats) — no per-tick sort
18
- //
19
- // Bail-outs:
20
- // • if requestAnimationFrame isn't available (rare RN host),
21
- // never start the loop
22
- // • if perf.now() is missing (older RN engines), fall back to
23
- // Date.now() — coarser but better than crashing
24
-
25
- import { emitMetric } from '@goliapkg/sentori-core';
26
-
27
- const TICK_FRAMES_BEFORE_EMIT = 300; // ~5 s at 60 fps
28
- const SAMPLE_CAP = 600; // safety: 10 s at 60 fps caps memory
29
-
30
- let _running = false;
31
- let _samples = new Float32Array(SAMPLE_CAP);
32
- let _write = 0;
33
- let _count = 0;
34
- let _prev = 0;
35
-
36
- function now(): number {
37
- // Engines without perf.now() (very old RN runtimes / minimal
38
- // JSCore builds) fall back to wall-clock. Date.now() has 1 ms
39
- // resolution so FPS readings cap at 1000; good enough.
40
- const p = (globalThis as { performance?: { now?: () => number } }).performance;
41
- return p?.now ? p.now() : Date.now();
42
- }
43
-
44
- function rafTick(t: number): void {
45
- if (!_running) return;
46
- if (_prev !== 0) {
47
- const dt = t - _prev;
48
- if (dt > 0 && dt < 1000) {
49
- _samples[_write] = dt;
50
- _write = (_write + 1) % SAMPLE_CAP;
51
- _count += 1;
52
- if (_count >= TICK_FRAMES_BEFORE_EMIT) {
53
- emitWindow();
54
- _count = 0;
55
- _write = 0;
56
- }
57
- }
58
- }
59
- _prev = t;
60
- requestAnimationFrame(rafTick);
61
- }
62
-
63
- function emitWindow(): void {
64
- // Copy + sort. ~300 floats, well under 1 ms even on a slow phone.
65
- // Allocation lives outside the per-tick hot path — only at emit.
66
- const n = _count;
67
- if (n === 0) return;
68
- const slice = new Float32Array(n);
69
- for (let i = 0; i < n; i++) slice[i] = _samples[i]!;
70
- slice.sort();
71
- const p50dt = percentile(slice, 0.5);
72
- const p95dt = percentile(slice, 0.95);
73
- // FPS = 1000 ms / per-frame Δt.
74
- const fpsP50 = p50dt > 0 ? Math.round(1000 / p50dt) : 0;
75
- // p95 of Δt is the *slowest* 95 % — i.e. when frames are bad.
76
- // p95 FPS conventionally means "5 % worst frames as fps".
77
- const fpsP5Slow = p95dt > 0 ? Math.round(1000 / p95dt) : 0;
78
- emitMetric('runtime.fps.p50', fpsP50);
79
- emitMetric('runtime.fps.p95', fpsP5Slow);
80
- }
81
-
82
- function percentile(sorted: Float32Array, q: number): number {
83
- if (sorted.length === 0) return 0;
84
- // Discrete percentile — index = ceil(q * n) - 1, clamped.
85
- const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1));
86
- return sorted[idx]!;
87
- }
88
-
89
- /** Idempotent start. Safe to call multiple times; only the first
90
- * call kicks off the rAF loop. */
91
- export function startFpsInstrument(): void {
92
- if (_running) return;
93
- if (typeof requestAnimationFrame !== 'function') return;
94
- _running = true;
95
- _prev = 0;
96
- _count = 0;
97
- _write = 0;
98
- requestAnimationFrame(rafTick);
99
- }
100
-
101
- /** Stop the loop. Used by tests + by hosts that want to opt out
102
- * mid-session. */
103
- export function stopFpsInstrument(): void {
104
- _running = false;
105
- }
106
-
107
- /** Test-only state inspection. */
108
- export function __peekFpsInstrumentState(): {
109
- count: number;
110
- running: boolean;
111
- } {
112
- return { count: _count, running: _running };
113
- }
114
-
115
- /** Test-only force-emit (skips the every-300-frames gate). */
116
- export function __forceEmitFpsForTests(): void {
117
- emitWindow();
118
- _count = 0;
119
- _write = 0;
120
- }
121
-
122
- /** Test-only sample injection so we can assert without spinning
123
- * rAF in the test runner. */
124
- export function __pushSampleForTests(dt: number): void {
125
- _samples[_write] = dt;
126
- _write = (_write + 1) % SAMPLE_CAP;
127
- _count += 1;
128
- }
129
-
130
- /** Test-only reset between runs. */
131
- export function __resetFpsInstrumentForTests(): void {
132
- _running = false;
133
- _samples = new Float32Array(SAMPLE_CAP);
134
- _write = 0;
135
- _count = 0;
136
- _prev = 0;
137
- }