@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/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/mask.ts DELETED
@@ -1,41 +0,0 @@
1
- // v0.7.3 — mask discovery via consumer-supplied query callback.
2
- //
3
- // The SDK does not own a registry of masked regions. The host app
4
- // keeps its own (e.g. a `Set<string>` updated as `<Maskable>`
5
- // components mount/unmount) and hands the SDK a thunk that returns
6
- // the current list of native-IDs to redact. The SDK calls the thunk
7
- // once per screenshot capture — cheap, called rarely, only on error.
8
- //
9
- // Why this shape: a logging SDK should never live on the render
10
- // path. Earlier iterations exported `<MaskRegion>` (React component)
11
- // and `setMaskedNode` (imperative ref helper), which forced every
12
- // PII-bearing UI file to import from the SDK and put SDK bugs in
13
- // the user's render tree. This module is JS-only — no React, no JSX,
14
- // no native module touch — so swapping or removing the SDK doesn't
15
- // affect rendering.
16
-
17
- type MaskQuery = () => string[];
18
-
19
- let _query: MaskQuery | null = null;
20
-
21
- /**
22
- * Register a callback the SDK calls right before each screenshot
23
- * capture. Return the native-IDs (the `nativeID` prop on the RN
24
- * `<View>`) that should be blacked-out in the captured image.
25
- *
26
- * Idempotent: a second call replaces the first. Pass `null` (or
27
- * call `clearMaskQuery`) to detach.
28
- */
29
- export function registerMaskQuery(query: MaskQuery): void {
30
- _query = query;
31
- }
32
-
33
- /** Unregister. Mostly for tests / teardown. */
34
- export function clearMaskQuery(): void {
35
- _query = null;
36
- }
37
-
38
- /** Internal — read by `handlers/screenshot.ts` at capture time. */
39
- export function getRegisteredMaskQuery(): MaskQuery | null {
40
- return _query;
41
- }
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
- }