@goliapkg/sentori-react-native 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +21 -0
- package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
- package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
- package/android/src/test/resources/robolectric.properties +10 -0
- package/lib/config.d.ts +16 -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 +31 -109
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +51 -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 +88 -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/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/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 +28 -23
- package/lib/rage-tap.js.map +1 -1
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +3 -24
- package/lib/replay.js.map +1 -1
- 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 +3 -3
- package/src/__tests__/iron-rule.test.ts +128 -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 +19 -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 +81 -215
- package/src/init.ts +94 -446
- package/src/long-task-monitor.ts +42 -64
- package/src/mobile-vitals.ts +22 -82
- package/src/native-pending.ts +92 -0
- package/src/navigation.ts +34 -142
- package/src/netinfo.ts +3 -3
- package/src/push.ts +2 -5
- package/src/rage-tap.tsx +29 -26
- package/src/replay.ts +3 -20
- 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/mask.d.ts +0 -16
- package/lib/mask.d.ts.map +0 -1
- package/lib/mask.js +0 -36
- package/lib/mask.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/mask.ts +0 -41
- 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/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/navigation.ts
CHANGED
|
@@ -1,35 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// as children — one trace per screen instead of one per request.
|
|
1
|
+
// react-navigation auto-instrumentation — v1 role: feed the signal
|
|
2
|
+
// ring (`nav` signals) and remember the current screen so warn
|
|
3
|
+
// surfaces can name where they happened. The span/trace/metric
|
|
4
|
+
// machinery this file used to drive is gone with the APM vocabulary.
|
|
6
5
|
//
|
|
7
6
|
// Mount `useTraceNavigation(navigationRef)` next to your
|
|
8
|
-
// `<NavigationContainer ref={navigationRef}>`.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// react-navigation is an OPTIONAL peer dependency — apps that don't
|
|
13
|
-
// use it never have to install it. The hook doesn't import from
|
|
14
|
-
// @react-navigation/native; consumers pass in the ref they already
|
|
15
|
-
// have, and we read its state via the public `getCurrentRoute()`.
|
|
16
|
-
//
|
|
17
|
-
// Caveat (active-span on RN is a module variable): requests fired
|
|
18
|
-
// from a `setTimeout` / background poll / detached promise after the
|
|
19
|
-
// screen settled may not see the nav span as active. If you want such
|
|
20
|
-
// a request parented to the current screen, pass it explicitly:
|
|
21
|
-
// `startSpan(op, { parent: activeSpan() })`.
|
|
7
|
+
// `<NavigationContainer ref={navigationRef}>`. react-navigation
|
|
8
|
+
// stays an OPTIONAL peer dependency: the hook never imports from
|
|
9
|
+
// @react-navigation/native — consumers pass the ref they already
|
|
10
|
+
// have and we read `getCurrentRoute()`.
|
|
22
11
|
|
|
23
12
|
import { useEffect, useRef } from 'react';
|
|
24
13
|
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
getNativeFrameCounters,
|
|
29
|
-
resetNativeFrameCounters,
|
|
30
|
-
} from './native';
|
|
31
|
-
import { track } from './track';
|
|
32
|
-
import { captureStep } from './trail';
|
|
14
|
+
import { pushSignal } from '@goliapkg/sentori-core';
|
|
33
15
|
|
|
34
16
|
/** Minimal contract: anything with `addListener('state', cb)` and
|
|
35
17
|
* `getCurrentRoute()` works. The real @react-navigation/native
|
|
@@ -39,150 +21,60 @@ export type NavigationRefLike = {
|
|
|
39
21
|
getCurrentRoute: () => { name: string } | undefined;
|
|
40
22
|
};
|
|
41
23
|
|
|
42
|
-
/** Process-global last-known route —
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* when no nav has happened yet (app just launched, dev-launcher,
|
|
46
|
-
* splash). */
|
|
24
|
+
/** Process-global last-known route — surfaces on detected warns
|
|
25
|
+
* (`surface.screen`) and rides nav signals. Null before the first
|
|
26
|
+
* navigation (splash, dev launcher). */
|
|
47
27
|
let _lastRoute: null | string = null;
|
|
48
28
|
|
|
29
|
+
export function currentScreen(): string | undefined {
|
|
30
|
+
return _lastRoute ?? undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Kept for compat with earlier integrations. */
|
|
49
34
|
export function getLastRoute(): null | string {
|
|
50
35
|
return _lastRoute;
|
|
51
36
|
}
|
|
52
37
|
|
|
53
|
-
/**
|
|
54
|
-
* Subscribe to react-navigation state changes and emit a
|
|
55
|
-
* `react.navigation` span per screen (including the initial one),
|
|
56
|
-
* each a fresh trace root. The span is kept active while the screen
|
|
57
|
-
* is current; child spans created in that window attribute up to it.
|
|
58
|
-
*
|
|
59
|
-
* import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'
|
|
60
|
-
* import { useTraceNavigation } from '@goliapkg/sentori-react-native'
|
|
61
|
-
*
|
|
62
|
-
* function App() {
|
|
63
|
-
* const navigationRef = useNavigationContainerRef()
|
|
64
|
-
* useTraceNavigation(navigationRef)
|
|
65
|
-
* return <NavigationContainer ref={navigationRef}>{...}</NavigationContainer>
|
|
66
|
-
* }
|
|
67
|
-
*
|
|
68
|
-
* Each span carries `{ nav.from, nav.to }` tags.
|
|
69
|
-
*/
|
|
70
38
|
export function useTraceNavigation(navigationRef: NavigationRefLike): void {
|
|
71
|
-
// Latest route name we've observed.
|
|
72
39
|
const lastRouteRef = useRef<null | string>(null);
|
|
73
|
-
|
|
74
|
-
// attached to the leaving span + the next captureStep breadcrumb.
|
|
75
|
-
const lastRouteEnteredAtRef = useRef<null | number>(null);
|
|
76
|
-
// Span for the screen the user is currently on. Finished when the
|
|
77
|
-
// next screen is entered (or on unmount).
|
|
78
|
-
const openSpanRef = useRef<null | SpanHandle>(null);
|
|
40
|
+
const enteredAtRef = useRef<null | number>(null);
|
|
79
41
|
|
|
80
42
|
useEffect(() => {
|
|
81
43
|
if (typeof navigationRef.addListener !== 'function') return;
|
|
82
44
|
if (typeof navigationRef.getCurrentRoute !== 'function') return;
|
|
83
45
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
to: string,
|
|
90
|
-
prevDwellMs: null | number,
|
|
91
|
-
) => {
|
|
92
|
-
const span = startSpan('react.navigation', {
|
|
93
|
-
name: from ? `${from} → ${to}` : to,
|
|
94
|
-
parent: null,
|
|
95
|
-
tags: { 'nav.from': from ?? '', 'nav.to': to },
|
|
96
|
-
});
|
|
97
|
-
openSpanRef.current = span;
|
|
98
|
-
setActiveSpan(span);
|
|
46
|
+
const noteScreen = (from: null | string, to: string) => {
|
|
47
|
+
const dwellMs =
|
|
48
|
+
enteredAtRef.current !== null
|
|
49
|
+
? Math.max(0, Date.now() - enteredAtRef.current)
|
|
50
|
+
: undefined;
|
|
99
51
|
lastRouteRef.current = to;
|
|
100
52
|
_lastRoute = to;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// carries the same number, but the trail is the most-glanced
|
|
105
|
-
// surface so we duplicate it as breadcrumb data. No bytes wasted:
|
|
106
|
-
// a breadcrumb without sessionTrail enabled is a no-op.
|
|
107
|
-
captureStep(`screen:${to}`, {
|
|
108
|
-
breadcrumb: {
|
|
109
|
-
data: prevDwellMs !== null ? { dwellMsPrev: prevDwellMs } : undefined,
|
|
110
|
-
message: from ? `${from} → ${to}` : to,
|
|
111
|
-
type: 'navigation',
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
// v1.1 chunk B — auto-pageview. Pushed into the track ring
|
|
115
|
-
// alongside the navigation span so the analytics path sees
|
|
116
|
-
// every screen entry without app code needing to wire its own
|
|
117
|
-
// pageview calls. `from`/`dwellMsPrev` ride in props so the
|
|
118
|
-
// Behavior view (chunk D) can reconstruct the user journey
|
|
119
|
-
// even when only the track stream is queried.
|
|
120
|
-
track(
|
|
121
|
-
'$pageview',
|
|
122
|
-
prevDwellMs !== null
|
|
123
|
-
? { from: from ?? null, dwellMsPrev: prevDwellMs }
|
|
124
|
-
: { from: from ?? null },
|
|
53
|
+
enteredAtRef.current = Date.now();
|
|
54
|
+
pushSignal('nav', {
|
|
55
|
+
from: from ?? undefined,
|
|
125
56
|
to,
|
|
126
|
-
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const finishOpenSpanWithDwell = (): null | number => {
|
|
130
|
-
const span = openSpanRef.current;
|
|
131
|
-
const enteredAt = lastRouteEnteredAtRef.current;
|
|
132
|
-
if (!span) return null;
|
|
133
|
-
const dwellMs = enteredAt !== null ? Math.max(0, Date.now() - enteredAt) : null;
|
|
134
|
-
// v0.9.4 #1 — drain native frame counters at screen-leave.
|
|
135
|
-
// Empty/null when native module not linked; tags omitted.
|
|
136
|
-
const fc = getNativeFrameCounters();
|
|
137
|
-
const finishTags: Record<string, string> = {};
|
|
138
|
-
if (dwellMs !== null) finishTags['nav.dwell_ms'] = String(dwellMs);
|
|
139
|
-
if (fc) {
|
|
140
|
-
finishTags['vital.slow_frames'] = String(fc.slow);
|
|
141
|
-
finishTags['vital.frozen_frames'] = String(fc.frozen);
|
|
142
|
-
}
|
|
143
|
-
span.finish({
|
|
144
|
-
status: 'ok',
|
|
145
|
-
tags: Object.keys(finishTags).length > 0 ? finishTags : undefined,
|
|
57
|
+
dwellMsPrev: dwellMs,
|
|
146
58
|
});
|
|
147
|
-
// Reset counters for the next screen.
|
|
148
|
-
resetNativeFrameCounters();
|
|
149
|
-
return dwellMs;
|
|
150
59
|
};
|
|
151
60
|
|
|
152
|
-
// Open a span for the initial screen so its requests are grouped
|
|
153
|
-
// too (auth / config / first data load are usually the busiest
|
|
154
|
-
// screen of a session).
|
|
155
61
|
const initial = navigationRef.getCurrentRoute()?.name ?? null;
|
|
156
|
-
if (initial !== null)
|
|
157
|
-
else lastRouteRef.current = null;
|
|
62
|
+
if (initial !== null) noteScreen(null, initial);
|
|
158
63
|
|
|
159
64
|
const unsubscribe = navigationRef.addListener('state', () => {
|
|
160
65
|
const next = navigationRef.getCurrentRoute()?.name ?? null;
|
|
161
66
|
const prev = lastRouteRef.current;
|
|
162
67
|
if (next === null || next === prev) return;
|
|
163
|
-
|
|
164
|
-
// v2.1 W2 — emit dwell as a runtime metric so the BI panel
|
|
165
|
-
// can rank screens by how long users sat on them. Tagged
|
|
166
|
-
// with `from` + `to` so a release-over-release diff can spot
|
|
167
|
-
// a 2x dwell regression on Onboarding etc. emit is cheap +
|
|
168
|
-
// bounded by the core ring cap; if `capture.runtimeMetrics:
|
|
169
|
-
// false` the flush is off but the ring still records up to
|
|
170
|
-
// the cap before dropping oldest.
|
|
171
|
-
if (typeof dwellMs === 'number' && dwellMs >= 0) {
|
|
172
|
-
emitMetric('runtime.route_nav_ms', dwellMs, {
|
|
173
|
-
from: prev ?? '',
|
|
174
|
-
to: next ?? '',
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
openScreenSpan(prev, next, dwellMs);
|
|
68
|
+
noteScreen(prev, next);
|
|
178
69
|
});
|
|
179
70
|
|
|
180
71
|
return () => {
|
|
181
72
|
unsubscribe();
|
|
182
|
-
|
|
183
|
-
openSpanRef.current = null;
|
|
184
|
-
lastRouteEnteredAtRef.current = null;
|
|
185
|
-
setActiveSpan(null);
|
|
73
|
+
enteredAtRef.current = null;
|
|
186
74
|
};
|
|
187
75
|
}, [navigationRef]);
|
|
188
76
|
}
|
|
77
|
+
|
|
78
|
+
export const __resetForTests = (): void => {
|
|
79
|
+
_lastRoute = null;
|
|
80
|
+
};
|
package/src/netinfo.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
// standard library), we subscribe at SDK init time and cache the
|
|
6
6
|
// latest network state. `collectDevice()` reads the cache
|
|
7
7
|
// synchronously at capture time. If the peer isn't installed, the
|
|
8
|
-
// cache stays `undefined` and `device.
|
|
8
|
+
// cache stays `undefined` and `device.network` is omitted —
|
|
9
9
|
// no warning, no crash.
|
|
10
10
|
//
|
|
11
11
|
// We collapse NetInfo's enum into the smaller set the protocol
|
|
12
|
-
// allows (see `Device.
|
|
12
|
+
// allows (see `Device.network` in `sdk/core/src/types.ts`):
|
|
13
13
|
// `wifi`, `2g/3g/4g/slow-2g`, `offline`, `unknown`. 5G collapses
|
|
14
14
|
// into `4g` because the schema doesn't have a 5g slot yet; the
|
|
15
15
|
// information loss is acceptable for an analytics dimension.
|
|
@@ -18,7 +18,7 @@ import type { Device } from '@goliapkg/sentori-core';
|
|
|
18
18
|
|
|
19
19
|
import { isNativeModuleLinked } from './native-loader';
|
|
20
20
|
|
|
21
|
-
type NetworkType = Device['
|
|
21
|
+
type NetworkType = Device['network'];
|
|
22
22
|
|
|
23
23
|
type NetInfoState = {
|
|
24
24
|
details?: { cellularGeneration?: null | string };
|
package/src/push.ts
CHANGED
|
@@ -21,9 +21,8 @@
|
|
|
21
21
|
// buffered events while the app is foreground. Pauses on
|
|
22
22
|
// background, resumes on active, per the perf iron rule.
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import { logger, pushSignal } from '@goliapkg/sentori-core'
|
|
25
25
|
|
|
26
|
-
import { track } from './track.js'
|
|
27
26
|
// AppState is RN-only; we treat it dynamically so the SDK keeps
|
|
28
27
|
// importing cleanly under Bun / web.
|
|
29
28
|
type AppStateModule = {
|
|
@@ -318,13 +317,11 @@ function autoCorrelate(
|
|
|
318
317
|
// Breadcrumb buffer: O(1) in-memory push. Tag both event types
|
|
319
318
|
// ('received' vs 'opened') so a later captureException shows
|
|
320
319
|
// whether the user actually saw the push.
|
|
321
|
-
|
|
320
|
+
pushSignal('push', { body: undefined, msgId, opened: eventType === 'opened', provider, title })
|
|
322
321
|
|
|
323
322
|
// Track event: reuses the existing SDK event pipeline. Two
|
|
324
323
|
// distinct names so dashboards can separate delivery from open.
|
|
325
324
|
const trackName = eventType === 'opened' ? 'sentori.push.opened' : 'sentori.push.received'
|
|
326
|
-
track(trackName, { msgId, provider })
|
|
327
|
-
|
|
328
325
|
// Enqueue ack — batched, see drainAckQueue.
|
|
329
326
|
enqueueAck(msgId)
|
|
330
327
|
}
|