@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/lib/verbs.js ADDED
@@ -0,0 +1,153 @@
1
+ // The five event verbs (design.md §4). Everything here is
2
+ // synchronous, never throws, and returns the client-minted event id
3
+ // — the zero-cost iron rule made code.
4
+ //
5
+ // sentori.error(err) 出了什么事?
6
+ // sentori.warn(name) 用户哪里不舒服?
7
+ // sentori.trace(name) 这里发生了什么?
8
+ // sentori.assert(n, ok) 这里应该成立吗?
9
+ // sentori.probe(ref) 那个 bug 回来了吗?
10
+ import { coerceError, parseStack, pushSignal, safeFn, snapshotSignals, uuidV7, } from '@goliapkg/sentori-core';
11
+ import { getConfig } from './config';
12
+ import { collectDevice } from './device';
13
+ import { onEventEmitted } from './emit-hooks';
14
+ import { currentContext, currentUserKey } from './scope';
15
+ import { countAssert, enqueue } from './transport';
16
+ /** Serialize any Error instances found in the data argument — the
17
+ * error-in-data convention (design.md §4): a caught-but-noteworthy
18
+ * exception needs no special API. One level deep is enough; nested
19
+ * containers of errors are an anti-pattern we don't reward. */
20
+ const serializeData = (data) => {
21
+ if (!data)
22
+ return undefined;
23
+ const out = {};
24
+ for (const [k, v] of Object.entries(data)) {
25
+ out[k] = v instanceof Error ? toSentoriError(v) : v;
26
+ }
27
+ return out;
28
+ };
29
+ const toSentoriError = (e) => ({
30
+ type: e.name || 'Error',
31
+ message: e.message,
32
+ stack: parseStack(e.stack),
33
+ cause: e.cause instanceof Error
34
+ ? toSentoriError(e.cause)
35
+ : null,
36
+ });
37
+ const platformOf = () => {
38
+ try {
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const RN = require('react-native');
41
+ const os = RN.Platform?.OS;
42
+ if (os === 'ios')
43
+ return 'ios';
44
+ if (os === 'android')
45
+ return 'android';
46
+ }
47
+ catch {
48
+ // headless / test environment
49
+ }
50
+ return 'javascript';
51
+ };
52
+ /** Assemble + enqueue one event. The single funnel every verb uses. */
53
+ const emit = (kind, opts) => {
54
+ const id = uuidV7();
55
+ const config = getConfig();
56
+ if (!config || !config.enabled)
57
+ return id; // no-op before init — iron rule
58
+ const payload = {};
59
+ if (opts.error)
60
+ payload.error = opts.error;
61
+ const data = serializeData(opts.data);
62
+ if (data)
63
+ payload.data = data;
64
+ const ctx = currentContext();
65
+ if (ctx)
66
+ payload.context = ctx;
67
+ if (opts.withSignals) {
68
+ const signals = snapshotSignals();
69
+ if (signals.length > 0)
70
+ payload.signals = signals;
71
+ }
72
+ const device = collectDevice();
73
+ if (device)
74
+ payload.device = device;
75
+ let event = {
76
+ id,
77
+ kind,
78
+ occurredAt: new Date().toISOString(),
79
+ platform: platformOf(),
80
+ release: config.release,
81
+ environment: config.environment,
82
+ name: opts.name,
83
+ surface: opts.surface,
84
+ userKey: currentUserKey(),
85
+ payload,
86
+ };
87
+ if (config.beforeSend) {
88
+ try {
89
+ const out = config.beforeSend(event);
90
+ if (out === null)
91
+ return id; // deliberate drop
92
+ if (out && typeof out === 'object')
93
+ event = out;
94
+ }
95
+ catch {
96
+ // Hook broke: the un-mutated event ships. The host's bug must
97
+ // not cost them the crash report.
98
+ }
99
+ }
100
+ enqueue(event);
101
+ onEventEmitted(event);
102
+ return id;
103
+ };
104
+ // ── the verbs ──────────────────────────────────────────────────────
105
+ export const error = safeFn('error', (err, data) => {
106
+ const coerced = coerceError(err);
107
+ return emit('error', {
108
+ error: toSentoriError(coerced),
109
+ data,
110
+ withSignals: true,
111
+ });
112
+ });
113
+ export const warn = safeFn('warn', (name, data) => {
114
+ // A hand-written warn's surface can ride in data.surface; the
115
+ // detected scenarios pass it explicitly via warnDetected.
116
+ const surface = data && typeof data.surface === 'object'
117
+ ? data.surface
118
+ : undefined;
119
+ return emit('warn', { name, surface, data, withSignals: true });
120
+ });
121
+ /** SDK-internal: a detected warn scenario with an explicit surface. */
122
+ export const warnDetected = (scenario, surface, data) => emit('warn', { name: scenario, surface, data, withSignals: true });
123
+ export const trace = safeFn('trace', (name, data, opts) => {
124
+ // Every trace is context: it lands in the ring regardless.
125
+ pushSignal('trace', { name, ...(data ?? {}) });
126
+ if (opts?.quiet)
127
+ return uuidV7();
128
+ return emit('trace', { name, data });
129
+ });
130
+ export const assert = safeFn('assert', (name, ok, data) => {
131
+ const config = getConfig();
132
+ if (config)
133
+ countAssert(name, ok, config.release);
134
+ if (ok)
135
+ return uuidV7(); // passes aggregate; they are never events
136
+ return emit('assert', { name, data, withSignals: true });
137
+ });
138
+ export const probe = safeFn('probe', (ref, data) => {
139
+ // A tripwire: reaching this call IS the signal. Never throws,
140
+ // never changes control flow (design.md §4).
141
+ return emit('probe', { name: ref, data });
142
+ });
143
+ // safeFn returns `R | undefined`; the public surface promises a
144
+ // string. An internal failure mints an id that simply never ships —
145
+ // honest enough, and the host never sees a throw.
146
+ export const verbs = {
147
+ error: (err, data) => error(err, data) ?? uuidV7(),
148
+ warn: (name, data) => warn(name, data) ?? uuidV7(),
149
+ trace: (name, data, opts) => trace(name, data, opts) ?? uuidV7(),
150
+ assert: (name, ok, data) => assert(name, ok, data) ?? uuidV7(),
151
+ probe: (ref, data) => probe(ref, data) ?? uuidV7(),
152
+ };
153
+ //# sourceMappingURL=verbs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verbs.js","sourceRoot":"","sources":["../src/verbs.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react-native",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Sentori SDK for React Native — JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "author": "GOLIA K.K. <takagi@golia.jp> (https://golia.jp)",
@@ -90,12 +90,12 @@
90
90
  "devDependencies": {
91
91
  "@types/bun": "latest",
92
92
  "@types/react": "^19",
93
- "typescript": "^6"
93
+ "typescript": "^7"
94
94
  },
95
95
  "publishConfig": {
96
96
  "access": "public"
97
97
  },
98
98
  "dependencies": {
99
- "@goliapkg/sentori-core": "^1.4.0"
99
+ "@goliapkg/sentori-core": "^2.0.0"
100
100
  }
101
101
  }
@@ -0,0 +1,128 @@
1
+ // The zero-cost iron rule, as executable gates (design.md §6).
2
+ //
3
+ // Dimension 3 (failure isolation): no Sentori failure may ever
4
+ // throw into, block, or alter the host app. These tests inject the
5
+ // failures; a single uncaught throw fails the suite.
6
+
7
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
8
+
9
+ import { clearSignals } from '@goliapkg/sentori-core';
10
+
11
+ import { __resetForTests as resetConfig, setConfig } from '../config';
12
+ import { init, __resetForTests as resetInit } from '../init';
13
+ import { __resetForTests as resetScope, patchContext, setUser } from '../scope';
14
+ import {
15
+ __resetForTests as resetTransport,
16
+ flush,
17
+ startTransport,
18
+ } from '../transport';
19
+ import { verbs } from '../verbs';
20
+
21
+ const baseConfig = {
22
+ token: 'st_test',
23
+ ingestUrl: 'http://localhost:18080',
24
+ release: 'app@1.0.0',
25
+ environment: 'test',
26
+ enabled: true,
27
+ detect: { rageTap: true, longFreeze: true, slowColdStart: true, slowApi: false },
28
+ replaySeconds: 30,
29
+ };
30
+
31
+ const originalFetch = globalThis.fetch;
32
+
33
+ const resetAll = () => {
34
+ resetTransport();
35
+ resetConfig();
36
+ resetScope();
37
+ resetInit();
38
+ clearSignals();
39
+ };
40
+
41
+ describe('iron rule: failure isolation', () => {
42
+ beforeEach(() => {
43
+ resetAll();
44
+ setConfig(baseConfig);
45
+ startTransport();
46
+ });
47
+ afterEach(() => {
48
+ globalThis.fetch = originalFetch;
49
+ resetAll();
50
+ });
51
+
52
+ it('server 500 never reaches the caller', async () => {
53
+ globalThis.fetch = (async () => new Response('boom', { status: 500 })) as typeof fetch;
54
+ verbs.error(new Error('x'));
55
+ await flush(); // retries internally, persists, never rejects
56
+ expect(true).toBe(true);
57
+ });
58
+
59
+ it('network down never reaches the caller', async () => {
60
+ globalThis.fetch = (async () => {
61
+ throw new TypeError('Network request failed');
62
+ }) as typeof fetch;
63
+ verbs.warn('rage_tap');
64
+ await flush();
65
+ expect(true).toBe(true);
66
+ });
67
+
68
+ it('a hostile beforeSend cannot break the verb', () => {
69
+ setConfig({
70
+ ...baseConfig,
71
+ beforeSend: () => {
72
+ throw new Error('host bug');
73
+ },
74
+ });
75
+ const id = verbs.error(new Error('x'));
76
+ expect(typeof id).toBe('string');
77
+ });
78
+
79
+ it('fuzz: the verbs accept garbage without throwing', () => {
80
+ const circular: Record<string, unknown> = {};
81
+ circular.self = circular;
82
+ const huge = 'x'.repeat(1_000_000);
83
+
84
+ expect(typeof verbs.error(null)).toBe('string');
85
+ expect(typeof verbs.error(undefined)).toBe('string');
86
+ expect(typeof verbs.error(42)).toBe('string');
87
+ expect(typeof verbs.error(circular)).toBe('string');
88
+ expect(typeof verbs.warn('', circular)).toBe('string');
89
+ expect(typeof verbs.warn(huge)).toBe('string');
90
+ expect(typeof verbs.trace('t', { huge })).toBe('string');
91
+ expect(typeof verbs.assert('a', false, circular)).toBe('string');
92
+ expect(typeof verbs.probe('p', { circular })).toBe('string');
93
+ setUser({ id: huge });
94
+ setUser(null);
95
+ patchContext(circular);
96
+ });
97
+
98
+ it('every verb before init is a silent no-op returning an id', () => {
99
+ resetAll();
100
+ expect(typeof verbs.error(new Error('x'))).toBe('string');
101
+ expect(typeof verbs.warn('w')).toBe('string');
102
+ expect(typeof verbs.trace('t')).toBe('string');
103
+ expect(typeof verbs.assert('a', false)).toBe('string');
104
+ expect(typeof verbs.probe('p')).toBe('string');
105
+ });
106
+
107
+ it('init with a broken config degrades to no-op, never throws', () => {
108
+ resetAll();
109
+ init(undefined as never);
110
+ init({} as never);
111
+ init({ token: '', ingestUrl: '' } as never);
112
+ expect(typeof verbs.error(new Error('x'))).toBe('string');
113
+ });
114
+ });
115
+
116
+ describe('iron rule: init timing', () => {
117
+ beforeEach(resetAll);
118
+ afterEach(resetAll);
119
+
120
+ it('init completes inside the 50 ms budget', () => {
121
+ const start = performance.now();
122
+ init(baseConfig);
123
+ const ms = performance.now() - start;
124
+ // The budget is for the synchronous path; native drains and the
125
+ // offline queue are fire-and-forget behind it.
126
+ expect(ms).toBeLessThan(50);
127
+ });
128
+ });
@@ -59,8 +59,6 @@ describe('uploadAttachment', () => {
59
59
  expect(seen.auth).toBe('Bearer st_pk_test');
60
60
  expect(out).not.toBeNull();
61
61
  expect(out!.ref).toBe('019e3000-7000-7000-8000-000000000001');
62
- expect(out!.kind).toBe('screenshot');
63
- expect(out!.source).toBe('js');
64
62
  });
65
63
 
66
64
  test('returns null on a non-201 response', async () => {
@@ -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
  });