@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
package/src/long-task-monitor.ts
CHANGED
|
@@ -1,57 +1,39 @@
|
|
|
1
|
-
//
|
|
1
|
+
// long_freeze — detected warn scenario (design.md §3, category B:
|
|
2
|
+
// 「app 卡住了 / 死了几秒」), JS-thread side.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Mini-spec: a setInterval(50 ms) tick measures wall-clock drift.
|
|
5
|
+
// Drift ≤ 2 s ⇒ a `freeze` signal in the ring (context only —
|
|
6
|
+
// brief jank is not worth an event). Drift > 2 s ⇒ one `warn`
|
|
7
|
+
// event, scenario `long_freeze`, surface = current screen, with the
|
|
8
|
+
// blocked duration. Rate-limited to 6 events/minute so a freeze
|
|
9
|
+
// storm cannot flood ingest.
|
|
9
10
|
//
|
|
10
|
-
// What it
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// profiler but accessing it from JS requires RN-internal bridges
|
|
17
|
-
// that vary per RN minor. We could swizzle / vendor headers but
|
|
18
|
-
// that's an Insight-build-config burden. The long-task monitor
|
|
19
|
-
// gets ~80% of the practical value (find slow renders, expensive
|
|
20
|
-
// reducers, accidental sync work in render path) with zero native
|
|
21
|
-
// code + zero RN-version sensitivity.
|
|
22
|
-
//
|
|
23
|
-
// Pairs naturally with +S4 (pre-crash sentinel, RAF frame budget,
|
|
24
|
-
// fires at the slow-frame threshold) — long-task monitor fires
|
|
25
|
-
// further down the slowness scale.
|
|
11
|
+
// What it cannot do: capture the stack DURING the block — JS is
|
|
12
|
+
// single-threaded; by the time our tick runs the busy code is gone.
|
|
13
|
+
// The native watchdogs (HangWatchdog / AnrWatchdog) cover the
|
|
14
|
+
// UI-thread side of the same scenario.
|
|
15
|
+
|
|
16
|
+
import { pushSignal } from '@goliapkg/sentori-core';
|
|
26
17
|
|
|
27
|
-
import {
|
|
18
|
+
import { currentScreen } from './navigation';
|
|
19
|
+
import { warnDetected } from './verbs';
|
|
28
20
|
|
|
29
21
|
const TICK_INTERVAL_MS = 50;
|
|
30
|
-
const
|
|
31
|
-
const
|
|
22
|
+
const SIGNAL_THRESHOLD_MS = 200; // ring entry: noticeable jank
|
|
23
|
+
const WARN_THRESHOLD_MS = 2_000; // warn event: a real freeze
|
|
24
|
+
const MAX_WARNS_PER_MIN = 6;
|
|
32
25
|
|
|
33
26
|
let _timer: ReturnType<typeof setInterval> | null = null;
|
|
34
27
|
let _lastTick = 0;
|
|
35
|
-
let
|
|
36
|
-
let
|
|
28
|
+
let _warnWindowStart = 0;
|
|
29
|
+
let _warnsThisWindow = 0;
|
|
37
30
|
|
|
38
|
-
export
|
|
39
|
-
enabled: boolean;
|
|
40
|
-
/** Threshold ms above which a tick lag becomes a longtask span.
|
|
41
|
-
* Default 200ms. Lower → noisier. */
|
|
42
|
-
thresholdMs?: number;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export function startLongTaskMonitor(opts: LongTaskMonitorOptions): void {
|
|
31
|
+
export function startLongTaskMonitor(): void {
|
|
46
32
|
if (_timer !== null) return;
|
|
47
|
-
if (!opts.enabled) return;
|
|
48
|
-
const threshold = opts.thresholdMs ?? LONGTASK_THRESHOLD_MS;
|
|
49
33
|
_lastTick = Date.now();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
_timer = setInterval(
|
|
53
|
-
tick(threshold);
|
|
54
|
-
}, TICK_INTERVAL_MS);
|
|
34
|
+
_warnWindowStart = _lastTick;
|
|
35
|
+
_warnsThisWindow = 0;
|
|
36
|
+
_timer = setInterval(tick, TICK_INTERVAL_MS);
|
|
55
37
|
(_timer as unknown as { unref?: () => void }).unref?.();
|
|
56
38
|
}
|
|
57
39
|
|
|
@@ -62,38 +44,34 @@ export function stopLongTaskMonitor(): void {
|
|
|
62
44
|
}
|
|
63
45
|
}
|
|
64
46
|
|
|
65
|
-
function tick(
|
|
47
|
+
function tick(): void {
|
|
66
48
|
const now = Date.now();
|
|
67
49
|
const elapsed = now - _lastTick;
|
|
68
50
|
_lastTick = now;
|
|
69
51
|
const lag = elapsed - TICK_INTERVAL_MS;
|
|
70
|
-
if (lag <=
|
|
52
|
+
if (lag <= SIGNAL_THRESHOLD_MS) return;
|
|
53
|
+
|
|
54
|
+
pushSignal('freeze', { blockedMs: Math.round(lag) });
|
|
55
|
+
if (lag <= WARN_THRESHOLD_MS) return;
|
|
71
56
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
_emitWindowStart = now;
|
|
76
|
-
_emitsThisWindow = 0;
|
|
57
|
+
if (now - _warnWindowStart >= 60_000) {
|
|
58
|
+
_warnWindowStart = now;
|
|
59
|
+
_warnsThisWindow = 0;
|
|
77
60
|
}
|
|
78
|
-
if (
|
|
79
|
-
|
|
61
|
+
if (_warnsThisWindow >= MAX_WARNS_PER_MIN) return;
|
|
62
|
+
_warnsThisWindow += 1;
|
|
80
63
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
'profile.duration_ms': String(Math.round(lag)),
|
|
87
|
-
'profile.tick_interval_ms': String(TICK_INTERVAL_MS),
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
span.finish({ endNowMs: now, status: 'ok' });
|
|
64
|
+
warnDetected(
|
|
65
|
+
'long_freeze',
|
|
66
|
+
{ screen: currentScreen() },
|
|
67
|
+
{ blockedMs: Math.round(lag), thread: 'js' },
|
|
68
|
+
);
|
|
91
69
|
}
|
|
92
70
|
|
|
93
71
|
/** Test-only. */
|
|
94
72
|
export function __resetLongTaskMonitorForTests(): void {
|
|
95
73
|
stopLongTaskMonitor();
|
|
96
74
|
_lastTick = 0;
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
_warnWindowStart = 0;
|
|
76
|
+
_warnsThisWindow = 0;
|
|
99
77
|
}
|
package/src/mask.ts
CHANGED
|
@@ -1,41 +1,39 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Screen masking — the privacy half of visual replay.
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// The host registers a query returning the `nativeID`s of views
|
|
4
|
+
// that must never appear in a screenshot (camera feeds, user
|
|
5
|
+
// identity, payment fields). Native paints black rectangles over
|
|
6
|
+
// those subtrees in the same render pass, so the pixels never
|
|
7
|
+
// leave the device.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// affect rendering.
|
|
9
|
+
// The query runs on every frame tick; keep it cheap (return a
|
|
10
|
+
// cached array). A throwing query is swallowed and masks NOTHING
|
|
11
|
+
// that tick — so a broken query fails visible-in-review rather
|
|
12
|
+
// than silently, and can never take the capture path down.
|
|
13
|
+
|
|
14
|
+
import { reportInternal } from '@goliapkg/sentori-core';
|
|
16
15
|
|
|
17
16
|
type MaskQuery = () => string[];
|
|
18
17
|
|
|
19
18
|
let _query: MaskQuery | null = null;
|
|
20
19
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
* capture. Return the native-IDs (the `nativeID` prop on the RN
|
|
24
|
-
* `<View>`) that should be blacked-out in the captured image.
|
|
25
|
-
*
|
|
26
|
-
* Idempotent: a second call replaces the first. Pass `null` (or
|
|
27
|
-
* call `clearMaskQuery`) to detach.
|
|
28
|
-
*/
|
|
29
|
-
export function registerMaskQuery(query: MaskQuery): void {
|
|
20
|
+
/** Register (or with `null`, clear) the mask query. */
|
|
21
|
+
export const registerMaskQuery = (query: MaskQuery | null): void => {
|
|
30
22
|
_query = query;
|
|
31
|
-
}
|
|
23
|
+
};
|
|
32
24
|
|
|
33
|
-
/**
|
|
34
|
-
export
|
|
35
|
-
_query
|
|
36
|
-
|
|
25
|
+
/** The current mask list; empty when unregistered or throwing. */
|
|
26
|
+
export const maskedNativeIds = (): string[] => {
|
|
27
|
+
if (!_query) return [];
|
|
28
|
+
try {
|
|
29
|
+
const ids = _query();
|
|
30
|
+
return Array.isArray(ids) ? ids.filter((x) => typeof x === 'string') : [];
|
|
31
|
+
} catch (e) {
|
|
32
|
+
reportInternal('mask-query', e);
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
};
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
37
|
+
export const __resetForTests = (): void => {
|
|
38
|
+
_query = null;
|
|
39
|
+
};
|
package/src/mobile-vitals.ts
CHANGED
|
@@ -1,34 +1,25 @@
|
|
|
1
|
-
//
|
|
1
|
+
// slow_cold_start — detected warn scenario (design.md §3, category
|
|
2
|
+
// C: 「启动过慢」).
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// • TTFD (Time-To-Full-Display): manual. Host calls
|
|
13
|
-
// sentori.markTimeToFullDisplay('Home').end() when the screen's
|
|
14
|
-
// data has loaded.
|
|
15
|
-
// • Slow / frozen frame counts: native side hooks CADisplayLink /
|
|
16
|
-
// Choreographer.FrameCallback; counters flush per navigation
|
|
17
|
-
// span.
|
|
18
|
-
//
|
|
19
|
-
// JS-side first: TTFD API + bundle-level vital captures. Native
|
|
20
|
-
// pieces ship as a separate native module method `getColdStartMs`
|
|
21
|
-
// + `getFrameCounts()` — graceful no-op if not linked.
|
|
4
|
+
// Mini-spec: the native side measures launch → JS-ready (iOS
|
|
5
|
+
// mach_absolute_time / Android Process.getStartElapsedRealtime);
|
|
6
|
+
// init() calls checkColdStart() once. > 3 s ⇒ one `warn` event,
|
|
7
|
+
// scenario `slow_cold_start`, with the measured ms. Always pushes a
|
|
8
|
+
// `lifecycle` signal so even a fast start shows on the timeline.
|
|
9
|
+
// Graceful no-op when the native module isn't linked (Expo Go,
|
|
10
|
+
// tests).
|
|
11
|
+
|
|
12
|
+
import { pushSignal } from '@goliapkg/sentori-core';
|
|
22
13
|
|
|
23
|
-
import {
|
|
14
|
+
import { getNativeColdStartMs } from './native';
|
|
15
|
+
import { warnDetected } from './verbs';
|
|
24
16
|
|
|
25
|
-
|
|
17
|
+
const SLOW_COLD_START_MS = 3_000;
|
|
26
18
|
|
|
27
19
|
let _coldStartMs: null | number = null;
|
|
28
20
|
let _coldStartCaptured = false;
|
|
29
21
|
|
|
30
|
-
/** Read the native-side cold start measurement once. Cached.
|
|
31
|
-
* null when the native module isn't linked (Expo Go / tests). */
|
|
22
|
+
/** Read the native-side cold start measurement once. Cached. */
|
|
32
23
|
export function getColdStartMs(): null | number {
|
|
33
24
|
if (_coldStartCaptured) return _coldStartMs;
|
|
34
25
|
_coldStartCaptured = true;
|
|
@@ -40,69 +31,18 @@ export function getColdStartMs(): null | number {
|
|
|
40
31
|
return _coldStartMs;
|
|
41
32
|
}
|
|
42
33
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* // ...data fetched, images rendered...
|
|
51
|
-
* h.end();
|
|
52
|
-
*
|
|
53
|
-
* If `.end()` is never called, the handle's span is finished as
|
|
54
|
-
* `cancelled` at the next `markTimeToFullDisplay` call (one TTFD in
|
|
55
|
-
* flight at a time is the typical case).
|
|
56
|
-
*/
|
|
57
|
-
export type TimeToFullDisplayHandle = {
|
|
58
|
-
end: (opts?: { status?: 'cancelled' | 'error' | 'ok' }) => void;
|
|
59
|
-
cancel: () => void;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
let _activeTtfd: null | {
|
|
63
|
-
finish: (status: 'cancelled' | 'error' | 'ok') => void;
|
|
64
|
-
route: string;
|
|
65
|
-
} = null;
|
|
66
|
-
|
|
67
|
-
export function markTimeToFullDisplay(route: string): TimeToFullDisplayHandle {
|
|
68
|
-
if (_activeTtfd && _activeTtfd.route !== route) {
|
|
69
|
-
_activeTtfd.finish('cancelled');
|
|
70
|
-
}
|
|
71
|
-
const span = startSpan('react.navigation.ttfd', {
|
|
72
|
-
name: route,
|
|
73
|
-
tags: { 'nav.route': route, 'vital.kind': 'ttfd' },
|
|
74
|
-
});
|
|
75
|
-
let finished = false;
|
|
76
|
-
const finish = (status: 'cancelled' | 'error' | 'ok'): void => {
|
|
77
|
-
if (finished) return;
|
|
78
|
-
finished = true;
|
|
79
|
-
span.finish({ status });
|
|
80
|
-
if (_activeTtfd && _activeTtfd.route === route) _activeTtfd = null;
|
|
81
|
-
};
|
|
82
|
-
_activeTtfd = { finish, route };
|
|
83
|
-
return {
|
|
84
|
-
cancel: () => finish('cancelled'),
|
|
85
|
-
end: (opts?: { status?: 'cancelled' | 'error' | 'ok' }) =>
|
|
86
|
-
finish(opts?.status ?? 'ok'),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** v0.9.4 #1 — read the per-screen slow/frozen frame counts since
|
|
91
|
-
* the most recent navigation transition. Native module reads
|
|
92
|
-
* CADisplayLink / Choreographer counters; returns null when not
|
|
93
|
-
* linked. */
|
|
94
|
-
export function getFrameCounters(): null | { slow: number; frozen: number } {
|
|
95
|
-
try {
|
|
96
|
-
return getNativeFrameCounters();
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
34
|
+
/** Called once from init(): signal always, warn when slow. */
|
|
35
|
+
export function checkColdStart(detectEnabled: boolean): void {
|
|
36
|
+
const ms = getColdStartMs();
|
|
37
|
+
if (ms === null) return;
|
|
38
|
+
pushSignal('lifecycle', { phase: 'cold_start', ms });
|
|
39
|
+
if (detectEnabled && ms > SLOW_COLD_START_MS) {
|
|
40
|
+
warnDetected('slow_cold_start', {}, { coldStartMs: ms, thresholdMs: SLOW_COLD_START_MS });
|
|
99
41
|
}
|
|
100
42
|
}
|
|
101
43
|
|
|
102
44
|
/** Test-only. */
|
|
103
45
|
export function __resetMobileVitalsForTests(): void {
|
|
104
|
-
if (_activeTtfd) _activeTtfd.finish('cancelled');
|
|
105
|
-
_activeTtfd = null;
|
|
106
46
|
_coldStartCaptured = false;
|
|
107
47
|
_coldStartMs = null;
|
|
108
48
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Native pending-crash drain: the crash handlers (Swift/Kotlin)
|
|
2
|
+
// write one JSON file per crash in the legacy event shape; this
|
|
3
|
+
// module converts them to the v1 wire and enqueues.
|
|
4
|
+
//
|
|
5
|
+
// The converter lives on the JS side so the native writers stay
|
|
6
|
+
// untouched — native code doesn't compile in preflight/CI, and a
|
|
7
|
+
// silent .kt/.swift break ships to npm (the v3.0.0 K2 lesson).
|
|
8
|
+
|
|
9
|
+
import { uuidV7 } from '@goliapkg/sentori-core';
|
|
10
|
+
import type { SentoriError, WireEvent } from '@goliapkg/sentori-core';
|
|
11
|
+
|
|
12
|
+
import { drainNativePending } from './native';
|
|
13
|
+
import { enqueue, flush, uploadAttachment } from './transport';
|
|
14
|
+
|
|
15
|
+
type LegacyNativeEvent = {
|
|
16
|
+
id?: string;
|
|
17
|
+
timestamp?: string;
|
|
18
|
+
platform?: string;
|
|
19
|
+
release?: string;
|
|
20
|
+
environment?: string;
|
|
21
|
+
device?: Record<string, unknown>;
|
|
22
|
+
app?: Record<string, unknown>;
|
|
23
|
+
error?: {
|
|
24
|
+
type?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
stack?: Array<Record<string, unknown>>;
|
|
27
|
+
};
|
|
28
|
+
_pendingAttachments?: Array<{
|
|
29
|
+
kind?: string;
|
|
30
|
+
base64?: string;
|
|
31
|
+
mediaType?: string;
|
|
32
|
+
source?: string;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const toWire = (raw: LegacyNativeEvent): WireEvent => {
|
|
37
|
+
const platform = raw.platform === 'android' ? 'android' : 'ios';
|
|
38
|
+
const error: SentoriError = {
|
|
39
|
+
type: raw.error?.type ?? 'NativeCrash',
|
|
40
|
+
message: raw.error?.message ?? '',
|
|
41
|
+
stack: (raw.error?.stack ?? []).map((f) => ({
|
|
42
|
+
file: typeof f.file === 'string' ? f.file : undefined,
|
|
43
|
+
function: typeof f.function === 'string' ? f.function : undefined,
|
|
44
|
+
line: typeof f.line === 'number' ? f.line : undefined,
|
|
45
|
+
inApp: typeof f.inApp === 'boolean' ? f.inApp : undefined,
|
|
46
|
+
})),
|
|
47
|
+
cause: null,
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
id: raw.id ?? uuidV7(),
|
|
51
|
+
kind: 'error',
|
|
52
|
+
occurredAt: raw.timestamp ?? new Date().toISOString(),
|
|
53
|
+
platform,
|
|
54
|
+
release: raw.release ?? '',
|
|
55
|
+
environment: raw.environment ?? '',
|
|
56
|
+
payload: {
|
|
57
|
+
error,
|
|
58
|
+
device: raw.device as WireEvent['payload']['device'],
|
|
59
|
+
app: raw.app as WireEvent['payload']['app'],
|
|
60
|
+
nativeCrash: true,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Drain, convert, enqueue, and ship pre-death attachments. */
|
|
66
|
+
export const shipNativePending = async (): Promise<void> => {
|
|
67
|
+
const files = await drainNativePending();
|
|
68
|
+
for (const text of files) {
|
|
69
|
+
try {
|
|
70
|
+
const raw = JSON.parse(text) as LegacyNativeEvent;
|
|
71
|
+
const pending = raw._pendingAttachments;
|
|
72
|
+
delete raw._pendingAttachments;
|
|
73
|
+
const wire = toWire(raw);
|
|
74
|
+
enqueue(wire);
|
|
75
|
+
if (pending && wire.id) {
|
|
76
|
+
for (const att of pending) {
|
|
77
|
+
if (att.base64 && att.kind) {
|
|
78
|
+
void uploadAttachment(
|
|
79
|
+
wire.id,
|
|
80
|
+
att.kind as import('@goliapkg/sentori-core').AttachmentKind,
|
|
81
|
+
{ base64: att.base64, mediaType: att.mediaType ?? 'application/octet-stream' },
|
|
82
|
+
{ source: (att.source as 'android' | 'ios' | 'js') ?? 'ios' },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// One corrupt crash file must not block the rest.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (files.length > 0) await flush();
|
|
92
|
+
};
|
package/src/native.ts
CHANGED
|
@@ -69,6 +69,13 @@ type SentoriNativeModule = {
|
|
|
69
69
|
* snapshot string or null on failure.
|
|
70
70
|
*/
|
|
71
71
|
captureWireframe?: (maskedIds: string[]) => null | string
|
|
72
|
+
|
|
73
|
+
/** v5.1 — low-bitrate replay frame; absent on older native builds. */
|
|
74
|
+
captureReplayFrame?: (
|
|
75
|
+
maskedIds: string[],
|
|
76
|
+
longEdgePx: number,
|
|
77
|
+
quality: number,
|
|
78
|
+
) => Promise<null | { base64: string; mediaType: string }>
|
|
72
79
|
/**
|
|
73
80
|
* v0.9.12 — diagnostic readout for the wireframe path. Cheap
|
|
74
81
|
* synchronous call that returns the path the last `captureWireframe`
|
|
@@ -387,6 +394,32 @@ export function getRecentNativeException(): null | {
|
|
|
387
394
|
* Callers must treat `null` as "no screenshot this round" — the
|
|
388
395
|
* error event still ships, just without a thumbnail.
|
|
389
396
|
*/
|
|
397
|
+
/** v5.1 — one low-bitrate frame for the screens replay ring.
|
|
398
|
+
* Null on every failure mode (module unbound, method missing on an
|
|
399
|
+
* older native build, capture failed); the ring just skips a beat. */
|
|
400
|
+
let warnedNoReplayFrame = false
|
|
401
|
+
export async function captureNativeReplayFrame(
|
|
402
|
+
maskedIds: string[],
|
|
403
|
+
longEdgePx: number,
|
|
404
|
+
quality: number,
|
|
405
|
+
): Promise<null | { base64: string; mediaType: string }> {
|
|
406
|
+
const n = native()
|
|
407
|
+
if (!n) return null
|
|
408
|
+
if (!n.captureReplayFrame) {
|
|
409
|
+
if (!warnedNoReplayFrame) {
|
|
410
|
+
warnedNoReplayFrame = true
|
|
411
|
+
logger.warn('native', 'captureReplayFrame missing — rebuild the native app for visual replay')
|
|
412
|
+
}
|
|
413
|
+
return null
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
return (await n.captureReplayFrame(maskedIds, longEdgePx, quality)) ?? null
|
|
417
|
+
} catch (e) {
|
|
418
|
+
logger.warn('native', 'captureReplayFrame threw', e)
|
|
419
|
+
return null
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
390
423
|
export async function captureNativeScreenshotWithMask(
|
|
391
424
|
maskedIds: string[],
|
|
392
425
|
): Promise<null | { base64: string; mediaType: string }> {
|