@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,247 +1,100 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
it,
|
|
4
|
-
expect,
|
|
5
|
-
beforeEach,
|
|
6
|
-
afterEach,
|
|
7
|
-
mock,
|
|
8
|
-
} from 'bun:test';
|
|
1
|
+
// Transport on the v1 wire: batch envelope, assert piggyback,
|
|
2
|
+
// offline persistence hooks.
|
|
9
3
|
|
|
10
|
-
import {
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
11
5
|
|
|
12
|
-
import {
|
|
6
|
+
import { __resetForTests as resetConfig, setConfig } from '../config';
|
|
13
7
|
import {
|
|
8
|
+
__peekAssertStats,
|
|
9
|
+
__peekQueue,
|
|
10
|
+
__resetForTests as resetTransport,
|
|
11
|
+
countAssert,
|
|
14
12
|
enqueue,
|
|
15
13
|
flush,
|
|
16
|
-
flushSpans,
|
|
17
14
|
startTransport,
|
|
18
|
-
__resetForTests as resetTransport,
|
|
19
|
-
__peekQueue,
|
|
20
15
|
} from '../transport';
|
|
21
|
-
import type { Event } from '../types';
|
|
22
16
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
platform: 'javascript',
|
|
28
|
-
release: 'app@1.0.0+1',
|
|
17
|
+
const baseConfig = {
|
|
18
|
+
token: 'st_test',
|
|
19
|
+
ingestUrl: 'http://localhost:18080',
|
|
20
|
+
release: 'app@1.0.0',
|
|
29
21
|
environment: 'test',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
enabled: true,
|
|
23
|
+
detect: { rageTap: true, longFreeze: true, slowColdStart: true, slowApi: false },
|
|
24
|
+
replaySeconds: 30,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const wire = (kind: 'error' | 'trace') => ({
|
|
28
|
+
id: '019e0000-0000-7000-8000-000000000001',
|
|
29
|
+
kind,
|
|
30
|
+
occurredAt: new Date().toISOString(),
|
|
31
|
+
platform: 'javascript' as const,
|
|
32
|
+
payload: {},
|
|
38
33
|
});
|
|
39
34
|
|
|
40
35
|
const originalFetch = globalThis.fetch;
|
|
41
36
|
|
|
42
|
-
describe('transport', () => {
|
|
37
|
+
describe('transport (v1 wire)', () => {
|
|
43
38
|
beforeEach(() => {
|
|
44
|
-
resetConfig();
|
|
45
39
|
resetTransport();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
release: 'app@1.0.0+1',
|
|
49
|
-
environment: 'test',
|
|
50
|
-
ingestUrl: 'http://localhost:8080',
|
|
51
|
-
enabled: true,
|
|
52
|
-
});
|
|
40
|
+
resetConfig();
|
|
41
|
+
setConfig(baseConfig);
|
|
53
42
|
startTransport();
|
|
54
43
|
});
|
|
55
|
-
|
|
56
44
|
afterEach(() => {
|
|
57
45
|
globalThis.fetch = originalFetch;
|
|
46
|
+
resetTransport();
|
|
47
|
+
resetConfig();
|
|
58
48
|
});
|
|
59
49
|
|
|
60
|
-
it('
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
globalThis.fetch = mock(
|
|
68
|
-
async (url: string | URL | Request, init?: RequestInit) => {
|
|
69
|
-
calls.push({ url: String(url), init });
|
|
70
|
-
return new Response(null, { status: 202 });
|
|
71
|
-
},
|
|
72
|
-
) as typeof fetch;
|
|
73
|
-
|
|
74
|
-
enqueue(makeEvent('a'));
|
|
75
|
-
await flush();
|
|
76
|
-
|
|
77
|
-
expect(calls).toHaveLength(1);
|
|
78
|
-
expect(calls[0]?.url).toBe('http://localhost:8080/v1/events');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('flush sends to /v1/events:batch for multiple events', async () => {
|
|
82
|
-
const calls: { url: string; init?: RequestInit }[] = [];
|
|
83
|
-
globalThis.fetch = mock(
|
|
84
|
-
async (url: string | URL | Request, init?: RequestInit) => {
|
|
85
|
-
calls.push({ url: String(url), init });
|
|
86
|
-
return new Response(null, { status: 202 });
|
|
87
|
-
},
|
|
88
|
-
) as typeof fetch;
|
|
89
|
-
|
|
90
|
-
enqueue(makeEvent('a'));
|
|
91
|
-
enqueue(makeEvent('b'));
|
|
92
|
-
await flush();
|
|
93
|
-
|
|
94
|
-
expect(calls).toHaveLength(1);
|
|
95
|
-
expect(calls[0]?.url).toBe('http://localhost:8080/v1/events:batch');
|
|
96
|
-
const body = JSON.parse((calls[0]?.init?.body as string) ?? '{}');
|
|
97
|
-
expect(body.events).toHaveLength(2);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('attaches Authorization and Sentori-Sdk headers', async () => {
|
|
101
|
-
let capturedHeaders: Record<string, string> | undefined;
|
|
102
|
-
globalThis.fetch = mock(
|
|
103
|
-
async (_url: string | URL | Request, init?: RequestInit) => {
|
|
104
|
-
capturedHeaders = init?.headers as Record<string, string>;
|
|
105
|
-
return new Response(null, { status: 202 });
|
|
106
|
-
},
|
|
107
|
-
) as typeof fetch;
|
|
108
|
-
|
|
109
|
-
enqueue(makeEvent('a'));
|
|
110
|
-
await flush();
|
|
111
|
-
|
|
112
|
-
expect(capturedHeaders?.Authorization).toBe('Bearer st_pk_test');
|
|
113
|
-
expect(capturedHeaders?.['Sentori-Sdk']).toMatch(/^react-native\//);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Phase 33 sub-D: offline / retry behavior.
|
|
117
|
-
|
|
118
|
-
it('retries up to MAX_RETRY (3) on a 5xx, then gives up', async () => {
|
|
119
|
-
let attempts = 0;
|
|
120
|
-
globalThis.fetch = mock(async () => {
|
|
121
|
-
attempts++;
|
|
122
|
-
return new Response('boom', { status: 503 });
|
|
123
|
-
}) as typeof fetch;
|
|
124
|
-
|
|
125
|
-
enqueue(makeEvent('a'));
|
|
126
|
-
// flush swallows the final throw (and falls through to persist).
|
|
127
|
-
// We're verifying the retry count, not the throw.
|
|
128
|
-
await flush();
|
|
129
|
-
expect(attempts).toBe(3);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('retries on network error (fetch throw), succeeds when recovered', async () => {
|
|
133
|
-
let attempts = 0;
|
|
134
|
-
globalThis.fetch = mock(async () => {
|
|
135
|
-
attempts++;
|
|
136
|
-
if (attempts < 3) throw new TypeError('NetworkError: offline');
|
|
137
|
-
return new Response(null, { status: 202 });
|
|
138
|
-
}) as typeof fetch;
|
|
139
|
-
|
|
140
|
-
enqueue(makeEvent('a'));
|
|
141
|
-
await flush();
|
|
142
|
-
expect(attempts).toBe(3);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('drops 4xx-other-than-429 without retry (client errors are unrecoverable)', async () => {
|
|
146
|
-
let attempts = 0;
|
|
147
|
-
globalThis.fetch = mock(async () => {
|
|
148
|
-
attempts++;
|
|
149
|
-
return new Response(null, { status: 400 });
|
|
50
|
+
it('posts the batch envelope to /v1/events:batch', async () => {
|
|
51
|
+
let seenUrl = '';
|
|
52
|
+
let seenBody: unknown;
|
|
53
|
+
globalThis.fetch = mock(async (url: unknown, init?: RequestInit) => {
|
|
54
|
+
seenUrl = String(url);
|
|
55
|
+
seenBody = JSON.parse(String(init?.body));
|
|
56
|
+
return new Response(JSON.stringify({ accepted: 1, outcomes: [] }), { status: 200 });
|
|
150
57
|
}) as typeof fetch;
|
|
151
58
|
|
|
152
|
-
enqueue(
|
|
59
|
+
enqueue(wire('error'));
|
|
153
60
|
await flush();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
expect(attempts).toBe(1);
|
|
61
|
+
expect(seenUrl).toBe('http://localhost:18080/v1/events:batch');
|
|
62
|
+
expect((seenBody as { events: unknown[] }).events.length).toBe(1);
|
|
157
63
|
});
|
|
158
64
|
|
|
159
|
-
it('
|
|
160
|
-
let
|
|
161
|
-
globalThis.fetch = mock(async () => {
|
|
162
|
-
|
|
163
|
-
return new Response(
|
|
65
|
+
it('piggybacks assert stats on the envelope and clears them', async () => {
|
|
66
|
+
let seenBody: { assertStats?: unknown[] } = {};
|
|
67
|
+
globalThis.fetch = mock(async (_url: unknown, init?: RequestInit) => {
|
|
68
|
+
seenBody = JSON.parse(String(init?.body));
|
|
69
|
+
return new Response(JSON.stringify({ accepted: 0, outcomes: [] }), { status: 200 });
|
|
164
70
|
}) as typeof fetch;
|
|
165
71
|
|
|
166
|
-
|
|
167
|
-
|
|
72
|
+
countAssert('total-positive', true, 'app@1.0.0');
|
|
73
|
+
countAssert('total-positive', true, 'app@1.0.0');
|
|
74
|
+
countAssert('total-positive', false, 'app@1.0.0');
|
|
75
|
+
expect(__peekAssertStats().length).toBe(1);
|
|
168
76
|
await flush();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
77
|
+
const stats = seenBody.assertStats as Array<{
|
|
78
|
+
name: string;
|
|
79
|
+
passDelta: number;
|
|
80
|
+
failDelta: number;
|
|
81
|
+
}>;
|
|
82
|
+
expect(stats.length).toBe(1);
|
|
83
|
+
expect(stats[0]?.passDelta).toBe(2);
|
|
84
|
+
expect(stats[0]?.failDelta).toBe(1);
|
|
85
|
+
expect(__peekAssertStats().length).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('never flushes before startTransport', async () => {
|
|
178
89
|
resetTransport();
|
|
179
|
-
|
|
180
|
-
setConfig({
|
|
181
|
-
token: 'st_pk_test',
|
|
182
|
-
release: 'app@1.0.0+1',
|
|
183
|
-
environment: 'test',
|
|
184
|
-
ingestUrl: 'http://localhost:8080',
|
|
185
|
-
enabled: true,
|
|
186
|
-
});
|
|
187
|
-
startTransport();
|
|
188
|
-
});
|
|
189
|
-
afterEach(() => {
|
|
190
|
-
globalThis.fetch = originalFetch;
|
|
191
|
-
clearSpans();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('POSTs buffered spans to /v1/spans:batch', async () => {
|
|
195
|
-
const calls: { url: string; init?: RequestInit }[] = [];
|
|
196
|
-
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
197
|
-
calls.push({ url: String(url), init });
|
|
198
|
-
return new Response(null, { status: 202 });
|
|
199
|
-
}) as typeof fetch;
|
|
200
|
-
|
|
201
|
-
startSpan('http.client', { name: 'GET /a' }).finish({ status: 'ok' });
|
|
202
|
-
startSpan('http.client', { name: 'GET /b' }).finish({ status: 'error' });
|
|
203
|
-
await flushSpans();
|
|
204
|
-
|
|
205
|
-
expect(calls).toHaveLength(1);
|
|
206
|
-
expect(calls[0]?.url).toBe('http://localhost:8080/v1/spans:batch');
|
|
207
|
-
const body = JSON.parse((calls[0]?.init?.body as string) ?? '{}');
|
|
208
|
-
expect(body.spans).toHaveLength(2);
|
|
209
|
-
expect(body.spans[0]).toMatchObject({ op: 'http.client', name: 'GET /a', status: 'ok' });
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('no-op when the span buffer is empty', async () => {
|
|
213
|
-
let attempts = 0;
|
|
90
|
+
let called = 0;
|
|
214
91
|
globalThis.fetch = mock(async () => {
|
|
215
|
-
|
|
216
|
-
return new Response(
|
|
217
|
-
}) as typeof fetch;
|
|
218
|
-
await flushSpans();
|
|
219
|
-
expect(attempts).toBe(0);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('splits >200 spans into multiple batches', async () => {
|
|
223
|
-
const sizes: number[] = [];
|
|
224
|
-
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
|
|
225
|
-
const body = JSON.parse((init?.body as string) ?? '{}');
|
|
226
|
-
sizes.push(body.spans.length);
|
|
227
|
-
return new Response(null, { status: 202 });
|
|
92
|
+
called += 1;
|
|
93
|
+
return new Response('{}', { status: 200 });
|
|
228
94
|
}) as typeof fetch;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
expect(sizes).toEqual([200, 200, 50]);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('drops on 5xx without retrying', async () => {
|
|
237
|
-
let attempts = 0;
|
|
238
|
-
globalThis.fetch = mock(async () => {
|
|
239
|
-
attempts++;
|
|
240
|
-
return new Response('boom', { status: 503 });
|
|
241
|
-
}) as typeof fetch;
|
|
242
|
-
|
|
243
|
-
startSpan('http.client', { name: 'GET /a' }).finish();
|
|
244
|
-
await flushSpans();
|
|
245
|
-
expect(attempts).toBe(1);
|
|
95
|
+
enqueue(wire('trace'));
|
|
96
|
+
await flush();
|
|
97
|
+
expect(called).toBe(0);
|
|
98
|
+
expect(__peekQueue().length).toBe(1);
|
|
246
99
|
});
|
|
247
100
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The five verbs: synchronous, never throw, id out, correct wire.
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
4
|
+
|
|
5
|
+
import { clearSignals } from '@goliapkg/sentori-core';
|
|
6
|
+
|
|
7
|
+
import { __resetForTests as resetConfig, setConfig } from '../config';
|
|
8
|
+
import { __resetForTests as resetScope } from '../scope';
|
|
9
|
+
import {
|
|
10
|
+
__peekAssertStats,
|
|
11
|
+
__peekQueue,
|
|
12
|
+
__resetForTests as resetTransport,
|
|
13
|
+
startTransport,
|
|
14
|
+
} from '../transport';
|
|
15
|
+
import { verbs } from '../verbs';
|
|
16
|
+
|
|
17
|
+
const baseConfig = {
|
|
18
|
+
token: 'st_test',
|
|
19
|
+
ingestUrl: 'http://localhost:18080',
|
|
20
|
+
release: 'app@1.0.0',
|
|
21
|
+
environment: 'test',
|
|
22
|
+
enabled: true,
|
|
23
|
+
detect: { rageTap: true, longFreeze: true, slowColdStart: true, slowApi: false },
|
|
24
|
+
replaySeconds: 30,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('the five verbs', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
resetTransport();
|
|
30
|
+
resetConfig();
|
|
31
|
+
resetScope();
|
|
32
|
+
clearSignals();
|
|
33
|
+
setConfig(baseConfig);
|
|
34
|
+
startTransport();
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
resetTransport();
|
|
38
|
+
resetConfig();
|
|
39
|
+
resetScope();
|
|
40
|
+
clearSignals();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('error coerces anything and enqueues with a stack', () => {
|
|
44
|
+
const id = verbs.error(new Error('boom'));
|
|
45
|
+
expect(typeof id).toBe('string');
|
|
46
|
+
const q = __peekQueue();
|
|
47
|
+
expect(q.length).toBe(1);
|
|
48
|
+
expect(q[0]?.kind).toBe('error');
|
|
49
|
+
expect(q[0]?.payload.error?.type).toBe('Error');
|
|
50
|
+
expect(q[0]?.payload.error?.message).toBe('boom');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('error serializes Error instances found in data (error-in-data)', () => {
|
|
54
|
+
verbs.warn('pay.retry', { attempt: 2, error: new TypeError('nope') });
|
|
55
|
+
const q = __peekQueue();
|
|
56
|
+
const data = q[0]?.payload.data as { error?: { type?: string } };
|
|
57
|
+
expect(data.error?.type).toBe('TypeError');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('warn carries name; trace quiet stays out of the queue', () => {
|
|
61
|
+
verbs.warn('rage_tap');
|
|
62
|
+
expect(__peekQueue().length).toBe(1);
|
|
63
|
+
verbs.trace('checkout.start', undefined, { quiet: true });
|
|
64
|
+
expect(__peekQueue().length).toBe(1); // unchanged
|
|
65
|
+
verbs.trace('checkout.start');
|
|
66
|
+
expect(__peekQueue().length).toBe(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('assert pass aggregates without an event; fail is an event', () => {
|
|
70
|
+
verbs.assert('total-positive', true);
|
|
71
|
+
expect(__peekQueue().length).toBe(0);
|
|
72
|
+
expect(__peekAssertStats()[0]?.passDelta).toBe(1);
|
|
73
|
+
verbs.assert('total-positive', false);
|
|
74
|
+
expect(__peekQueue().length).toBe(1);
|
|
75
|
+
expect(__peekQueue()[0]?.kind).toBe('assert');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('probe never throws and reports the ref as name', () => {
|
|
79
|
+
const id = verbs.probe('SENT-123');
|
|
80
|
+
expect(typeof id).toBe('string');
|
|
81
|
+
expect(__peekQueue()[0]?.kind).toBe('probe');
|
|
82
|
+
expect(__peekQueue()[0]?.name).toBe('SENT-123');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('before init everything is a silent no-op that still returns an id', () => {
|
|
86
|
+
resetConfig();
|
|
87
|
+
const id = verbs.error(new Error('x'));
|
|
88
|
+
expect(typeof id).toBe('string');
|
|
89
|
+
expect(__peekQueue().length).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -1,58 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
// SDK-internal runtime config, set once by init(). Everything else
|
|
2
|
+
// reads through getConfig() and treats null as "not initialized →
|
|
3
|
+
// every verb is a no-op" (the failure-isolation iron rule).
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
* v2.3 — `ReadyInfo` is shared across SDKs via `@goliapkg/sentori-core`
|
|
5
|
-
* so a host that switches from web to RN reads the same shape. The RN
|
|
6
|
-
* SDK always populates `native` + `coldStartMs`; the core type marks
|
|
7
|
-
* both optional for the web SDK's benefit (web has no native module).
|
|
8
|
-
*/
|
|
9
|
-
export type { ReadyInfo };
|
|
5
|
+
import type { LogLevel, WireEvent } from '@goliapkg/sentori-core';
|
|
10
6
|
|
|
11
7
|
export type Config = {
|
|
12
8
|
token: string;
|
|
9
|
+
ingestUrl: string;
|
|
13
10
|
release: string;
|
|
14
11
|
environment: string;
|
|
15
|
-
ingestUrl: string;
|
|
16
12
|
enabled: boolean;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
sessionTrailEnabled: boolean;
|
|
30
|
-
/** v2.0 W3 — when true, every `track(name, props)` call also
|
|
31
|
-
* pushes a `{ type: 'track', data: { name, props } }` breadcrumb
|
|
32
|
-
* so a subsequent capture carries the customer journey. Defaults
|
|
33
|
-
* to false to preserve v1 customer breadcrumb shape on upgrade. */
|
|
34
|
-
trackAutoBreadcrumb: boolean;
|
|
35
|
-
/** v2.3 — Sentori console output gate.
|
|
36
|
-
*
|
|
37
|
-
* Default `warn`: SDK is silent on host's console unless
|
|
38
|
-
* something is genuinely broken (transport sustained failure,
|
|
39
|
-
* native module not found, internal SDK exception). No
|
|
40
|
-
* per-tick / per-init / per-breadcrumb noise.
|
|
41
|
-
*
|
|
42
|
-
* Set `'silent'` for absolute silence (e.g. CI smoke runs);
|
|
43
|
-
* set `'info'` or `'debug'` when debugging Sentori itself. */
|
|
13
|
+
/** Warn-scenario auto-detection switches (conservative defaults). */
|
|
14
|
+
detect: {
|
|
15
|
+
rageTap: boolean;
|
|
16
|
+
longFreeze: boolean;
|
|
17
|
+
slowColdStart: boolean;
|
|
18
|
+
slowApi: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** B-type replay rolling buffer, seconds. 0 disables. */
|
|
21
|
+
replaySeconds: number;
|
|
22
|
+
replayScreens: boolean;
|
|
23
|
+
/** Sentori console output gate. Default `warn`: silent on the
|
|
24
|
+
* host's console unless something is genuinely broken. */
|
|
44
25
|
logLevel?: LogLevel;
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
* wraps any host-side logging here. */
|
|
49
|
-
onReady?: (info: ReadyInfo) => void;
|
|
50
|
-
/** v2.3 — host-side mutate-or-drop hook called once per event
|
|
51
|
-
* just before transport enqueue. Return the event to send it,
|
|
52
|
-
* `null` to drop. Sync only. If the hook throws or returns a
|
|
53
|
-
* non-event, SDK falls back to the un-mutated event and emits
|
|
54
|
-
* one one-shot warn. */
|
|
55
|
-
beforeSend?: BeforeSendHook;
|
|
26
|
+
/** Host-side mutate-or-drop hook, sync only. Throw / bad return →
|
|
27
|
+
* the un-mutated event ships and one warn is emitted. */
|
|
28
|
+
beforeSend?: (event: WireEvent) => WireEvent | null;
|
|
56
29
|
};
|
|
57
30
|
|
|
58
31
|
let _config: Config | null = null;
|
package/src/device.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Synchronous device snapshot — what the JS layer can know without
|
|
2
|
+
// crossing the bridge. The native module enriches asynchronously via
|
|
3
|
+
// setNativeDeviceInfo at init; until then the JS-visible subset
|
|
4
|
+
// ships (an event with a thin device beats a delayed event).
|
|
5
|
+
|
|
6
|
+
import type { Device } from '@goliapkg/sentori-core';
|
|
7
|
+
|
|
8
|
+
let _native: Partial<Device> | null = null;
|
|
9
|
+
|
|
10
|
+
export const setNativeDeviceInfo = (info: Partial<Device>): void => {
|
|
11
|
+
_native = info;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const collectDevice = (): Device | undefined => {
|
|
15
|
+
let base: Device | undefined;
|
|
16
|
+
try {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
|
+
const RN = require('react-native') as {
|
|
19
|
+
Platform?: { OS?: string; Version?: number | string };
|
|
20
|
+
Dimensions?: { get(dim: string): { width: number; height: number; scale: number } };
|
|
21
|
+
};
|
|
22
|
+
const os = RN.Platform?.OS ?? 'unknown';
|
|
23
|
+
base = { os, osVersion: RN.Platform?.Version?.toString() };
|
|
24
|
+
const win = RN.Dimensions?.get('window');
|
|
25
|
+
if (win) {
|
|
26
|
+
base.screen = { width: win.width, height: win.height, scale: win.scale };
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return _native ? ({ os: 'unknown', ..._native } as Device) : undefined;
|
|
30
|
+
}
|
|
31
|
+
return _native ? { ...base, ..._native } : base;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const __resetForTests = (): void => {
|
|
35
|
+
_native = null;
|
|
36
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Post-emit hooks — how B-type replay (and future reactions) learn
|
|
2
|
+
// "an event just went out" without the verbs knowing about them.
|
|
3
|
+
//
|
|
4
|
+
// Hooks run inside their own try/catch: a broken subscriber must
|
|
5
|
+
// never take the emit path down with it (failure isolation).
|
|
6
|
+
|
|
7
|
+
import type { WireEvent } from '@goliapkg/sentori-core';
|
|
8
|
+
import { reportInternal } from '@goliapkg/sentori-core';
|
|
9
|
+
|
|
10
|
+
type EmitHook = (event: WireEvent) => void;
|
|
11
|
+
|
|
12
|
+
const hooks: EmitHook[] = [];
|
|
13
|
+
|
|
14
|
+
export const registerEmitHook = (hook: EmitHook): void => {
|
|
15
|
+
hooks.push(hook);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const onEventEmitted = (event: WireEvent): void => {
|
|
19
|
+
for (const hook of hooks) {
|
|
20
|
+
try {
|
|
21
|
+
hook(event);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
reportInternal('emit-hook', e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const __resetForTests = (): void => {
|
|
29
|
+
hooks.length = 0;
|
|
30
|
+
};
|
package/src/error-boundary.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
-
import { captureError } from
|
|
3
|
+
import { error as captureError } from "./verbs";
|
|
4
4
|
|
|
5
5
|
export type ErrorBoundaryProps = {
|
|
6
6
|
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Best-effort and dev-only: any failure (Metro not running, timeout,
|
|
8
8
|
// bad response) leaves the stack untouched. Never throws.
|
|
9
9
|
|
|
10
|
-
import type { Frame, SentoriError } from '
|
|
10
|
+
import type { Frame, SentoriError } from '@goliapkg/sentori-core';
|
|
11
11
|
|
|
12
12
|
const TIMEOUT_MS = 2000;
|
|
13
13
|
|
|
@@ -132,6 +132,7 @@ export async function symbolicateErrorViaMetro(
|
|
|
132
132
|
err: SentoriError,
|
|
133
133
|
opts: { url?: string } = {},
|
|
134
134
|
): Promise<void> {
|
|
135
|
+
if (!err.stack) return;
|
|
135
136
|
const sym = await symbolicateStackViaMetro(err.stack, opts);
|
|
136
137
|
if (sym) err.stack = sym;
|
|
137
138
|
if (err.cause) await symbolicateErrorViaMetro(err.cause, opts);
|
package/src/handlers/global.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* twice". Sentry historically did the opposite (treat inactive as
|
|
13
13
|
* still alive), but that lets a swiped-away session never end.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { pushSignal } from '@goliapkg/sentori-core';
|
|
16
16
|
|
|
17
17
|
let _installed = false;
|
|
18
18
|
let _subscription: { remove: () => void } | null = null;
|
|
@@ -40,9 +40,9 @@ export const installLifecycleHandler = (): void => {
|
|
|
40
40
|
|
|
41
41
|
_subscription = AppState.addEventListener('change', (state) => {
|
|
42
42
|
if (state === 'active') {
|
|
43
|
-
|
|
43
|
+
pushSignal('lifecycle', { state: 'active' });
|
|
44
44
|
} else if (state === 'background' || state === 'inactive') {
|
|
45
|
-
|
|
45
|
+
pushSignal('lifecycle', { state: 'background' });
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
};
|