@goliapkg/sentori-react-native 3.1.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 +5 -5
  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
@@ -1,111 +0,0 @@
1
- // v1.1 +S7 升级 — control channel unit tests.
2
-
3
- import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
4
-
5
- import { setConfig, __resetForTests as resetConfig } from '../config';
6
- import {
7
- __resetControlChannelForTests,
8
- isLiveMode,
9
- startControlChannel,
10
- stopControlChannel,
11
- } from '../control-channel';
12
- import { setUser } from '../capture';
13
-
14
- const originalFetch = globalThis.fetch;
15
-
16
- beforeEach(() => {
17
- resetConfig();
18
- __resetControlChannelForTests();
19
- setConfig({
20
- token: 'st_pk_test',
21
- release: 'app@1.0.0+1',
22
- environment: 'test',
23
- ingestUrl: 'http://localhost:8080',
24
- enabled: true,
25
- screenshotsEnabled: false,
26
- errorSampleRate: null,
27
- traceSampleRate: null,
28
- sessionTrailEnabled: false,
29
- });
30
- setUser({ id: 'u-1' });
31
- });
32
-
33
- afterEach(() => {
34
- globalThis.fetch = originalFetch;
35
- __resetControlChannelForTests();
36
- });
37
-
38
- describe('control-channel', () => {
39
- test('isLiveMode() is false by default', () => {
40
- expect(isLiveMode()).toBe(false);
41
- });
42
-
43
- test('start → poll → server says liveMode:true → isLiveMode flips on', async () => {
44
- globalThis.fetch = mock(async () =>
45
- new Response(JSON.stringify({ liveMode: true, ttlMs: 60_000 }), {
46
- status: 200,
47
- headers: { 'content-type': 'application/json' },
48
- }),
49
- ) as typeof fetch;
50
-
51
- startControlChannel();
52
- // The initial pollOnce is awaited via void, so give it a microtask.
53
- await new Promise((r) => setTimeout(r, 30));
54
- expect(isLiveMode()).toBe(true);
55
- stopControlChannel();
56
- });
57
-
58
- test('server says liveMode:false → isLiveMode stays false', async () => {
59
- globalThis.fetch = mock(async () =>
60
- new Response(JSON.stringify({ liveMode: false, ttlMs: 0 }), {
61
- status: 200,
62
- headers: { 'content-type': 'application/json' },
63
- }),
64
- ) as typeof fetch;
65
-
66
- startControlChannel();
67
- await new Promise((r) => setTimeout(r, 30));
68
- expect(isLiveMode()).toBe(false);
69
- stopControlChannel();
70
- });
71
-
72
- test('network failure leaves prior state (no throw on poll error)', async () => {
73
- globalThis.fetch = mock(async () => {
74
- throw new Error('econn');
75
- }) as typeof fetch;
76
- startControlChannel();
77
- await new Promise((r) => setTimeout(r, 30));
78
- // Should not have thrown; isLiveMode stays false.
79
- expect(isLiveMode()).toBe(false);
80
- stopControlChannel();
81
- });
82
-
83
- test('server-reported ttlMs is capped at 15min defence-in-depth', async () => {
84
- // 24h ttl from server — caller should cap.
85
- globalThis.fetch = mock(async () =>
86
- new Response(JSON.stringify({ liveMode: true, ttlMs: 24 * 60 * 60_000 }), {
87
- status: 200,
88
- headers: { 'content-type': 'application/json' },
89
- }),
90
- ) as typeof fetch;
91
- startControlChannel();
92
- await new Promise((r) => setTimeout(r, 30));
93
- // We don't expose _liveModeUntil directly, but we can at least
94
- // confirm liveMode flipped on without runaway state.
95
- expect(isLiveMode()).toBe(true);
96
- stopControlChannel();
97
- });
98
-
99
- test('without setUser, liveMode stays off (no key to poll)', async () => {
100
- setUser(undefined);
101
- globalThis.fetch = mock(async () =>
102
- new Response(JSON.stringify({ liveMode: true, ttlMs: 60_000 }), {
103
- status: 200,
104
- }),
105
- ) as typeof fetch;
106
- startControlChannel();
107
- await new Promise((r) => setTimeout(r, 30));
108
- expect(isLiveMode()).toBe(false);
109
- stopControlChannel();
110
- });
111
- });
@@ -1,60 +0,0 @@
1
- // v1.1 chunk S1 — install-id module persistence + race-safety.
2
-
3
- import {
4
- afterEach,
5
- beforeEach,
6
- describe,
7
- expect,
8
- it,
9
- } from 'bun:test';
10
-
11
- import {
12
- __resetInstallIdForTests,
13
- getInstallId,
14
- peekInstallId,
15
- } from '../install-id';
16
-
17
- describe('install-id', () => {
18
- beforeEach(() => {
19
- __resetInstallIdForTests();
20
- });
21
-
22
- afterEach(() => {
23
- __resetInstallIdForTests();
24
- });
25
-
26
- it('peekInstallId is null before first resolve', () => {
27
- expect(peekInstallId()).toBe(null);
28
- });
29
-
30
- it('getInstallId generates a stable id and caches it', async () => {
31
- const first = await getInstallId();
32
- expect(typeof first).toBe('string');
33
- expect(first.length).toBeGreaterThan(10);
34
- // sync peek after resolve returns the same value
35
- expect(peekInstallId()).toBe(first);
36
- // second call returns the cached value
37
- expect(await getInstallId()).toBe(first);
38
- });
39
-
40
- it('concurrent callers share the same resolve promise', async () => {
41
- const [a, b, c] = await Promise.all([
42
- getInstallId(),
43
- getInstallId(),
44
- getInstallId(),
45
- ]);
46
- expect(a).toBe(b);
47
- expect(b).toBe(c);
48
- });
49
-
50
- it('produces UUIDv7 shape (variant + version nibble)', async () => {
51
- const id = await getInstallId();
52
- // RFC 4122 dash positions
53
- expect(id[8]).toBe('-');
54
- expect(id[13]).toBe('-');
55
- expect(id[14]).toBe('7'); // version
56
- expect(id[18]).toBe('-');
57
- // variant nibble is 8|9|a|b (high 2 bits = 10)
58
- expect(['8', '9', 'a', 'b']).toContain(id[19]);
59
- });
60
- });
@@ -1,59 +0,0 @@
1
- // v0.9.6 #4 — long-task monitor unit tests.
2
-
3
- import { afterEach, describe, expect, test } from 'bun:test';
4
-
5
- import { clearSpans, drainSpans } from '@goliapkg/sentori-core';
6
-
7
- import {
8
- __resetLongTaskMonitorForTests,
9
- startLongTaskMonitor,
10
- stopLongTaskMonitor,
11
- } from '../long-task-monitor';
12
-
13
- afterEach(() => {
14
- __resetLongTaskMonitorForTests();
15
- clearSpans();
16
- });
17
-
18
- describe('long-task-monitor', () => {
19
- test('disabled is a no-op', () => {
20
- startLongTaskMonitor({ enabled: false });
21
- expect(drainSpans().length).toBe(0);
22
- });
23
-
24
- test('double-start is idempotent', () => {
25
- startLongTaskMonitor({ enabled: true });
26
- // Second call must not register a parallel interval — would
27
- // double-count every tick.
28
- startLongTaskMonitor({ enabled: true });
29
- stopLongTaskMonitor();
30
- });
31
-
32
- test('synthetic long task crosses threshold and emits a span', async () => {
33
- // 80ms threshold so we don't need a 200ms+ block in the test.
34
- startLongTaskMonitor({ enabled: true, thresholdMs: 80 });
35
- // Busy-block the JS thread for ~150ms; the next tick will see
36
- // elapsed >> tickInterval and emit a longtask span.
37
- const t0 = Date.now();
38
- while (Date.now() - t0 < 150) {
39
- // burn — no setTimeout, the thread must actually be busy.
40
- }
41
- // Let the next 50ms tick fire.
42
- await new Promise((r) => setTimeout(r, 80));
43
- const spans = drainSpans();
44
- const longtask = spans.find((s) => s.op === 'sentori.longtask');
45
- expect(longtask).toBeDefined();
46
- expect(longtask?.tags?.['profile.kind']).toBe('longtask');
47
- stopLongTaskMonitor();
48
- });
49
-
50
- test('sub-threshold ticks do not emit', async () => {
51
- startLongTaskMonitor({ enabled: true, thresholdMs: 500 });
52
- // Don't block — let normal idle ticks happen.
53
- await new Promise((r) => setTimeout(r, 200));
54
- const spans = drainSpans();
55
- const longtasks = spans.filter((s) => s.op === 'sentori.longtask');
56
- expect(longtasks.length).toBe(0);
57
- stopLongTaskMonitor();
58
- });
59
- });
@@ -1,45 +0,0 @@
1
- import { afterEach, describe, expect, test } from 'bun:test';
2
-
3
- import { clearSpans, drainSpans } from '@goliapkg/sentori-core';
4
-
5
- import { measureFn } from '../measure';
6
-
7
- afterEach(() => {
8
- clearSpans();
9
- });
10
-
11
- describe('measureFn', () => {
12
- test('runs fn, returns result, emits an ok span', async () => {
13
- const r = await measureFn('addToCart', async () => 42);
14
- expect(r).toBe(42);
15
- const spans = drainSpans();
16
- expect(spans.length).toBe(1);
17
- expect(spans[0]!.op).toBe('sentori.measureFn');
18
- expect(spans[0]!.name).toBe('addToCart');
19
- expect(spans[0]!.status).toBe('ok');
20
- });
21
-
22
- test('supports sync fn too (Promise.resolve hides the difference)', async () => {
23
- const r = await measureFn('syncJob', () => 'hello');
24
- expect(r).toBe('hello');
25
- expect(drainSpans()[0]!.status).toBe('ok');
26
- });
27
-
28
- test('propagates thrown errors and marks span as error', async () => {
29
- await expect(
30
- measureFn('failing', async () => {
31
- throw new Error('nope');
32
- }),
33
- ).rejects.toThrow('nope');
34
- const spans = drainSpans();
35
- expect(spans.length).toBe(1);
36
- expect(spans[0]!.status).toBe('error');
37
- expect(spans[0]!.tags['error.message']).toBe('nope');
38
- });
39
-
40
- test('passes through caller tags', async () => {
41
- await measureFn('withTags', async () => 'ok', { tags: { region: 'jp' } });
42
- const spans = drainSpans();
43
- expect(spans[0]!.tags.region).toBe('jp');
44
- });
45
- });
@@ -1,175 +0,0 @@
1
- import {
2
- __resetTraceContextForTests,
3
- __useFallbackTraceContextForTests,
4
- activeSpan,
5
- clearSpans,
6
- drainSpans,
7
- setActiveSpan,
8
- startSpan,
9
- } from '@goliapkg/sentori-core';
10
- import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test';
11
-
12
- import { type NavigationRefLike, useTraceNavigation } from '../navigation';
13
-
14
- // We test the hook without a React renderer (sdk/react-native has no
15
- // react-test-renderer / @testing-library dev-dep). `harness` mirrors
16
- // the hook's effect body 1:1 — when production changes, this changes
17
- // too. The point is to verify the observable contract: spans pushed
18
- // in the right order + the open screen span left active.
19
-
20
- class FakeNav implements NavigationRefLike {
21
- private listeners: Array<() => void> = [];
22
- private route: { name: string } | undefined;
23
-
24
- setInitialRoute(name: string): void {
25
- this.route = { name };
26
- }
27
- addListener(_event: 'state', listener: () => void): () => void {
28
- this.listeners.push(listener);
29
- return () => {
30
- this.listeners = this.listeners.filter((l) => l !== listener);
31
- };
32
- }
33
- getCurrentRoute(): { name: string } | undefined {
34
- return this.route;
35
- }
36
- go(name: string): void {
37
- this.route = { name };
38
- this.listeners.forEach((l) => l());
39
- }
40
- }
41
-
42
- function harness(navigationRef: NavigationRefLike): () => void {
43
- let lastRoute: null | string = null;
44
- let openSpan: ReturnType<typeof startSpan> | null = null;
45
-
46
- const openScreenSpan = (from: null | string, to: string) => {
47
- const span = startSpan('react.navigation', {
48
- name: from ? `${from} → ${to}` : to,
49
- parent: null,
50
- tags: { 'nav.from': from ?? '', 'nav.to': to },
51
- });
52
- openSpan = span;
53
- setActiveSpan(span);
54
- lastRoute = to;
55
- };
56
-
57
- const initial = navigationRef.getCurrentRoute()?.name ?? null;
58
- if (initial !== null) openScreenSpan(null, initial);
59
- else lastRoute = null;
60
-
61
- const unsub = navigationRef.addListener('state', () => {
62
- const next = navigationRef.getCurrentRoute()?.name ?? null;
63
- if (next === null || next === lastRoute) return;
64
- openSpan?.finish({ status: 'ok' });
65
- openScreenSpan(lastRoute, next);
66
- });
67
-
68
- return () => {
69
- unsub();
70
- openSpan?.finish({ status: 'ok' });
71
- openSpan = null;
72
- setActiveSpan(null);
73
- };
74
- }
75
-
76
- beforeEach(() => {
77
- // Navigation relies on setActiveSpan, which is a no-op on the
78
- // ALS impl that bun (Node) picks. In production this runs on RN,
79
- // where the fallback impl is in effect — exercise that one.
80
- __useFallbackTraceContextForTests();
81
- clearSpans();
82
- });
83
- afterEach(() => {
84
- setActiveSpan(null);
85
- clearSpans();
86
- });
87
- afterAll(() => {
88
- __resetTraceContextForTests();
89
- });
90
-
91
- describe('useTraceNavigation', () => {
92
- test('exports a hook', () => {
93
- expect(typeof useTraceNavigation).toBe('function');
94
- });
95
-
96
- test('initial mount opens a span for the first screen and leaves it active', () => {
97
- const nav = new FakeNav();
98
- nav.setInitialRoute('Home');
99
- const cleanup = harness(nav);
100
-
101
- // span not finished yet (still on Home) → buffer empty
102
- expect(drainSpans()).toHaveLength(0);
103
- expect(activeSpan()).not.toBeNull();
104
-
105
- cleanup();
106
- const spans = drainSpans();
107
- expect(spans).toHaveLength(1);
108
- expect(spans[0]?.op).toBe('react.navigation');
109
- expect(spans[0]?.name).toBe('Home');
110
- expect(spans[0]?.tags).toEqual({ 'nav.from': '', 'nav.to': 'Home' });
111
- });
112
-
113
- test('mount with no current route → no span, no active', () => {
114
- const nav = new FakeNav(); // no initial route
115
- const cleanup = harness(nav);
116
- expect(activeSpan()).toBeNull();
117
- cleanup();
118
- expect(drainSpans()).toHaveLength(0);
119
- });
120
-
121
- test('http.client span during a screen becomes a child of the nav span', () => {
122
- const nav = new FakeNav();
123
- nav.setInitialRoute('Home');
124
- const cleanup = harness(nav);
125
- const navSpan = activeSpan()!;
126
-
127
- // simulate the network handler opening a request span (no explicit
128
- // parent → inherits the active nav span)
129
- const req = startSpan('http.client', { name: 'GET /x' });
130
- expect(req.parentSpanId).toBe(navSpan.spanId);
131
- expect(req.traceId).toBe(navSpan.traceId);
132
- req.finish({ status: 'ok' });
133
-
134
- cleanup();
135
- const spans = drainSpans();
136
- const navTrace = spans.find((s) => s.op === 'react.navigation')!.traceId;
137
- expect(spans.every((s) => s.traceId === navTrace)).toBe(true);
138
- });
139
-
140
- test('each screen is its own trace root (transitions do not nest)', () => {
141
- const nav = new FakeNav();
142
- nav.setInitialRoute('A');
143
- const cleanup = harness(nav);
144
- nav.go('B');
145
- nav.go('C');
146
- cleanup();
147
-
148
- const spans = drainSpans().filter((s) => s.op === 'react.navigation');
149
- expect(spans).toHaveLength(3);
150
- expect(spans.map((s) => s.name)).toEqual(['A', 'A → B', 'B → C']);
151
- expect(spans.every((s) => s.parentSpanId === null)).toBe(true);
152
- expect(new Set(spans.map((s) => s.traceId)).size).toBe(3);
153
- });
154
-
155
- test('same-route state event does not open a new span', () => {
156
- const nav = new FakeNav();
157
- nav.setInitialRoute('Home');
158
- const cleanup = harness(nav);
159
- nav.go('Home');
160
- cleanup();
161
- expect(drainSpans().filter((s) => s.op === 'react.navigation')).toHaveLength(1);
162
- });
163
-
164
- test('cleanup finishes the open span and clears the active span', () => {
165
- const nav = new FakeNav();
166
- nav.setInitialRoute('Home');
167
- const cleanup = harness(nav);
168
- nav.go('Settings');
169
- cleanup();
170
-
171
- expect(activeSpan()).toBeNull();
172
- const spans = drainSpans().filter((s) => s.op === 'react.navigation');
173
- expect(spans.map((s) => s.name)).toEqual(['Home', 'Home → Settings']);
174
- });
175
- });
@@ -1,238 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
2
-
3
- // Mock the native bridge layer only — leaves expo-modules-core / react-native
4
- // pristine so other test files in the same bun process aren't affected.
5
-
6
- type DrainResult = {
7
- token?: string
8
- error?: string
9
- notifications: Array<Record<string, unknown>>
10
- taps: Array<Record<string, unknown>>
11
- }
12
-
13
- let _drainQueue: DrainResult[] = []
14
- let _permissionAnswer: null | string = null
15
- let _registerInvocations = 0
16
- let _unregisterInvocations = 0
17
-
18
- mock.module('../native', () => ({
19
- pushGetStatus: async () => _permissionAnswer,
20
- pushRequestPermission: async () => _permissionAnswer ?? 'notDetermined',
21
- pushRegister: () => {
22
- _registerInvocations++
23
- },
24
- pushUnregister: () => {
25
- _unregisterInvocations++
26
- },
27
- pushDrainState: async () => {
28
- if (_drainQueue.length === 0) {
29
- return { notifications: [], taps: [] }
30
- }
31
- return _drainQueue.shift()!
32
- },
33
- }))
34
-
35
- // Mock the config getter — push reads ingest URL + bearer from here.
36
- // Stub ALL public exports so other test files that import named
37
- // exports from '../config' don't get a "Export named X not found"
38
- // error from the partial mock (bun mock.module replaces the whole
39
- // module surface, not just the named exports listed).
40
- let _mockConfig: { ingestUrl: string; token: string } | null = null
41
- mock.module('../config', () => ({
42
- getConfig: () => _mockConfig,
43
- isInitialized: () => _mockConfig !== null,
44
- setConfig: (c: typeof _mockConfig) => {
45
- _mockConfig = c
46
- },
47
- __resetForTests: () => {
48
- _mockConfig = null
49
- },
50
- }))
51
-
52
- import { register, unregister, getCachedIpt, __setPlatformForTests } from '../push'
53
-
54
- type FetchCall = { url: string; method: string; body?: string }
55
- let _fetchCalls: FetchCall[] = []
56
- let _fetchResponse: { status: number; body: unknown } = {
57
- status: 200,
58
- body: { id: 'ipt_deadbeef' },
59
- }
60
-
61
- const originalFetch = globalThis.fetch
62
- beforeEach(() => {
63
- _fetchCalls = []
64
- _mockConfig = { ingestUrl: 'https://ingest.test', token: 'st_test' }
65
- _drainQueue = []
66
- _permissionAnswer = null
67
- _registerInvocations = 0
68
- _unregisterInvocations = 0
69
- _fetchResponse = { status: 200, body: { id: 'ipt_deadbeef' } }
70
- globalThis.fetch = (async (
71
- input: RequestInfo | URL,
72
- init?: RequestInit,
73
- ): Promise<Response> => {
74
- _fetchCalls.push({
75
- url: typeof input === 'string' ? input : input.toString(),
76
- method: init?.method ?? 'GET',
77
- body: typeof init?.body === 'string' ? init.body : undefined,
78
- })
79
- return new Response(JSON.stringify(_fetchResponse.body), {
80
- status: _fetchResponse.status,
81
- })
82
- }) as typeof fetch
83
- })
84
-
85
- afterEach(() => {
86
- globalThis.fetch = originalFetch
87
- })
88
-
89
- describe('push.register', () => {
90
- it('rejects cleanly when permission is denied', async () => {
91
- _permissionAnswer = 'denied'
92
- await expect(register()).rejects.toThrow(/permission/i)
93
- expect(_fetchCalls).toHaveLength(0)
94
- })
95
-
96
- it('rejects when the native token times out', async () => {
97
- _permissionAnswer = 'granted'
98
- await expect(register({ tokenTimeoutMs: 50 })).rejects.toThrow(/not received/i)
99
- expect(_fetchCalls).toHaveLength(0)
100
- })
101
-
102
- it('POSTs to /v1/push/tokens with the APNs hex token and resolves to ipt', async () => {
103
- _permissionAnswer = 'granted'
104
- _drainQueue = [
105
- { notifications: [], taps: [] },
106
- { token: '0123abcdef', notifications: [], taps: [] },
107
- ]
108
- const result = await register({ linkHash: 'h1' })
109
- expect(result.ipt).toBe('ipt_deadbeef')
110
- expect(getCachedIpt()).toBe('ipt_deadbeef')
111
- expect(_fetchCalls).toHaveLength(1)
112
- expect(_fetchCalls[0]?.url).toContain('/v1/push/tokens')
113
- expect(_fetchCalls[0]?.method).toBe('POST')
114
- const body = JSON.parse(_fetchCalls[0]?.body ?? '{}')
115
- expect(body.provider).toBe('apns')
116
- expect(body.nativeToken).toBe('0123abcdef')
117
- expect(body.linkHash).toBe('h1')
118
- expect(_registerInvocations).toBe(1)
119
- })
120
-
121
- it('surfaces server failures', async () => {
122
- _permissionAnswer = 'granted'
123
- _drainQueue = [{ token: '00ff', notifications: [], taps: [] }]
124
- _fetchResponse = { status: 503, body: { error: 'dbNotConfigured' } }
125
- await expect(register()).rejects.toThrow(/503/)
126
- })
127
-
128
- it('fires onMessage for buffered notifications surfaced during waitForToken', async () => {
129
- _permissionAnswer = 'granted'
130
- _drainQueue = [
131
- {
132
- notifications: [
133
- { id: 'n1', title: 'Hi', body: 'hello', userInfo: { x: 1 } },
134
- ],
135
- taps: [],
136
- },
137
- { token: 'abcd', notifications: [], taps: [] },
138
- ]
139
- const seen: Array<{ title?: string; body?: string }> = []
140
- await register({ onMessage: (m) => seen.push(m) })
141
- expect(seen).toHaveLength(1)
142
- expect(seen[0]?.title).toBe('Hi')
143
- expect(seen[0]?.body).toBe('hello')
144
- })
145
- })
146
-
147
- describe('push.register — Android (FCM) branch', () => {
148
- beforeEach(() => __setPlatformForTests('android'))
149
- afterEach(() => __setPlatformForTests(null))
150
-
151
- it('POSTs with provider:"fcm" and omits env on Android', async () => {
152
- _permissionAnswer = 'granted'
153
- _drainQueue = [{ token: 'fcm-reg-token', notifications: [], taps: [] }]
154
- await register()
155
- expect(_fetchCalls).toHaveLength(1)
156
- const body = JSON.parse(_fetchCalls[0]?.body ?? '{}') as Record<string, unknown>
157
- expect(body.provider).toBe('fcm')
158
- expect(body.nativeToken).toBe('fcm-reg-token')
159
- expect('env' in body).toBe(false)
160
- })
161
- })
162
-
163
- describe('push.auto-correlate (v2.26)', () => {
164
- it('writes a push breadcrumb when notification carries _sentori.msgId', async () => {
165
- const { clearBreadcrumbs, getBreadcrumbs } = await import('@goliapkg/sentori-core')
166
- clearBreadcrumbs()
167
- _permissionAnswer = 'granted'
168
- _drainQueue = [
169
- {
170
- notifications: [
171
- {
172
- body: 'a body',
173
- id: 'n2',
174
- title: 'A title',
175
- userInfo: { _sentori: { msgId: 'send_abc123' } },
176
- },
177
- ],
178
- taps: [],
179
- },
180
- { notifications: [], taps: [], token: 'abcd' },
181
- ]
182
- await register()
183
- const crumbs = getBreadcrumbs()
184
- const pushCrumb = crumbs.find((c) => c.type === 'push')
185
- expect(pushCrumb).toBeDefined()
186
- expect((pushCrumb?.data as Record<string, unknown>)?.msgId).toBe('send_abc123')
187
- expect((pushCrumb?.data as Record<string, unknown>)?.title).toBe('A title')
188
- expect((pushCrumb?.data as Record<string, unknown>)?.opened).toBe(false)
189
- })
190
-
191
- it('skips breadcrumb when payload has no _sentori.msgId', async () => {
192
- const { clearBreadcrumbs, getBreadcrumbs } = await import('@goliapkg/sentori-core')
193
- clearBreadcrumbs()
194
- _permissionAnswer = 'granted'
195
- _drainQueue = [
196
- { notifications: [{ id: 'n3', title: 'No correlation' }], taps: [] },
197
- { notifications: [], taps: [], token: 'abcd' },
198
- ]
199
- await register()
200
- const crumbs = getBreadcrumbs()
201
- expect(crumbs.find((c) => c.type === 'push')).toBeUndefined()
202
- })
203
-
204
- it('marks tap breadcrumb as opened:true', async () => {
205
- const { clearBreadcrumbs, getBreadcrumbs } = await import('@goliapkg/sentori-core')
206
- clearBreadcrumbs()
207
- _permissionAnswer = 'granted'
208
- _drainQueue = [
209
- {
210
- notifications: [],
211
- taps: [{ userInfo: { _sentori: { msgId: 'send_tap' } } }],
212
- },
213
- { notifications: [], taps: [], token: 'abcd' },
214
- ]
215
- await register()
216
- const pushCrumb = getBreadcrumbs().find((c) => c.type === 'push')
217
- expect(pushCrumb).toBeDefined()
218
- expect((pushCrumb?.data as Record<string, unknown>)?.opened).toBe(true)
219
- expect((pushCrumb?.data as Record<string, unknown>)?.msgId).toBe('send_tap')
220
- })
221
- })
222
-
223
- describe('push.unregister', () => {
224
- it('DELETEs the cached ipt and clears the local state', async () => {
225
- _permissionAnswer = 'granted'
226
- _drainQueue = [{ token: 'feedface', notifications: [], taps: [] }]
227
- await register()
228
- expect(getCachedIpt()).toBe('ipt_deadbeef')
229
- _fetchCalls = []
230
-
231
- await unregister()
232
- expect(_fetchCalls).toHaveLength(1)
233
- expect(_fetchCalls[0]?.url).toContain('/v1/push/tokens/ipt_deadbeef')
234
- expect(_fetchCalls[0]?.method).toBe('DELETE')
235
- expect(getCachedIpt()).toBeNull()
236
- expect(_unregisterInvocations).toBeGreaterThan(0)
237
- })
238
- })