@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/transport.ts CHANGED
@@ -1,9 +1,17 @@
1
- import { addBreadcrumb, drainSpans, logger } from '@goliapkg/sentori-core';
1
+ // Event transport — the only place the SDK talks to the network.
2
+ //
3
+ // Quiet by default, complete when it matters (design.md §4): events
4
+ // batch on a 5 s timer or a 10-deep queue, whichever first; assert
5
+ // pass-counts piggyback on whatever batch goes out next (never their
6
+ // own request); failures back off and finally persist to an offline
7
+ // queue drained on next launch. Nothing here ever throws into the
8
+ // host app.
9
+
10
+ import type { AssertStat, BatchEnvelope, WireEvent } from '@goliapkg/sentori-core';
11
+ import { logger } from '@goliapkg/sentori-core';
2
12
 
3
13
  import { getConfig } from './config';
4
- import { isLiveMode } from './control-channel';
5
14
  import { isAnyNativeModuleLinked } from './native-loader';
6
- import type { Event } from './types';
7
15
 
8
16
  const FLUSH_INTERVAL_MS = 5_000;
9
17
  const BATCH_SIZE = 10;
@@ -11,28 +19,15 @@ const MAX_RETRY = 3;
11
19
  const STORAGE_KEY = '@sentori/pending';
12
20
  const MAX_PERSISTED = 1000;
13
21
 
14
- // Spans are higher-volume and lower-value than error events: we batch
15
- // them on their own timer, cap each request at the server's per-batch
16
- // limit, and drop on failure rather than persisting offline.
17
- const SPAN_FLUSH_INTERVAL_MS = 10_000;
18
- const SPAN_BATCH_MAX = 200;
22
+ const SDK_VERSION = '5.0.0';
19
23
 
20
- let _queue: Event[] = [];
24
+ let _queue: WireEvent[] = [];
25
+ let _assertStats = new Map<string, AssertStat>();
21
26
  let _flushTimer: ReturnType<typeof setTimeout> | null = null;
22
- let _spanTimer: ReturnType<typeof setInterval> | null = null;
23
27
  let _started = false;
24
28
 
25
- const SDK_VERSION = '0.0.0';
26
-
27
- export const enqueue = (event: Event): void => {
29
+ export const enqueue = (event: WireEvent): void => {
28
30
  _queue.push(event);
29
- // v1.1 +S7 升级 — when the dashboard has armed live-debug for the
30
- // current user, flush immediately instead of waiting for the 5 s
31
- // batch interval. Dashboard sees each event with sub-second latency.
32
- if (isLiveMode()) {
33
- void flush();
34
- return;
35
- }
36
31
  if (_queue.length >= BATCH_SIZE) {
37
32
  void flush();
38
33
  } else if (!_flushTimer) {
@@ -43,73 +38,58 @@ export const enqueue = (event: Event): void => {
43
38
  }
44
39
  };
45
40
 
46
- export const startTransport = (): void => {
47
- _started = true;
48
- if (!_spanTimer) {
49
- _spanTimer = setInterval(() => {
50
- void flushSpans();
51
- }, SPAN_FLUSH_INTERVAL_MS);
52
- }
53
- };
54
-
55
- export const flushSpans = async (): Promise<void> => {
56
- if (!_started) return;
57
- const spans = drainSpans();
58
- if (spans.length === 0) return;
59
- const config = getConfig();
60
- if (!config) return;
61
- for (let i = 0; i < spans.length; i += SPAN_BATCH_MAX) {
62
- const chunk = spans.slice(i, i + SPAN_BATCH_MAX);
63
- try {
64
- await sendSpansOnce(chunk, config.ingestUrl, config.token);
65
- } catch {
66
- // drop the remaining chunks — span uploads aren't worth retrying
67
- break;
68
- }
41
+ /**
42
+ * Count an assert outcome. Pass-counts NEVER become events — they
43
+ * aggregate here and ride the next batch envelope (the liveness
44
+ * ledger without a heartbeat flood).
45
+ */
46
+ export const countAssert = (name: string, ok: boolean, release: string): void => {
47
+ const key = `${name}${release}`;
48
+ const cur = _assertStats.get(key) ?? { name, release, passDelta: 0, failDelta: 0 };
49
+ if (ok) cur.passDelta += 1;
50
+ else cur.failDelta = (cur.failDelta ?? 0) + 1;
51
+ _assertStats.set(key, cur);
52
+ // Stats with no event traffic still ship eventually, on a lazy
53
+ // timer six times the batch interval.
54
+ if (!_flushTimer && _queue.length === 0) {
55
+ _flushTimer = setTimeout(() => {
56
+ _flushTimer = null;
57
+ void flush();
58
+ }, FLUSH_INTERVAL_MS * 6);
69
59
  }
70
60
  };
71
61
 
72
- const sendSpansOnce = async (
73
- spans: unknown[],
74
- ingestUrl: string,
75
- token: string,
76
- ): Promise<void> => {
77
- const resp = await fetch(`${ingestUrl}/v1/spans:batch`, {
78
- method: 'POST',
79
- headers: {
80
- 'Content-Type': 'application/json',
81
- Authorization: `Bearer ${token}`,
82
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
83
- },
84
- body: JSON.stringify({ spans }),
85
- });
86
- // 5xx: server overloaded. 4xx (bad token / quota / oversized): also
87
- // pointless to keep sending. Either way, stop the rest of the batch.
88
- if (resp.status >= 400) throw new Error(`spans-${resp.status}`);
62
+ export const startTransport = (): void => {
63
+ _started = true;
89
64
  };
90
65
 
91
66
  export const flush = async (): Promise<void> => {
92
67
  if (!_started) return;
93
- if (_queue.length === 0) return;
94
-
95
68
  const config = getConfig();
96
69
  if (!config) return;
97
70
 
98
- const batch = _queue.splice(0, _queue.length);
71
+ const events = _queue.splice(0, _queue.length);
72
+ const stats = [..._assertStats.values()];
73
+ _assertStats = new Map();
99
74
  if (_flushTimer) {
100
75
  clearTimeout(_flushTimer);
101
76
  _flushTimer = null;
102
77
  }
78
+ if (events.length === 0 && stats.length === 0) return;
79
+
80
+ const envelope: BatchEnvelope = { events };
81
+ if (stats.length > 0) envelope.assertStats = stats;
103
82
 
104
83
  try {
105
- await sendWithRetry(batch, config.ingestUrl, config.token);
84
+ await sendWithRetry(envelope, config.ingestUrl, config.token);
106
85
  } catch {
107
- await persist(batch);
86
+ // Events survive offline; assert deltas are cheap enough to lose.
87
+ await persist(events);
108
88
  }
109
89
  };
110
90
 
111
91
  const sendWithRetry = async (
112
- events: Event[],
92
+ envelope: BatchEnvelope,
113
93
  ingestUrl: string,
114
94
  token: string,
115
95
  ): Promise<void> => {
@@ -117,7 +97,7 @@ const sendWithRetry = async (
117
97
  let delayMs = 1000;
118
98
  while (true) {
119
99
  try {
120
- await sendOnce(events, ingestUrl, token);
100
+ await sendOnce(envelope, ingestUrl, token);
121
101
  return;
122
102
  } catch (e) {
123
103
  attempt++;
@@ -129,22 +109,18 @@ const sendWithRetry = async (
129
109
  };
130
110
 
131
111
  const sendOnce = async (
132
- events: Event[],
112
+ envelope: BatchEnvelope,
133
113
  ingestUrl: string,
134
114
  token: string,
135
115
  ): Promise<void> => {
136
- const url =
137
- events.length === 1 ? `${ingestUrl}/v1/events` : `${ingestUrl}/v1/events:batch`;
138
- const body = events.length === 1 ? events[0] : { events };
139
-
140
- const resp = await fetch(url, {
116
+ const resp = await fetch(`${ingestUrl}/v1/events:batch`, {
141
117
  method: 'POST',
142
118
  headers: {
143
119
  'Content-Type': 'application/json',
144
120
  Authorization: `Bearer ${token}`,
145
121
  'Sentori-Sdk': `react-native/${SDK_VERSION}`,
146
122
  },
147
- body: JSON.stringify(body),
123
+ body: JSON.stringify(envelope),
148
124
  });
149
125
 
150
126
  if (resp.status === 429) {
@@ -162,11 +138,11 @@ const sendOnce = async (
162
138
  if (resp.status >= 500) {
163
139
  throw new Error(`server-${resp.status}`);
164
140
  }
165
- // 4xx other than 429 = client error, drop silently
141
+ // 4xx other than 429 = client error; per-item outcomes are the
142
+ // server's business — drop silently rather than crashloop.
166
143
  };
167
144
 
168
- const sleep = (ms: number): Promise<void> =>
169
- new Promise((r) => setTimeout(r, ms));
145
+ const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
170
146
 
171
147
  type AsyncStorageLike = {
172
148
  getItem(key: string): Promise<string | null>;
@@ -175,18 +151,14 @@ type AsyncStorageLike = {
175
151
  };
176
152
 
177
153
  const getAsyncStorage = async (): Promise<AsyncStorageLike | null> => {
178
- // v0.8.5 — host may have the JS package without pod install /
179
- // prebuild → getItem crashes from a microtask outside our reach.
154
+ // Host may have the JS package without pod install / prebuild →
155
+ // getItem would crash from a microtask outside our reach.
180
156
  if (!isAnyNativeModuleLinked(['RNCAsyncStorage', 'AsyncStorageModule'])) {
181
157
  return null;
182
158
  }
183
159
  try {
184
- // Resolve via the host's runtime `require` rather than `import()`.
185
- // `import()` is type-checked at build time (TS6 strict-mode); the
186
- // peer dep isn't installed in monorepo-root CI, which made `bun
187
- // run build:sdks` fail to find the type declarations. The peer is
188
- // optional at runtime anyway — the isAnyNativeModuleLinked guard
189
- // above already returned `null` if the package isn't installed.
160
+ // Resolve via the host's runtime `require` rather than `import()`
161
+ // — the peer dep is optional and absent in monorepo CI.
190
162
  // eslint-disable-next-line @typescript-eslint/no-require-imports
191
163
  const mod = require('@react-native-async-storage/async-storage') as {
192
164
  default?: AsyncStorageLike;
@@ -197,12 +169,13 @@ const getAsyncStorage = async (): Promise<AsyncStorageLike | null> => {
197
169
  }
198
170
  };
199
171
 
200
- const persist = async (events: Event[]): Promise<void> => {
172
+ const persist = async (events: WireEvent[]): Promise<void> => {
173
+ if (events.length === 0) return;
201
174
  const AsyncStorage = await getAsyncStorage();
202
175
  if (!AsyncStorage) return;
203
176
  try {
204
177
  const existing = await AsyncStorage.getItem(STORAGE_KEY);
205
- const prev: Event[] = existing ? JSON.parse(existing) : [];
178
+ const prev: WireEvent[] = existing ? JSON.parse(existing) : [];
206
179
  const merged = [...prev, ...events].slice(-MAX_PERSISTED);
207
180
  await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
208
181
  } catch {
@@ -217,7 +190,7 @@ export const drainOfflineQueue = async (): Promise<void> => {
217
190
  const raw = await AsyncStorage.getItem(STORAGE_KEY);
218
191
  if (!raw) return;
219
192
  await AsyncStorage.removeItem(STORAGE_KEY);
220
- const events: Event[] = JSON.parse(raw);
193
+ const events: WireEvent[] = JSON.parse(raw);
221
194
  for (const e of events) _queue.push(e);
222
195
  await flush();
223
196
  } catch {
@@ -225,228 +198,27 @@ export const drainOfflineQueue = async (): Promise<void> => {
225
198
  }
226
199
  };
227
200
 
228
- export const __resetForTests = (): void => {
229
- _queue = [];
230
- if (_flushTimer) clearTimeout(_flushTimer);
231
- _flushTimer = null;
232
- if (_spanTimer) clearInterval(_spanTimer);
233
- _spanTimer = null;
234
- _started = false;
235
- };
236
-
237
- export const __peekQueue = (): readonly Event[] => _queue;
238
-
239
- /**
240
- * Phase 26 sub-B: session ping transport. Best-effort; we don't queue
241
- * pings the way we queue events because they fire on background and
242
- * AsyncStorage writes during background can be killed by the OS. If
243
- * the network's down, the ping is lost — the session counters tolerate
244
- * this.
245
- */
246
- export const sendSessionPing = async (
247
- ingestUrl: string,
248
- token: string,
249
- ping: unknown
250
- ): Promise<void> => {
251
- try {
252
- await fetch(`${ingestUrl}/v1/sessions`, {
253
- body: JSON.stringify(ping),
254
- headers: {
255
- Authorization: `Bearer ${token}`,
256
- 'Content-Type': 'application/json',
257
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
258
- },
259
- method: 'POST',
260
- });
261
- } catch {
262
- // best-effort
263
- }
264
- };
265
-
266
- /**
267
- * v1.1 chunk B — flush a batched set of `sentori.track()` analytics
268
- * events. The host SDK batches calls into a fixed-size ring drained
269
- * every 30 s. Best-effort; a failure drops the batch.
270
- */
271
- export const sendTrackBatch = async (
272
- ingestUrl: string,
273
- token: string,
274
- events: Array<{
275
- environment?: string;
276
- name: string;
277
- props?: Record<string, unknown>;
278
- release?: string;
279
- route?: string;
280
- sessionId?: string;
281
- ts: string;
282
- userId?: string;
283
- }>,
284
- ): Promise<void> => {
285
- if (events.length === 0) return;
286
- try {
287
- await fetch(`${ingestUrl}/v1/track:batch`, {
288
- body: JSON.stringify({ events }),
289
- headers: {
290
- Authorization: `Bearer ${token}`,
291
- 'Content-Type': 'application/json',
292
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
293
- },
294
- method: 'POST',
295
- });
296
- } catch {
297
- // best-effort
298
- }
299
- };
300
-
301
- /**
302
- * v0.8.3 — flush a batched set of custom metrics. The host SDK
303
- * batches recordMetric() calls into a fixed-size ring (drained on
304
- * a timer + at next captureException) so a busy loop doesn't spin
305
- * up one fetch per point. Best-effort, no retry.
306
- */
307
- export const sendMetricsBatch = async (
308
- ingestUrl: string,
309
- token: string,
310
- metrics: Array<{
311
- name: string;
312
- tags?: Record<string, string>;
313
- ts?: string;
314
- value: number;
315
- }>,
316
- ): Promise<void> => {
317
- if (metrics.length === 0) return;
318
- try {
319
- await fetch(`${ingestUrl}/v1/metrics:batch`, {
320
- body: JSON.stringify({ metrics }),
321
- headers: {
322
- Authorization: `Bearer ${token}`,
323
- 'Content-Type': 'application/json',
324
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
325
- },
326
- method: 'POST',
327
- });
328
- } catch {
329
- // best-effort
330
- }
331
- };
332
-
333
201
  /**
334
- * v2.1 W2 — POST a batched set of auto-instrument runtime metric
335
- * points. Sibling of sendMetricsBatch; different endpoint
336
- * (`/v1/runtime-metrics:batch`) because the storage shape +
337
- * validation rules + rate-limit budget differ — see
338
- * docs/design/v2-metrics.md.
339
- *
340
- * Returns true on 2xx so the caller can leave the batch drained;
341
- * returns false on anything else (network error / non-2xx) so the
342
- * caller rebuffer-and-retries on the next flush via
343
- * `rebufferRuntimeMetrics(batch)`.
344
- */
345
- export const sendRuntimeMetricsBatch = async (
346
- ingestUrl: string,
347
- token: string,
348
- metrics: Array<{
349
- name: string;
350
- tags?: Record<string, string>;
351
- ts: string;
352
- value: number;
353
- }>,
354
- ): Promise<boolean> => {
355
- if (metrics.length === 0) return true;
356
- try {
357
- const resp = await fetch(`${ingestUrl}/v1/runtime-metrics:batch`, {
358
- body: JSON.stringify({ metrics }),
359
- headers: {
360
- Authorization: `Bearer ${token}`,
361
- 'Content-Type': 'application/json',
362
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
363
- },
364
- method: 'POST',
365
- });
366
- return resp.ok;
367
- } catch {
368
- return false;
369
- }
370
- };
371
-
372
- /**
373
- * v0.8.2 — submit a user-supplied bug report. Fire-and-forget; resolves
374
- * with the server-assigned id on success or `null` on any failure.
375
- * The host app typically calls this from a "Report a problem" form;
376
- * pass `eventId` if you're reporting a specific crash the user just
377
- * saw so the report links to that event's issue automatically.
378
- */
379
- export const sendUserReport = async (
380
- ingestUrl: string,
381
- token: string,
382
- report: {
383
- body: string;
384
- email?: string;
385
- eventId?: string;
386
- name?: string;
387
- title: string;
388
- },
389
- ): Promise<null | { id: string; issueId: null | string }> => {
390
- try {
391
- const resp = await fetch(`${ingestUrl}/v1/user-reports`, {
392
- body: JSON.stringify(report),
393
- headers: {
394
- Authorization: `Bearer ${token}`,
395
- 'Content-Type': 'application/json',
396
- 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
397
- },
398
- method: 'POST',
399
- });
400
- if (!resp.ok) return null;
401
- const j = (await resp.json()) as { id: string; issueId: null | string };
402
- return j;
403
- } catch {
404
- return null;
405
- }
406
- };
407
-
408
- // ──────────────────────────────────────────────────────────────────
409
- // Phase 42 sub-D.05 — attachment upload pipeline
410
- // ──────────────────────────────────────────────────────────────────
411
-
412
- /**
413
- * Upload a base64-encoded binary blob as an attachment for a known
414
- * event. The event must NOT have been POSTed yet — the server-side
415
- * ingest validation in events.rs only honours `event.attachments[].ref`
416
- * when the matching `event_attachments` row already exists for the
417
- * same (event_id, project_id). Caller's contract:
418
- *
419
- * 1. Generate `event.id` (uuidV7).
420
- * 2. Build the blob (e.g. via `captureScreenshot`).
421
- * 3. `await uploadAttachment(...)` → get `{ ref, sizeBytes, mediaType }`.
422
- * 4. Push `{ ref, kind, ... }` into `event.attachments` then enqueue.
423
- *
424
- * Returns `null` on any non-fatal failure (network down, store
425
- * disabled, 4xx, timeout). The error event still ships without the
426
- * attachment so we never lose the actual crash.
202
+ * Upload one attachment for an already-enqueued event. Returns null
203
+ * on any non-fatal failure — the event still ships without the
204
+ * attachment so the crash itself is never lost.
427
205
  */
428
206
  export const uploadAttachment = async (
429
207
  eventId: string,
430
- kind: import('./types').AttachmentMeta['kind'],
208
+ kind: import('@goliapkg/sentori-core').AttachmentKind,
431
209
  blob: { base64: string; mediaType: string },
432
210
  opts: { source?: 'android' | 'ios' | 'js' } = {},
433
- ): Promise<import('./types').AttachmentMeta | null> => {
211
+ ): Promise<{ ref: string } | null> => {
434
212
  const config = getConfig();
435
213
  if (!config) return null;
436
214
  const url = `${config.ingestUrl}/v1/events/${encodeURIComponent(eventId)}/attachments/${encodeURIComponent(kind)}`;
437
215
 
438
- // RN-style multipart: `{ uri, type, name }` is what the native
439
- // FormData implementation expects for a file part — the bridge
440
- // serializes a data: URI without us having to allocate a Blob.
441
216
  const form = new FormData();
442
- form.append(
443
- 'file',
444
- {
445
- name: filenameFor(kind, blob.mediaType),
446
- type: blob.mediaType,
447
- uri: `data:${blob.mediaType};base64,${blob.base64}`,
448
- } as unknown as Blob,
449
- );
217
+ form.append('file', {
218
+ name: `${kind}.bin`,
219
+ type: blob.mediaType,
220
+ uri: `data:${blob.mediaType};base64,${blob.base64}`,
221
+ } as unknown as Blob);
450
222
  form.append('source', opts.source ?? 'js');
451
223
 
452
224
  try {
@@ -458,84 +230,25 @@ export const uploadAttachment = async (
458
230
  },
459
231
  method: 'POST',
460
232
  });
461
- // Phase 48 sub-A: accept any 2xx instead of strict 201. Reverse
462
- // proxies in front of ingest occasionally rewrite 201 → 202 (the
463
- // exact symptom Insight observed), and a 200 is also a valid
464
- // "stored" response. We still require a JSON body shaped like
465
- // UploadResponse; non-JSON bodies fall through to null.
466
233
  if (resp.status < 200 || resp.status >= 300) {
467
- noteAttachmentFailure(eventId, kind, `http_${resp.status}`);
468
- // rc.6 — surface the status in dev so Insight-style triage
469
- // doesn't have to guess between 413/422/500. Pre-rc.6 only
470
- // the breadcrumb carried the reason; logcat only saw the
471
- // generic `upload returned null` line.
472
- logger.debug('transport', 'attachment upload non-2xx',
473
- 'eventId=', eventId,
474
- 'kind=', kind,
475
- 'status=', resp.status,
476
- );
234
+ logger.warn(`attachment ${kind} upload http_${resp.status}`);
477
235
  return null;
478
236
  }
479
- const j = (await resp.json().catch(() => null)) as null | {
480
- refId: string;
481
- sizeBytes: number;
482
- mediaType: string;
483
- kind: string;
484
- };
485
- if (!j || !j.refId) {
486
- noteAttachmentFailure(eventId, kind, 'bad_response_body');
487
- logger.debug('transport', 'attachment upload bad-response-body',
488
- 'eventId=', eventId,
489
- 'kind=', kind,
490
- 'status=', resp.status,
491
- );
492
- return null;
493
- }
494
- return {
495
- kind,
496
- mediaType: j.mediaType,
497
- ref: j.refId,
498
- sizeBytes: j.sizeBytes,
499
- source: opts.source ?? 'js',
500
- };
237
+ const body = (await resp.json()) as { refId?: string };
238
+ return body.refId ? { ref: body.refId } : null;
501
239
  } catch (e) {
502
- const reason = e instanceof Error ? `fetch_${e.name}` : 'fetch_unknown';
503
- noteAttachmentFailure(eventId, kind, reason);
504
- logger.debug('transport', 'attachment upload fetch threw',
505
- 'eventId=', eventId,
506
- 'kind=', kind,
507
- 'reason=', reason,
508
- );
240
+ logger.warn(`attachment ${kind} upload failed: ${String(e)}`);
509
241
  return null;
510
242
  }
511
243
  };
512
244
 
513
- /**
514
- * v0.9.7 — Insight F2 fix-forward. When uploadAttachment silently
515
- * returns null, the host event ships without the screenshot and the
516
- * dashboard renders "No attachments captured" — looking identical
517
- * to "host didn't enable screenshots" and leaving us guessing at
518
- * which layer broke.
519
- *
520
- * Drop a breadcrumb so the next event on the same SDK instance
521
- * carries a tagged trail entry. Dashboard's breadcrumb panel will
522
- * show `sentori.attach.failed { eventId, kind, reason }` and we can
523
- * tell upload-broke from never-tried at a glance.
524
- */
525
- function noteAttachmentFailure(
526
- eventId: string,
527
- kind: string,
528
- reason: string,
529
- ): void {
530
- addBreadcrumb('custom', {
531
- category: 'sentori.attach.failed',
532
- eventId,
533
- kind,
534
- reason,
535
- });
536
- }
245
+ export const __resetForTests = (): void => {
246
+ _queue = [];
247
+ _assertStats = new Map();
248
+ if (_flushTimer) clearTimeout(_flushTimer);
249
+ _flushTimer = null;
250
+ _started = false;
251
+ };
537
252
 
538
- function filenameFor(kind: string, mediaType: string): string {
539
- const ext = mediaType.split('/')[1] ?? 'bin';
540
- return `${kind}.${ext}`;
541
- }
253
+ export const __peekQueue = (): readonly WireEvent[] => _queue;
254
+ export const __peekAssertStats = (): readonly AssertStat[] => [..._assertStats.values()];