@goliapkg/sentori-react-native 4.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +131 -0
- package/README.md +40 -30
- package/android/build.gradle +21 -0
- package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
- package/android/src/main/java/com/sentori/SentoriModule.kt +8 -0
- package/android/src/main/java/com/sentori/SentoriScreenshotCapture.kt +36 -4
- package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
- package/android/src/test/resources/robolectric.properties +10 -0
- package/ios/SentoriCrashHandler.swift +48 -4
- package/ios/SentoriModule.swift +12 -0
- package/ios/SentoriScreenshotCapture.swift +34 -3
- package/lib/config.d.ts +17 -47
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +3 -0
- package/lib/config.js.map +1 -1
- package/lib/device.d.ts +5 -0
- package/lib/device.d.ts.map +1 -0
- package/lib/device.js +29 -0
- package/lib/device.js.map +1 -0
- package/lib/emit-hooks.d.ts +7 -0
- package/lib/emit-hooks.d.ts.map +1 -0
- package/lib/emit-hooks.js +24 -0
- package/lib/emit-hooks.js.map +1 -0
- package/lib/error-boundary.d.ts.map +1 -1
- package/lib/error-boundary.js +1 -1
- package/lib/error-boundary.js.map +1 -1
- package/lib/feature-flags.d.ts.map +1 -1
- package/lib/handlers/dev-symbolicate.d.ts +1 -1
- package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
- package/lib/handlers/dev-symbolicate.js +2 -0
- package/lib/handlers/dev-symbolicate.js.map +1 -1
- package/lib/handlers/global.js +1 -1
- package/lib/handlers/global.js.map +1 -1
- package/lib/handlers/lifecycle.js +3 -3
- package/lib/handlers/lifecycle.js.map +1 -1
- package/lib/handlers/network.d.ts +2 -2
- package/lib/handlers/network.d.ts.map +1 -1
- package/lib/handlers/network.js +69 -187
- package/lib/handlers/network.js.map +1 -1
- package/lib/handlers/promise.js +1 -1
- package/lib/handlers/promise.js.map +1 -1
- package/lib/handlers/screenshot.d.ts.map +1 -1
- package/lib/handlers/screenshot.js +3 -12
- package/lib/handlers/screenshot.js.map +1 -1
- package/lib/index.d.ts +32 -109
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +54 -103
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +3 -150
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +100 -261
- package/lib/init.js.map +1 -1
- package/lib/long-task-monitor.d.ts +1 -7
- package/lib/long-task-monitor.d.ts.map +1 -1
- package/lib/long-task-monitor.js +37 -58
- package/lib/long-task-monitor.js.map +1 -1
- package/lib/mask.d.ts +5 -13
- package/lib/mask.d.ts.map +1 -1
- package/lib/mask.js +29 -30
- package/lib/mask.js.map +1 -1
- package/lib/mobile-vitals.d.ts +3 -31
- package/lib/mobile-vitals.d.ts.map +1 -1
- package/lib/mobile-vitals.js +22 -61
- package/lib/mobile-vitals.js.map +1 -1
- package/lib/native-pending.d.ts +3 -0
- package/lib/native-pending.d.ts.map +1 -0
- package/lib/native-pending.js +64 -0
- package/lib/native-pending.js.map +1 -0
- package/lib/native.d.ts +4 -11
- package/lib/native.d.ts.map +1 -1
- package/lib/native.js +23 -0
- package/lib/native.js.map +1 -1
- package/lib/navigation.d.ts +3 -17
- package/lib/navigation.d.ts.map +1 -1
- package/lib/navigation.js +32 -133
- package/lib/navigation.js.map +1 -1
- package/lib/netinfo.d.ts +1 -1
- package/lib/netinfo.d.ts.map +1 -1
- package/lib/netinfo.js +2 -2
- package/lib/netinfo.js.map +1 -1
- package/lib/push.d.ts.map +1 -1
- package/lib/push.js +2 -4
- package/lib/push.js.map +1 -1
- package/lib/rage-tap.d.ts.map +1 -1
- package/lib/rage-tap.js +56 -23
- package/lib/rage-tap.js.map +1 -1
- package/lib/replay-screens.d.ts +15 -0
- package/lib/replay-screens.d.ts.map +1 -0
- package/lib/replay-screens.js +87 -0
- package/lib/replay-screens.js.map +1 -0
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +3 -24
- package/lib/replay.js.map +1 -1
- package/lib/responsiveness-detector.d.ts +18 -0
- package/lib/responsiveness-detector.d.ts.map +1 -0
- package/lib/responsiveness-detector.js +55 -0
- package/lib/responsiveness-detector.js.map +1 -0
- package/lib/scope.d.ts +7 -0
- package/lib/scope.d.ts.map +1 -0
- package/lib/scope.js +41 -0
- package/lib/scope.js.map +1 -0
- package/lib/transport.d.ts +19 -92
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +75 -288
- package/lib/transport.js.map +1 -1
- package/lib/verbs.d.ts +16 -0
- package/lib/verbs.d.ts.map +1 -0
- package/lib/verbs.js +153 -0
- package/lib/verbs.js.map +1 -0
- package/package.json +4 -3
- package/src/__tests__/iron-rule.test.ts +128 -0
- package/src/__tests__/replay-screens.test.ts +63 -0
- package/src/__tests__/responsiveness-detector.test.ts +74 -0
- package/src/__tests__/screenshot.test.ts +0 -2
- package/src/__tests__/transport.test.ts +66 -213
- package/src/__tests__/verbs.test.ts +91 -0
- package/src/config.ts +20 -47
- package/src/device.ts +36 -0
- package/src/emit-hooks.ts +30 -0
- package/src/error-boundary.tsx +1 -1
- package/src/handlers/dev-symbolicate.ts +2 -1
- package/src/handlers/global.ts +1 -1
- package/src/handlers/lifecycle.ts +3 -3
- package/src/handlers/network.ts +95 -211
- package/src/handlers/promise.ts +1 -1
- package/src/handlers/screenshot.ts +3 -11
- package/src/index.ts +84 -215
- package/src/init.ts +115 -449
- package/src/long-task-monitor.ts +42 -64
- package/src/mask.ts +29 -31
- package/src/mobile-vitals.ts +22 -82
- package/src/native-pending.ts +92 -0
- package/src/native.ts +33 -0
- package/src/navigation.ts +34 -142
- package/src/netinfo.ts +3 -3
- package/src/push.ts +2 -5
- package/src/rage-tap.tsx +72 -25
- package/src/replay-screens.ts +95 -0
- package/src/replay.ts +3 -20
- package/src/responsiveness-detector.ts +66 -0
- package/src/scope.ts +50 -0
- package/src/transport.ts +84 -371
- package/src/verbs.ts +198 -0
- package/lib/breadcrumbs.d.ts +0 -18
- package/lib/breadcrumbs.d.ts.map +0 -1
- package/lib/breadcrumbs.js +0 -42
- package/lib/breadcrumbs.js.map +0 -1
- package/lib/bundle-info.d.ts +0 -12
- package/lib/bundle-info.d.ts.map +0 -1
- package/lib/bundle-info.js +0 -73
- package/lib/bundle-info.js.map +0 -1
- package/lib/capture.d.ts +0 -140
- package/lib/capture.d.ts.map +0 -1
- package/lib/capture.js +0 -511
- package/lib/capture.js.map +0 -1
- package/lib/compat/sentry.d.ts +0 -126
- package/lib/compat/sentry.d.ts.map +0 -1
- package/lib/compat/sentry.js +0 -332
- package/lib/compat/sentry.js.map +0 -1
- package/lib/control-channel.d.ts +0 -6
- package/lib/control-channel.d.ts.map +0 -1
- package/lib/control-channel.js +0 -96
- package/lib/control-channel.js.map +0 -1
- package/lib/expo-compat.d.ts +0 -270
- package/lib/expo-compat.d.ts.map +0 -1
- package/lib/expo-compat.js +0 -500
- package/lib/expo-compat.js.map +0 -1
- package/lib/feedback-widget.d.ts +0 -35
- package/lib/feedback-widget.d.ts.map +0 -1
- package/lib/feedback-widget.js +0 -193
- package/lib/feedback-widget.js.map +0 -1
- package/lib/feedback.d.ts +0 -2
- package/lib/feedback.d.ts.map +0 -1
- package/lib/feedback.js +0 -17
- package/lib/feedback.js.map +0 -1
- package/lib/heartbeat.d.ts +0 -9
- package/lib/heartbeat.d.ts.map +0 -1
- package/lib/heartbeat.js +0 -138
- package/lib/heartbeat.js.map +0 -1
- package/lib/index.js.bak +0 -64
- package/lib/install-id.d.ts +0 -17
- package/lib/install-id.d.ts.map +0 -1
- package/lib/install-id.js +0 -125
- package/lib/install-id.js.map +0 -1
- package/lib/lifecycle.d.ts +0 -28
- package/lib/lifecycle.d.ts.map +0 -1
- package/lib/lifecycle.js +0 -73
- package/lib/lifecycle.js.map +0 -1
- package/lib/measure.d.ts +0 -4
- package/lib/measure.d.ts.map +0 -1
- package/lib/measure.js +0 -25
- package/lib/measure.js.map +0 -1
- package/lib/metrics.d.ts +0 -24
- package/lib/metrics.d.ts.map +0 -1
- package/lib/metrics.js +0 -81
- package/lib/metrics.js.map +0 -1
- package/lib/pre-crash-sentinel.d.ts +0 -12
- package/lib/pre-crash-sentinel.d.ts.map +0 -1
- package/lib/pre-crash-sentinel.js +0 -116
- package/lib/pre-crash-sentinel.js.map +0 -1
- package/lib/report-security.d.ts +0 -40
- package/lib/report-security.d.ts.map +0 -1
- package/lib/report-security.js +0 -159
- package/lib/report-security.js.map +0 -1
- package/lib/runtime-metrics-fps.d.ts +0 -19
- package/lib/runtime-metrics-fps.d.ts.map +0 -1
- package/lib/runtime-metrics-fps.js +0 -128
- package/lib/runtime-metrics-fps.js.map +0 -1
- package/lib/runtime-metrics-heap.d.ts +0 -13
- package/lib/runtime-metrics-heap.d.ts.map +0 -1
- package/lib/runtime-metrics-heap.js +0 -67
- package/lib/runtime-metrics-heap.js.map +0 -1
- package/lib/runtime-metrics-network.d.ts +0 -23
- package/lib/runtime-metrics-network.d.ts.map +0 -1
- package/lib/runtime-metrics-network.js +0 -99
- package/lib/runtime-metrics-network.js.map +0 -1
- package/lib/runtime-metrics.d.ts +0 -43
- package/lib/runtime-metrics.d.ts.map +0 -1
- package/lib/runtime-metrics.js +0 -115
- package/lib/runtime-metrics.js.map +0 -1
- package/lib/sample-profiler.d.ts +0 -21
- package/lib/sample-profiler.d.ts.map +0 -1
- package/lib/sample-profiler.js +0 -152
- package/lib/sample-profiler.js.map +0 -1
- package/lib/sentinel-context.d.ts +0 -4
- package/lib/sentinel-context.d.ts.map +0 -1
- package/lib/sentinel-context.js +0 -52
- package/lib/sentinel-context.js.map +0 -1
- package/lib/session-tracker.d.ts +0 -6
- package/lib/session-tracker.d.ts.map +0 -1
- package/lib/session-tracker.js +0 -50
- package/lib/session-tracker.js.map +0 -1
- package/lib/stack.d.ts +0 -3
- package/lib/stack.d.ts.map +0 -1
- package/lib/stack.js +0 -7
- package/lib/stack.js.map +0 -1
- package/lib/state-snapshots.d.ts +0 -33
- package/lib/state-snapshots.d.ts.map +0 -1
- package/lib/state-snapshots.js +0 -109
- package/lib/state-snapshots.js.map +0 -1
- package/lib/track.d.ts +0 -34
- package/lib/track.d.ts.map +0 -1
- package/lib/track.js +0 -107
- package/lib/track.js.map +0 -1
- package/lib/trail.d.ts +0 -12
- package/lib/trail.d.ts.map +0 -1
- package/lib/trail.js +0 -28
- package/lib/trail.js.map +0 -1
- package/lib/trust-score.d.ts +0 -20
- package/lib/trust-score.d.ts.map +0 -1
- package/lib/trust-score.js +0 -151
- package/lib/trust-score.js.map +0 -1
- package/lib/types.d.ts +0 -2
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -4
- package/lib/types.js.map +0 -1
- package/lib/uuid.d.ts +0 -2
- package/lib/uuid.d.ts.map +0 -1
- package/lib/uuid.js +0 -3
- package/lib/uuid.js.map +0 -1
- package/src/__tests__/before-send.test.ts +0 -72
- package/src/__tests__/breadcrumbs.test.ts +0 -44
- package/src/__tests__/capture-replay.test.ts +0 -150
- package/src/__tests__/compat-sentry.test.ts +0 -121
- package/src/__tests__/control-channel.test.ts +0 -111
- package/src/__tests__/install-id.test.ts +0 -60
- package/src/__tests__/long-task-monitor.test.ts +0 -59
- package/src/__tests__/measure.test.ts +0 -45
- package/src/__tests__/navigation.test.ts +0 -175
- package/src/__tests__/push.test.ts +0 -238
- package/src/__tests__/report-security.test.ts +0 -106
- package/src/__tests__/runtime-metrics-instruments.test.ts +0 -109
- package/src/__tests__/runtime-metrics-network.test.ts +0 -78
- package/src/__tests__/sample-profiler.test.ts +0 -59
- package/src/__tests__/stack.test.ts +0 -45
- package/src/__tests__/state-snapshots.test.ts +0 -119
- package/src/__tests__/tracing.test.ts +0 -201
- package/src/__tests__/track.test.ts +0 -91
- package/src/__tests__/uuid.test.ts +0 -41
- package/src/breadcrumbs.ts +0 -57
- package/src/bundle-info.ts +0 -95
- package/src/capture.ts +0 -660
- package/src/compat/sentry.ts +0 -490
- package/src/control-channel.ts +0 -98
- package/src/expo-compat.ts +0 -698
- package/src/feedback-widget.tsx +0 -316
- package/src/feedback.ts +0 -21
- package/src/heartbeat.ts +0 -157
- package/src/install-id.ts +0 -146
- package/src/lifecycle.ts +0 -76
- package/src/measure.ts +0 -28
- package/src/metrics.ts +0 -94
- package/src/pre-crash-sentinel.ts +0 -140
- package/src/report-security.ts +0 -165
- package/src/runtime-metrics-fps.ts +0 -137
- package/src/runtime-metrics-heap.ts +0 -78
- package/src/runtime-metrics-network.ts +0 -103
- package/src/runtime-metrics.ts +0 -122
- package/src/sample-profiler.ts +0 -169
- package/src/sentinel-context.ts +0 -60
- package/src/session-tracker.ts +0 -53
- package/src/stack.ts +0 -10
- package/src/state-snapshots.ts +0 -133
- package/src/track.ts +0 -123
- package/src/trail.ts +0 -32
- package/src/trust-score.ts +0 -176
- package/src/types.ts +0 -21
- 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
|
-
})
|