@goliapkg/sentori-react-native 4.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/MIGRATION.md +131 -0
  2. package/README.md +40 -30
  3. package/android/build.gradle +21 -0
  4. package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
  5. package/android/src/main/java/com/sentori/SentoriModule.kt +8 -0
  6. package/android/src/main/java/com/sentori/SentoriScreenshotCapture.kt +36 -4
  7. package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
  8. package/android/src/test/resources/robolectric.properties +10 -0
  9. package/ios/SentoriCrashHandler.swift +48 -4
  10. package/ios/SentoriModule.swift +12 -0
  11. package/ios/SentoriScreenshotCapture.swift +34 -3
  12. package/lib/config.d.ts +17 -47
  13. package/lib/config.d.ts.map +1 -1
  14. package/lib/config.js +3 -0
  15. package/lib/config.js.map +1 -1
  16. package/lib/device.d.ts +5 -0
  17. package/lib/device.d.ts.map +1 -0
  18. package/lib/device.js +29 -0
  19. package/lib/device.js.map +1 -0
  20. package/lib/emit-hooks.d.ts +7 -0
  21. package/lib/emit-hooks.d.ts.map +1 -0
  22. package/lib/emit-hooks.js +24 -0
  23. package/lib/emit-hooks.js.map +1 -0
  24. package/lib/error-boundary.d.ts.map +1 -1
  25. package/lib/error-boundary.js +1 -1
  26. package/lib/error-boundary.js.map +1 -1
  27. package/lib/feature-flags.d.ts.map +1 -1
  28. package/lib/handlers/dev-symbolicate.d.ts +1 -1
  29. package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
  30. package/lib/handlers/dev-symbolicate.js +2 -0
  31. package/lib/handlers/dev-symbolicate.js.map +1 -1
  32. package/lib/handlers/global.js +1 -1
  33. package/lib/handlers/global.js.map +1 -1
  34. package/lib/handlers/lifecycle.js +3 -3
  35. package/lib/handlers/lifecycle.js.map +1 -1
  36. package/lib/handlers/network.d.ts +2 -2
  37. package/lib/handlers/network.d.ts.map +1 -1
  38. package/lib/handlers/network.js +69 -187
  39. package/lib/handlers/network.js.map +1 -1
  40. package/lib/handlers/promise.js +1 -1
  41. package/lib/handlers/promise.js.map +1 -1
  42. package/lib/handlers/screenshot.d.ts.map +1 -1
  43. package/lib/handlers/screenshot.js +3 -12
  44. package/lib/handlers/screenshot.js.map +1 -1
  45. package/lib/index.d.ts +32 -109
  46. package/lib/index.d.ts.map +1 -1
  47. package/lib/index.js +54 -103
  48. package/lib/index.js.map +1 -1
  49. package/lib/init.d.ts +3 -150
  50. package/lib/init.d.ts.map +1 -1
  51. package/lib/init.js +100 -261
  52. package/lib/init.js.map +1 -1
  53. package/lib/long-task-monitor.d.ts +1 -7
  54. package/lib/long-task-monitor.d.ts.map +1 -1
  55. package/lib/long-task-monitor.js +37 -58
  56. package/lib/long-task-monitor.js.map +1 -1
  57. package/lib/mask.d.ts +5 -13
  58. package/lib/mask.d.ts.map +1 -1
  59. package/lib/mask.js +29 -30
  60. package/lib/mask.js.map +1 -1
  61. package/lib/mobile-vitals.d.ts +3 -31
  62. package/lib/mobile-vitals.d.ts.map +1 -1
  63. package/lib/mobile-vitals.js +22 -61
  64. package/lib/mobile-vitals.js.map +1 -1
  65. package/lib/native-pending.d.ts +3 -0
  66. package/lib/native-pending.d.ts.map +1 -0
  67. package/lib/native-pending.js +64 -0
  68. package/lib/native-pending.js.map +1 -0
  69. package/lib/native.d.ts +4 -11
  70. package/lib/native.d.ts.map +1 -1
  71. package/lib/native.js +23 -0
  72. package/lib/native.js.map +1 -1
  73. package/lib/navigation.d.ts +3 -17
  74. package/lib/navigation.d.ts.map +1 -1
  75. package/lib/navigation.js +32 -133
  76. package/lib/navigation.js.map +1 -1
  77. package/lib/netinfo.d.ts +1 -1
  78. package/lib/netinfo.d.ts.map +1 -1
  79. package/lib/netinfo.js +2 -2
  80. package/lib/netinfo.js.map +1 -1
  81. package/lib/push.d.ts.map +1 -1
  82. package/lib/push.js +2 -4
  83. package/lib/push.js.map +1 -1
  84. package/lib/rage-tap.d.ts.map +1 -1
  85. package/lib/rage-tap.js +56 -23
  86. package/lib/rage-tap.js.map +1 -1
  87. package/lib/replay-screens.d.ts +15 -0
  88. package/lib/replay-screens.d.ts.map +1 -0
  89. package/lib/replay-screens.js +87 -0
  90. package/lib/replay-screens.js.map +1 -0
  91. package/lib/replay.d.ts.map +1 -1
  92. package/lib/replay.js +3 -24
  93. package/lib/replay.js.map +1 -1
  94. package/lib/responsiveness-detector.d.ts +18 -0
  95. package/lib/responsiveness-detector.d.ts.map +1 -0
  96. package/lib/responsiveness-detector.js +55 -0
  97. package/lib/responsiveness-detector.js.map +1 -0
  98. package/lib/scope.d.ts +7 -0
  99. package/lib/scope.d.ts.map +1 -0
  100. package/lib/scope.js +41 -0
  101. package/lib/scope.js.map +1 -0
  102. package/lib/transport.d.ts +19 -92
  103. package/lib/transport.d.ts.map +1 -1
  104. package/lib/transport.js +75 -288
  105. package/lib/transport.js.map +1 -1
  106. package/lib/verbs.d.ts +16 -0
  107. package/lib/verbs.d.ts.map +1 -0
  108. package/lib/verbs.js +153 -0
  109. package/lib/verbs.js.map +1 -0
  110. package/package.json +4 -3
  111. package/src/__tests__/iron-rule.test.ts +128 -0
  112. package/src/__tests__/replay-screens.test.ts +63 -0
  113. package/src/__tests__/responsiveness-detector.test.ts +74 -0
  114. package/src/__tests__/screenshot.test.ts +0 -2
  115. package/src/__tests__/transport.test.ts +66 -213
  116. package/src/__tests__/verbs.test.ts +91 -0
  117. package/src/config.ts +20 -47
  118. package/src/device.ts +36 -0
  119. package/src/emit-hooks.ts +30 -0
  120. package/src/error-boundary.tsx +1 -1
  121. package/src/handlers/dev-symbolicate.ts +2 -1
  122. package/src/handlers/global.ts +1 -1
  123. package/src/handlers/lifecycle.ts +3 -3
  124. package/src/handlers/network.ts +95 -211
  125. package/src/handlers/promise.ts +1 -1
  126. package/src/handlers/screenshot.ts +3 -11
  127. package/src/index.ts +84 -215
  128. package/src/init.ts +115 -449
  129. package/src/long-task-monitor.ts +42 -64
  130. package/src/mask.ts +29 -31
  131. package/src/mobile-vitals.ts +22 -82
  132. package/src/native-pending.ts +92 -0
  133. package/src/native.ts +33 -0
  134. package/src/navigation.ts +34 -142
  135. package/src/netinfo.ts +3 -3
  136. package/src/push.ts +2 -5
  137. package/src/rage-tap.tsx +72 -25
  138. package/src/replay-screens.ts +95 -0
  139. package/src/replay.ts +3 -20
  140. package/src/responsiveness-detector.ts +66 -0
  141. package/src/scope.ts +50 -0
  142. package/src/transport.ts +84 -371
  143. package/src/verbs.ts +198 -0
  144. package/lib/breadcrumbs.d.ts +0 -18
  145. package/lib/breadcrumbs.d.ts.map +0 -1
  146. package/lib/breadcrumbs.js +0 -42
  147. package/lib/breadcrumbs.js.map +0 -1
  148. package/lib/bundle-info.d.ts +0 -12
  149. package/lib/bundle-info.d.ts.map +0 -1
  150. package/lib/bundle-info.js +0 -73
  151. package/lib/bundle-info.js.map +0 -1
  152. package/lib/capture.d.ts +0 -140
  153. package/lib/capture.d.ts.map +0 -1
  154. package/lib/capture.js +0 -511
  155. package/lib/capture.js.map +0 -1
  156. package/lib/compat/sentry.d.ts +0 -126
  157. package/lib/compat/sentry.d.ts.map +0 -1
  158. package/lib/compat/sentry.js +0 -332
  159. package/lib/compat/sentry.js.map +0 -1
  160. package/lib/control-channel.d.ts +0 -6
  161. package/lib/control-channel.d.ts.map +0 -1
  162. package/lib/control-channel.js +0 -96
  163. package/lib/control-channel.js.map +0 -1
  164. package/lib/expo-compat.d.ts +0 -270
  165. package/lib/expo-compat.d.ts.map +0 -1
  166. package/lib/expo-compat.js +0 -500
  167. package/lib/expo-compat.js.map +0 -1
  168. package/lib/feedback-widget.d.ts +0 -35
  169. package/lib/feedback-widget.d.ts.map +0 -1
  170. package/lib/feedback-widget.js +0 -193
  171. package/lib/feedback-widget.js.map +0 -1
  172. package/lib/feedback.d.ts +0 -2
  173. package/lib/feedback.d.ts.map +0 -1
  174. package/lib/feedback.js +0 -17
  175. package/lib/feedback.js.map +0 -1
  176. package/lib/heartbeat.d.ts +0 -9
  177. package/lib/heartbeat.d.ts.map +0 -1
  178. package/lib/heartbeat.js +0 -138
  179. package/lib/heartbeat.js.map +0 -1
  180. package/lib/index.js.bak +0 -64
  181. package/lib/install-id.d.ts +0 -17
  182. package/lib/install-id.d.ts.map +0 -1
  183. package/lib/install-id.js +0 -125
  184. package/lib/install-id.js.map +0 -1
  185. package/lib/lifecycle.d.ts +0 -28
  186. package/lib/lifecycle.d.ts.map +0 -1
  187. package/lib/lifecycle.js +0 -73
  188. package/lib/lifecycle.js.map +0 -1
  189. package/lib/measure.d.ts +0 -4
  190. package/lib/measure.d.ts.map +0 -1
  191. package/lib/measure.js +0 -25
  192. package/lib/measure.js.map +0 -1
  193. package/lib/metrics.d.ts +0 -24
  194. package/lib/metrics.d.ts.map +0 -1
  195. package/lib/metrics.js +0 -81
  196. package/lib/metrics.js.map +0 -1
  197. package/lib/pre-crash-sentinel.d.ts +0 -12
  198. package/lib/pre-crash-sentinel.d.ts.map +0 -1
  199. package/lib/pre-crash-sentinel.js +0 -116
  200. package/lib/pre-crash-sentinel.js.map +0 -1
  201. package/lib/report-security.d.ts +0 -40
  202. package/lib/report-security.d.ts.map +0 -1
  203. package/lib/report-security.js +0 -159
  204. package/lib/report-security.js.map +0 -1
  205. package/lib/runtime-metrics-fps.d.ts +0 -19
  206. package/lib/runtime-metrics-fps.d.ts.map +0 -1
  207. package/lib/runtime-metrics-fps.js +0 -128
  208. package/lib/runtime-metrics-fps.js.map +0 -1
  209. package/lib/runtime-metrics-heap.d.ts +0 -13
  210. package/lib/runtime-metrics-heap.d.ts.map +0 -1
  211. package/lib/runtime-metrics-heap.js +0 -67
  212. package/lib/runtime-metrics-heap.js.map +0 -1
  213. package/lib/runtime-metrics-network.d.ts +0 -23
  214. package/lib/runtime-metrics-network.d.ts.map +0 -1
  215. package/lib/runtime-metrics-network.js +0 -99
  216. package/lib/runtime-metrics-network.js.map +0 -1
  217. package/lib/runtime-metrics.d.ts +0 -43
  218. package/lib/runtime-metrics.d.ts.map +0 -1
  219. package/lib/runtime-metrics.js +0 -115
  220. package/lib/runtime-metrics.js.map +0 -1
  221. package/lib/sample-profiler.d.ts +0 -21
  222. package/lib/sample-profiler.d.ts.map +0 -1
  223. package/lib/sample-profiler.js +0 -152
  224. package/lib/sample-profiler.js.map +0 -1
  225. package/lib/sentinel-context.d.ts +0 -4
  226. package/lib/sentinel-context.d.ts.map +0 -1
  227. package/lib/sentinel-context.js +0 -52
  228. package/lib/sentinel-context.js.map +0 -1
  229. package/lib/session-tracker.d.ts +0 -6
  230. package/lib/session-tracker.d.ts.map +0 -1
  231. package/lib/session-tracker.js +0 -50
  232. package/lib/session-tracker.js.map +0 -1
  233. package/lib/stack.d.ts +0 -3
  234. package/lib/stack.d.ts.map +0 -1
  235. package/lib/stack.js +0 -7
  236. package/lib/stack.js.map +0 -1
  237. package/lib/state-snapshots.d.ts +0 -33
  238. package/lib/state-snapshots.d.ts.map +0 -1
  239. package/lib/state-snapshots.js +0 -109
  240. package/lib/state-snapshots.js.map +0 -1
  241. package/lib/track.d.ts +0 -34
  242. package/lib/track.d.ts.map +0 -1
  243. package/lib/track.js +0 -107
  244. package/lib/track.js.map +0 -1
  245. package/lib/trail.d.ts +0 -12
  246. package/lib/trail.d.ts.map +0 -1
  247. package/lib/trail.js +0 -28
  248. package/lib/trail.js.map +0 -1
  249. package/lib/trust-score.d.ts +0 -20
  250. package/lib/trust-score.d.ts.map +0 -1
  251. package/lib/trust-score.js +0 -151
  252. package/lib/trust-score.js.map +0 -1
  253. package/lib/types.d.ts +0 -2
  254. package/lib/types.d.ts.map +0 -1
  255. package/lib/types.js +0 -4
  256. package/lib/types.js.map +0 -1
  257. package/lib/uuid.d.ts +0 -2
  258. package/lib/uuid.d.ts.map +0 -1
  259. package/lib/uuid.js +0 -3
  260. package/lib/uuid.js.map +0 -1
  261. package/src/__tests__/before-send.test.ts +0 -72
  262. package/src/__tests__/breadcrumbs.test.ts +0 -44
  263. package/src/__tests__/capture-replay.test.ts +0 -150
  264. package/src/__tests__/compat-sentry.test.ts +0 -121
  265. package/src/__tests__/control-channel.test.ts +0 -111
  266. package/src/__tests__/install-id.test.ts +0 -60
  267. package/src/__tests__/long-task-monitor.test.ts +0 -59
  268. package/src/__tests__/measure.test.ts +0 -45
  269. package/src/__tests__/navigation.test.ts +0 -175
  270. package/src/__tests__/push.test.ts +0 -238
  271. package/src/__tests__/report-security.test.ts +0 -106
  272. package/src/__tests__/runtime-metrics-instruments.test.ts +0 -109
  273. package/src/__tests__/runtime-metrics-network.test.ts +0 -78
  274. package/src/__tests__/sample-profiler.test.ts +0 -59
  275. package/src/__tests__/stack.test.ts +0 -45
  276. package/src/__tests__/state-snapshots.test.ts +0 -119
  277. package/src/__tests__/tracing.test.ts +0 -201
  278. package/src/__tests__/track.test.ts +0 -91
  279. package/src/__tests__/uuid.test.ts +0 -41
  280. package/src/breadcrumbs.ts +0 -57
  281. package/src/bundle-info.ts +0 -95
  282. package/src/capture.ts +0 -660
  283. package/src/compat/sentry.ts +0 -490
  284. package/src/control-channel.ts +0 -98
  285. package/src/expo-compat.ts +0 -698
  286. package/src/feedback-widget.tsx +0 -316
  287. package/src/feedback.ts +0 -21
  288. package/src/heartbeat.ts +0 -157
  289. package/src/install-id.ts +0 -146
  290. package/src/lifecycle.ts +0 -76
  291. package/src/measure.ts +0 -28
  292. package/src/metrics.ts +0 -94
  293. package/src/pre-crash-sentinel.ts +0 -140
  294. package/src/report-security.ts +0 -165
  295. package/src/runtime-metrics-fps.ts +0 -137
  296. package/src/runtime-metrics-heap.ts +0 -78
  297. package/src/runtime-metrics-network.ts +0 -103
  298. package/src/runtime-metrics.ts +0 -122
  299. package/src/sample-profiler.ts +0 -169
  300. package/src/sentinel-context.ts +0 -60
  301. package/src/session-tracker.ts +0 -53
  302. package/src/stack.ts +0 -10
  303. package/src/state-snapshots.ts +0 -133
  304. package/src/track.ts +0 -123
  305. package/src/trail.ts +0 -32
  306. package/src/trust-score.ts +0 -176
  307. package/src/types.ts +0 -21
  308. package/src/uuid.ts +0 -2
@@ -1,247 +1,100 @@
1
- import {
2
- describe,
3
- it,
4
- expect,
5
- beforeEach,
6
- afterEach,
7
- mock,
8
- } from 'bun:test';
1
+ // Transport on the v1 wire: batch envelope, assert piggyback,
2
+ // offline persistence hooks.
9
3
 
10
- import { clearSpans, startSpan } from '@goliapkg/sentori-core';
4
+ import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
11
5
 
12
- import { setConfig, __resetForTests as resetConfig } from '../config';
6
+ import { __resetForTests as resetConfig, setConfig } from '../config';
13
7
  import {
8
+ __peekAssertStats,
9
+ __peekQueue,
10
+ __resetForTests as resetTransport,
11
+ countAssert,
14
12
  enqueue,
15
13
  flush,
16
- flushSpans,
17
14
  startTransport,
18
- __resetForTests as resetTransport,
19
- __peekQueue,
20
15
  } from '../transport';
21
- import type { Event } from '../types';
22
16
 
23
- const makeEvent = (id: string): Event => ({
24
- id,
25
- timestamp: '2026-05-09T00:00:00.000Z',
26
- kind: 'error',
27
- platform: 'javascript',
28
- release: 'app@1.0.0+1',
17
+ const baseConfig = {
18
+ token: 'st_test',
19
+ ingestUrl: 'http://localhost:18080',
20
+ release: 'app@1.0.0',
29
21
  environment: 'test',
30
- device: { os: 'ios', osVersion: '17.0' },
31
- app: { version: '1.0.0' },
32
- error: {
33
- type: 'TypeError',
34
- message: 'x',
35
- stack: [{ file: 'a.ts', line: 1, inApp: true }],
36
- cause: null,
37
- },
22
+ enabled: true,
23
+ detect: { rageTap: true, longFreeze: true, slowColdStart: true, slowApi: false },
24
+ replaySeconds: 30,
25
+ };
26
+
27
+ const wire = (kind: 'error' | 'trace') => ({
28
+ id: '019e0000-0000-7000-8000-000000000001',
29
+ kind,
30
+ occurredAt: new Date().toISOString(),
31
+ platform: 'javascript' as const,
32
+ payload: {},
38
33
  });
39
34
 
40
35
  const originalFetch = globalThis.fetch;
41
36
 
42
- describe('transport', () => {
37
+ describe('transport (v1 wire)', () => {
43
38
  beforeEach(() => {
44
- resetConfig();
45
39
  resetTransport();
46
- setConfig({
47
- token: 'st_pk_test',
48
- release: 'app@1.0.0+1',
49
- environment: 'test',
50
- ingestUrl: 'http://localhost:8080',
51
- enabled: true,
52
- });
40
+ resetConfig();
41
+ setConfig(baseConfig);
53
42
  startTransport();
54
43
  });
55
-
56
44
  afterEach(() => {
57
45
  globalThis.fetch = originalFetch;
46
+ resetTransport();
47
+ resetConfig();
58
48
  });
59
49
 
60
- it('enqueues without immediate flush below batch size', () => {
61
- enqueue(makeEvent('a'));
62
- expect(__peekQueue()).toHaveLength(1);
63
- });
64
-
65
- it('flush sends to /v1/events for single event', async () => {
66
- const calls: { url: string; init?: RequestInit }[] = [];
67
- globalThis.fetch = mock(
68
- async (url: string | URL | Request, init?: RequestInit) => {
69
- calls.push({ url: String(url), init });
70
- return new Response(null, { status: 202 });
71
- },
72
- ) as typeof fetch;
73
-
74
- enqueue(makeEvent('a'));
75
- await flush();
76
-
77
- expect(calls).toHaveLength(1);
78
- expect(calls[0]?.url).toBe('http://localhost:8080/v1/events');
79
- });
80
-
81
- it('flush sends to /v1/events:batch for multiple events', async () => {
82
- const calls: { url: string; init?: RequestInit }[] = [];
83
- globalThis.fetch = mock(
84
- async (url: string | URL | Request, init?: RequestInit) => {
85
- calls.push({ url: String(url), init });
86
- return new Response(null, { status: 202 });
87
- },
88
- ) as typeof fetch;
89
-
90
- enqueue(makeEvent('a'));
91
- enqueue(makeEvent('b'));
92
- await flush();
93
-
94
- expect(calls).toHaveLength(1);
95
- expect(calls[0]?.url).toBe('http://localhost:8080/v1/events:batch');
96
- const body = JSON.parse((calls[0]?.init?.body as string) ?? '{}');
97
- expect(body.events).toHaveLength(2);
98
- });
99
-
100
- it('attaches Authorization and Sentori-Sdk headers', async () => {
101
- let capturedHeaders: Record<string, string> | undefined;
102
- globalThis.fetch = mock(
103
- async (_url: string | URL | Request, init?: RequestInit) => {
104
- capturedHeaders = init?.headers as Record<string, string>;
105
- return new Response(null, { status: 202 });
106
- },
107
- ) as typeof fetch;
108
-
109
- enqueue(makeEvent('a'));
110
- await flush();
111
-
112
- expect(capturedHeaders?.Authorization).toBe('Bearer st_pk_test');
113
- expect(capturedHeaders?.['Sentori-Sdk']).toMatch(/^react-native\//);
114
- });
115
-
116
- // Phase 33 sub-D: offline / retry behavior.
117
-
118
- it('retries up to MAX_RETRY (3) on a 5xx, then gives up', async () => {
119
- let attempts = 0;
120
- globalThis.fetch = mock(async () => {
121
- attempts++;
122
- return new Response('boom', { status: 503 });
123
- }) as typeof fetch;
124
-
125
- enqueue(makeEvent('a'));
126
- // flush swallows the final throw (and falls through to persist).
127
- // We're verifying the retry count, not the throw.
128
- await flush();
129
- expect(attempts).toBe(3);
130
- });
131
-
132
- it('retries on network error (fetch throw), succeeds when recovered', async () => {
133
- let attempts = 0;
134
- globalThis.fetch = mock(async () => {
135
- attempts++;
136
- if (attempts < 3) throw new TypeError('NetworkError: offline');
137
- return new Response(null, { status: 202 });
138
- }) as typeof fetch;
139
-
140
- enqueue(makeEvent('a'));
141
- await flush();
142
- expect(attempts).toBe(3);
143
- });
144
-
145
- it('drops 4xx-other-than-429 without retry (client errors are unrecoverable)', async () => {
146
- let attempts = 0;
147
- globalThis.fetch = mock(async () => {
148
- attempts++;
149
- return new Response(null, { status: 400 });
50
+ it('posts the batch envelope to /v1/events:batch', async () => {
51
+ let seenUrl = '';
52
+ let seenBody: unknown;
53
+ globalThis.fetch = mock(async (url: unknown, init?: RequestInit) => {
54
+ seenUrl = String(url);
55
+ seenBody = JSON.parse(String(init?.body));
56
+ return new Response(JSON.stringify({ accepted: 1, outcomes: [] }), { status: 200 });
150
57
  }) as typeof fetch;
151
58
 
152
- enqueue(makeEvent('a'));
59
+ enqueue(wire('error'));
153
60
  await flush();
154
- // sendOnce treats 4xx-other-than-429 as a no-throw exit, so the
155
- // retry loop also exits — one attempt, no double-send.
156
- expect(attempts).toBe(1);
61
+ expect(seenUrl).toBe('http://localhost:18080/v1/events:batch');
62
+ expect((seenBody as { events: unknown[] }).events.length).toBe(1);
157
63
  });
158
64
 
159
- it('does not duplicate events when flush is called twice in a row', async () => {
160
- let attempts = 0;
161
- globalThis.fetch = mock(async () => {
162
- attempts++;
163
- return new Response(null, { status: 202 });
65
+ it('piggybacks assert stats on the envelope and clears them', async () => {
66
+ let seenBody: { assertStats?: unknown[] } = {};
67
+ globalThis.fetch = mock(async (_url: unknown, init?: RequestInit) => {
68
+ seenBody = JSON.parse(String(init?.body));
69
+ return new Response(JSON.stringify({ accepted: 0, outcomes: [] }), { status: 200 });
164
70
  }) as typeof fetch;
165
71
 
166
- enqueue(makeEvent('a'));
167
- enqueue(makeEvent('b'));
72
+ countAssert('total-positive', true, 'app@1.0.0');
73
+ countAssert('total-positive', true, 'app@1.0.0');
74
+ countAssert('total-positive', false, 'app@1.0.0');
75
+ expect(__peekAssertStats().length).toBe(1);
168
76
  await flush();
169
- await flush(); // second flush sees an empty queue and no-ops
170
- expect(attempts).toBe(1);
171
- expect(__peekQueue()).toHaveLength(0);
172
- });
173
- });
174
-
175
- describe('span flush', () => {
176
- beforeEach(() => {
177
- resetConfig();
77
+ const stats = seenBody.assertStats as Array<{
78
+ name: string;
79
+ passDelta: number;
80
+ failDelta: number;
81
+ }>;
82
+ expect(stats.length).toBe(1);
83
+ expect(stats[0]?.passDelta).toBe(2);
84
+ expect(stats[0]?.failDelta).toBe(1);
85
+ expect(__peekAssertStats().length).toBe(0);
86
+ });
87
+
88
+ it('never flushes before startTransport', async () => {
178
89
  resetTransport();
179
- clearSpans();
180
- setConfig({
181
- token: 'st_pk_test',
182
- release: 'app@1.0.0+1',
183
- environment: 'test',
184
- ingestUrl: 'http://localhost:8080',
185
- enabled: true,
186
- });
187
- startTransport();
188
- });
189
- afterEach(() => {
190
- globalThis.fetch = originalFetch;
191
- clearSpans();
192
- });
193
-
194
- it('POSTs buffered spans to /v1/spans:batch', async () => {
195
- const calls: { url: string; init?: RequestInit }[] = [];
196
- globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
197
- calls.push({ url: String(url), init });
198
- return new Response(null, { status: 202 });
199
- }) as typeof fetch;
200
-
201
- startSpan('http.client', { name: 'GET /a' }).finish({ status: 'ok' });
202
- startSpan('http.client', { name: 'GET /b' }).finish({ status: 'error' });
203
- await flushSpans();
204
-
205
- expect(calls).toHaveLength(1);
206
- expect(calls[0]?.url).toBe('http://localhost:8080/v1/spans:batch');
207
- const body = JSON.parse((calls[0]?.init?.body as string) ?? '{}');
208
- expect(body.spans).toHaveLength(2);
209
- expect(body.spans[0]).toMatchObject({ op: 'http.client', name: 'GET /a', status: 'ok' });
210
- });
211
-
212
- it('no-op when the span buffer is empty', async () => {
213
- let attempts = 0;
90
+ let called = 0;
214
91
  globalThis.fetch = mock(async () => {
215
- attempts++;
216
- return new Response(null, { status: 202 });
217
- }) as typeof fetch;
218
- await flushSpans();
219
- expect(attempts).toBe(0);
220
- });
221
-
222
- it('splits >200 spans into multiple batches', async () => {
223
- const sizes: number[] = [];
224
- globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
225
- const body = JSON.parse((init?.body as string) ?? '{}');
226
- sizes.push(body.spans.length);
227
- return new Response(null, { status: 202 });
92
+ called += 1;
93
+ return new Response('{}', { status: 200 });
228
94
  }) as typeof fetch;
229
-
230
- for (let i = 0; i < 450; i++) startSpan('http.client', { name: `GET /${i}` }).finish();
231
- await flushSpans();
232
-
233
- expect(sizes).toEqual([200, 200, 50]);
234
- });
235
-
236
- it('drops on 5xx without retrying', async () => {
237
- let attempts = 0;
238
- globalThis.fetch = mock(async () => {
239
- attempts++;
240
- return new Response('boom', { status: 503 });
241
- }) as typeof fetch;
242
-
243
- startSpan('http.client', { name: 'GET /a' }).finish();
244
- await flushSpans();
245
- expect(attempts).toBe(1);
95
+ enqueue(wire('trace'));
96
+ await flush();
97
+ expect(called).toBe(0);
98
+ expect(__peekQueue().length).toBe(1);
246
99
  });
247
100
  });
@@ -0,0 +1,91 @@
1
+ // The five verbs: synchronous, never throw, id out, correct wire.
2
+
3
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
4
+
5
+ import { clearSignals } from '@goliapkg/sentori-core';
6
+
7
+ import { __resetForTests as resetConfig, setConfig } from '../config';
8
+ import { __resetForTests as resetScope } from '../scope';
9
+ import {
10
+ __peekAssertStats,
11
+ __peekQueue,
12
+ __resetForTests as resetTransport,
13
+ startTransport,
14
+ } from '../transport';
15
+ import { verbs } from '../verbs';
16
+
17
+ const baseConfig = {
18
+ token: 'st_test',
19
+ ingestUrl: 'http://localhost:18080',
20
+ release: 'app@1.0.0',
21
+ environment: 'test',
22
+ enabled: true,
23
+ detect: { rageTap: true, longFreeze: true, slowColdStart: true, slowApi: false },
24
+ replaySeconds: 30,
25
+ };
26
+
27
+ describe('the five verbs', () => {
28
+ beforeEach(() => {
29
+ resetTransport();
30
+ resetConfig();
31
+ resetScope();
32
+ clearSignals();
33
+ setConfig(baseConfig);
34
+ startTransport();
35
+ });
36
+ afterEach(() => {
37
+ resetTransport();
38
+ resetConfig();
39
+ resetScope();
40
+ clearSignals();
41
+ });
42
+
43
+ it('error coerces anything and enqueues with a stack', () => {
44
+ const id = verbs.error(new Error('boom'));
45
+ expect(typeof id).toBe('string');
46
+ const q = __peekQueue();
47
+ expect(q.length).toBe(1);
48
+ expect(q[0]?.kind).toBe('error');
49
+ expect(q[0]?.payload.error?.type).toBe('Error');
50
+ expect(q[0]?.payload.error?.message).toBe('boom');
51
+ });
52
+
53
+ it('error serializes Error instances found in data (error-in-data)', () => {
54
+ verbs.warn('pay.retry', { attempt: 2, error: new TypeError('nope') });
55
+ const q = __peekQueue();
56
+ const data = q[0]?.payload.data as { error?: { type?: string } };
57
+ expect(data.error?.type).toBe('TypeError');
58
+ });
59
+
60
+ it('warn carries name; trace quiet stays out of the queue', () => {
61
+ verbs.warn('rage_tap');
62
+ expect(__peekQueue().length).toBe(1);
63
+ verbs.trace('checkout.start', undefined, { quiet: true });
64
+ expect(__peekQueue().length).toBe(1); // unchanged
65
+ verbs.trace('checkout.start');
66
+ expect(__peekQueue().length).toBe(2);
67
+ });
68
+
69
+ it('assert pass aggregates without an event; fail is an event', () => {
70
+ verbs.assert('total-positive', true);
71
+ expect(__peekQueue().length).toBe(0);
72
+ expect(__peekAssertStats()[0]?.passDelta).toBe(1);
73
+ verbs.assert('total-positive', false);
74
+ expect(__peekQueue().length).toBe(1);
75
+ expect(__peekQueue()[0]?.kind).toBe('assert');
76
+ });
77
+
78
+ it('probe never throws and reports the ref as name', () => {
79
+ const id = verbs.probe('SENT-123');
80
+ expect(typeof id).toBe('string');
81
+ expect(__peekQueue()[0]?.kind).toBe('probe');
82
+ expect(__peekQueue()[0]?.name).toBe('SENT-123');
83
+ });
84
+
85
+ it('before init everything is a silent no-op that still returns an id', () => {
86
+ resetConfig();
87
+ const id = verbs.error(new Error('x'));
88
+ expect(typeof id).toBe('string');
89
+ expect(__peekQueue().length).toBe(0);
90
+ });
91
+ });
package/src/config.ts CHANGED
@@ -1,58 +1,31 @@
1
- import type { BeforeSendHook, LogLevel, ReadyInfo } from '@goliapkg/sentori-core';
1
+ // SDK-internal runtime config, set once by init(). Everything else
2
+ // reads through getConfig() and treats null as "not initialized →
3
+ // every verb is a no-op" (the failure-isolation iron rule).
2
4
 
3
- /**
4
- * v2.3 — `ReadyInfo` is shared across SDKs via `@goliapkg/sentori-core`
5
- * so a host that switches from web to RN reads the same shape. The RN
6
- * SDK always populates `native` + `coldStartMs`; the core type marks
7
- * both optional for the web SDK's benefit (web has no native module).
8
- */
9
- export type { ReadyInfo };
5
+ import type { LogLevel, WireEvent } from '@goliapkg/sentori-core';
10
6
 
11
7
  export type Config = {
12
8
  token: string;
9
+ ingestUrl: string;
13
10
  release: string;
14
11
  environment: string;
15
- ingestUrl: string;
16
12
  enabled: boolean;
17
- /** Phase 42 sub-D.07: opt-in screenshot capture on captureException. */
18
- screenshotsEnabled: boolean;
19
- /** Phase 44 sub-B: per-event-class sampling rates 0..1.
20
- * `null` = keep everything (default). */
21
- errorSampleRate: null | number;
22
- traceSampleRate: null | number;
23
- /** v2.0 — sampling rate for `kind: 'message'` events emitted via
24
- * `captureMessage`. `null` = keep all (default). */
25
- messageSampleRate: null | number;
26
- /** Phase 46: when true, every `captureException` seals the
27
- * session-trail buffer and uploads it as a `sessionTrail`
28
- * attachment. Defaults to false. */
29
- sessionTrailEnabled: boolean;
30
- /** v2.0 W3 — when true, every `track(name, props)` call also
31
- * pushes a `{ type: 'track', data: { name, props } }` breadcrumb
32
- * so a subsequent capture carries the customer journey. Defaults
33
- * to false to preserve v1 customer breadcrumb shape on upgrade. */
34
- trackAutoBreadcrumb: boolean;
35
- /** v2.3 — Sentori console output gate.
36
- *
37
- * Default `warn`: SDK is silent on host's console unless
38
- * something is genuinely broken (transport sustained failure,
39
- * native module not found, internal SDK exception). No
40
- * per-tick / per-init / per-breadcrumb noise.
41
- *
42
- * Set `'silent'` for absolute silence (e.g. CI smoke runs);
43
- * set `'info'` or `'debug'` when debugging Sentori itself. */
13
+ /** Warn-scenario auto-detection switches (conservative defaults). */
14
+ detect: {
15
+ rageTap: boolean;
16
+ longFreeze: boolean;
17
+ slowColdStart: boolean;
18
+ slowApi: boolean;
19
+ };
20
+ /** B-type replay rolling buffer, seconds. 0 disables. */
21
+ replaySeconds: number;
22
+ replayScreens: boolean;
23
+ /** Sentori console output gate. Default `warn`: silent on the
24
+ * host's console unless something is genuinely broken. */
44
25
  logLevel?: LogLevel;
45
- /** v2.3 — fires once after init completes. Use this to know the
46
- * SDK is live instead of scanning the console. `info` carries
47
- * the native-module bind status + cold-start timing. Host
48
- * wraps any host-side logging here. */
49
- onReady?: (info: ReadyInfo) => void;
50
- /** v2.3 — host-side mutate-or-drop hook called once per event
51
- * just before transport enqueue. Return the event to send it,
52
- * `null` to drop. Sync only. If the hook throws or returns a
53
- * non-event, SDK falls back to the un-mutated event and emits
54
- * one one-shot warn. */
55
- beforeSend?: BeforeSendHook;
26
+ /** Host-side mutate-or-drop hook, sync only. Throw / bad return →
27
+ * the un-mutated event ships and one warn is emitted. */
28
+ beforeSend?: (event: WireEvent) => WireEvent | null;
56
29
  };
57
30
 
58
31
  let _config: Config | null = null;
package/src/device.ts ADDED
@@ -0,0 +1,36 @@
1
+ // Synchronous device snapshot — what the JS layer can know without
2
+ // crossing the bridge. The native module enriches asynchronously via
3
+ // setNativeDeviceInfo at init; until then the JS-visible subset
4
+ // ships (an event with a thin device beats a delayed event).
5
+
6
+ import type { Device } from '@goliapkg/sentori-core';
7
+
8
+ let _native: Partial<Device> | null = null;
9
+
10
+ export const setNativeDeviceInfo = (info: Partial<Device>): void => {
11
+ _native = info;
12
+ };
13
+
14
+ export const collectDevice = (): Device | undefined => {
15
+ let base: Device | undefined;
16
+ try {
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const RN = require('react-native') as {
19
+ Platform?: { OS?: string; Version?: number | string };
20
+ Dimensions?: { get(dim: string): { width: number; height: number; scale: number } };
21
+ };
22
+ const os = RN.Platform?.OS ?? 'unknown';
23
+ base = { os, osVersion: RN.Platform?.Version?.toString() };
24
+ const win = RN.Dimensions?.get('window');
25
+ if (win) {
26
+ base.screen = { width: win.width, height: win.height, scale: win.scale };
27
+ }
28
+ } catch {
29
+ return _native ? ({ os: 'unknown', ..._native } as Device) : undefined;
30
+ }
31
+ return _native ? { ...base, ..._native } : base;
32
+ };
33
+
34
+ export const __resetForTests = (): void => {
35
+ _native = null;
36
+ };
@@ -0,0 +1,30 @@
1
+ // Post-emit hooks — how B-type replay (and future reactions) learn
2
+ // "an event just went out" without the verbs knowing about them.
3
+ //
4
+ // Hooks run inside their own try/catch: a broken subscriber must
5
+ // never take the emit path down with it (failure isolation).
6
+
7
+ import type { WireEvent } from '@goliapkg/sentori-core';
8
+ import { reportInternal } from '@goliapkg/sentori-core';
9
+
10
+ type EmitHook = (event: WireEvent) => void;
11
+
12
+ const hooks: EmitHook[] = [];
13
+
14
+ export const registerEmitHook = (hook: EmitHook): void => {
15
+ hooks.push(hook);
16
+ };
17
+
18
+ export const onEventEmitted = (event: WireEvent): void => {
19
+ for (const hook of hooks) {
20
+ try {
21
+ hook(event);
22
+ } catch (e) {
23
+ reportInternal('emit-hook', e);
24
+ }
25
+ }
26
+ };
27
+
28
+ export const __resetForTests = (): void => {
29
+ hooks.length = 0;
30
+ };
@@ -1,6 +1,6 @@
1
1
  import React, { Component, type ErrorInfo, type ReactNode } from 'react';
2
2
 
3
- import { captureError } from './capture';
3
+ import { error as captureError } from "./verbs";
4
4
 
5
5
  export type ErrorBoundaryProps = {
6
6
  fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
@@ -7,7 +7,7 @@
7
7
  // Best-effort and dev-only: any failure (Metro not running, timeout,
8
8
  // bad response) leaves the stack untouched. Never throws.
9
9
 
10
- import type { Frame, SentoriError } from '../types';
10
+ import type { Frame, SentoriError } from '@goliapkg/sentori-core';
11
11
 
12
12
  const TIMEOUT_MS = 2000;
13
13
 
@@ -132,6 +132,7 @@ export async function symbolicateErrorViaMetro(
132
132
  err: SentoriError,
133
133
  opts: { url?: string } = {},
134
134
  ): Promise<void> {
135
+ if (!err.stack) return;
135
136
  const sym = await symbolicateStackViaMetro(err.stack, opts);
136
137
  if (sym) err.stack = sym;
137
138
  if (err.cause) await symbolicateErrorViaMetro(err.cause, opts);
@@ -1,4 +1,4 @@
1
- import { captureError } from '../capture';
1
+ import { error as captureError } from '../verbs';
2
2
 
3
3
  type ErrorUtilsHandler = (error: Error, isFatal?: boolean) => void;
4
4
 
@@ -12,7 +12,7 @@
12
12
  * twice". Sentry historically did the opposite (treat inactive as
13
13
  * still alive), but that lets a swiped-away session never end.
14
14
  */
15
- import { endSession, startSession } from '../session-tracker';
15
+ import { pushSignal } from '@goliapkg/sentori-core';
16
16
 
17
17
  let _installed = false;
18
18
  let _subscription: { remove: () => void } | null = null;
@@ -40,9 +40,9 @@ export const installLifecycleHandler = (): void => {
40
40
 
41
41
  _subscription = AppState.addEventListener('change', (state) => {
42
42
  if (state === 'active') {
43
- startSession();
43
+ pushSignal('lifecycle', { state: 'active' });
44
44
  } else if (state === 'background' || state === 'inactive') {
45
- endSession();
45
+ pushSignal('lifecycle', { state: 'background' });
46
46
  }
47
47
  });
48
48
  };