@goliapkg/sentori-react-native 1.0.0-rc.8 → 1.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/bin/sentori-rn-upload-source-bundle.cjs +193 -0
- package/lib/capture.d.ts.map +1 -1
- package/lib/capture.js +9 -0
- package/lib/capture.js.map +1 -1
- package/lib/heartbeat.d.ts +9 -0
- package/lib/heartbeat.d.ts.map +1 -0
- package/lib/heartbeat.js +140 -0
- package/lib/heartbeat.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +15 -0
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +6 -0
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +18 -0
- package/lib/init.js.map +1 -1
- package/lib/install-id.d.ts +17 -0
- package/lib/install-id.d.ts.map +1 -0
- package/lib/install-id.js +125 -0
- package/lib/install-id.js.map +1 -0
- package/lib/navigation.d.ts +1 -0
- package/lib/navigation.d.ts.map +1 -1
- package/lib/navigation.js +20 -0
- package/lib/navigation.js.map +1 -1
- package/lib/replay.d.ts +27 -9
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +209 -167
- package/lib/replay.js.map +1 -1
- package/lib/report-security.d.ts +40 -0
- package/lib/report-security.d.ts.map +1 -0
- package/lib/report-security.js +159 -0
- package/lib/report-security.js.map +1 -0
- package/lib/track.d.ts +34 -0
- package/lib/track.d.ts.map +1 -0
- package/lib/track.js +98 -0
- package/lib/track.js.map +1 -0
- package/lib/transport.d.ts +15 -0
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +23 -0
- package/lib/transport.js.map +1 -1
- package/lib/trust-score.d.ts +20 -0
- package/lib/trust-score.d.ts.map +1 -0
- package/lib/trust-score.js +151 -0
- package/lib/trust-score.js.map +1 -0
- package/package.json +6 -2
- package/src/__tests__/install-id.test.ts +60 -0
- package/src/__tests__/replay-encoding.test.ts +237 -0
- package/src/__tests__/report-security.test.ts +106 -0
- package/src/__tests__/track.test.ts +91 -0
- package/src/capture.ts +8 -0
- package/src/heartbeat.ts +158 -0
- package/src/index.ts +24 -0
- package/src/init.ts +23 -0
- package/src/install-id.ts +146 -0
- package/src/navigation.ts +26 -0
- package/src/replay.ts +258 -176
- package/src/report-security.ts +165 -0
- package/src/track.ts +114 -0
- package/src/transport.ts +35 -0
- package/src/trust-score.ts +176 -0
package/src/index.ts
CHANGED
|
@@ -25,7 +25,11 @@ import {
|
|
|
25
25
|
} from './mobile-vitals';
|
|
26
26
|
import { bindState, recordState, unbindState } from './state-snapshots';
|
|
27
27
|
import { startMoment } from '@goliapkg/sentori-core';
|
|
28
|
+
import { getInstallId } from './install-id';
|
|
28
29
|
import { flushMetrics, recordMetric } from './metrics';
|
|
30
|
+
import { linkFederatedIdentity, reportPinMismatch, reportSecurity } from './report-security';
|
|
31
|
+
import { flushTrack, track } from './track';
|
|
32
|
+
import { queryTrustScore } from './trust-score';
|
|
29
33
|
import { RageTapCapture } from './rage-tap';
|
|
30
34
|
import {
|
|
31
35
|
endSession,
|
|
@@ -44,6 +48,13 @@ export const sentori = {
|
|
|
44
48
|
sendUserFeedback,
|
|
45
49
|
recordMetric,
|
|
46
50
|
flushMetrics,
|
|
51
|
+
track,
|
|
52
|
+
flushTrack,
|
|
53
|
+
getInstallId,
|
|
54
|
+
reportSecurity,
|
|
55
|
+
reportPinMismatch,
|
|
56
|
+
queryTrustScore,
|
|
57
|
+
linkFederatedIdentity,
|
|
47
58
|
measureFn,
|
|
48
59
|
startMoment,
|
|
49
60
|
bindState,
|
|
@@ -87,6 +98,19 @@ export {
|
|
|
87
98
|
} from './feature-flags';
|
|
88
99
|
export { clearMaskQuery, registerMaskQuery } from './mask';
|
|
89
100
|
export { flushMetrics, recordMetric } from './metrics';
|
|
101
|
+
export { flushTrack, track, type TrackEvent, type TrackProps } from './track';
|
|
102
|
+
export { getInstallId, peekInstallId } from './install-id';
|
|
103
|
+
export {
|
|
104
|
+
linkFederatedIdentity,
|
|
105
|
+
reportPinMismatch,
|
|
106
|
+
reportSecurity,
|
|
107
|
+
type SecurityReportData,
|
|
108
|
+
} from './report-security';
|
|
109
|
+
export {
|
|
110
|
+
queryTrustScore,
|
|
111
|
+
type TrustScore,
|
|
112
|
+
type TrustSignal,
|
|
113
|
+
} from './trust-score';
|
|
90
114
|
export { measureFn } from './measure';
|
|
91
115
|
export {
|
|
92
116
|
getColdStartMs,
|
package/src/init.ts
CHANGED
|
@@ -8,9 +8,12 @@ import {
|
|
|
8
8
|
markLaunchCompleted,
|
|
9
9
|
runLaunchCrashGuard,
|
|
10
10
|
} from './launch-crash-guard';
|
|
11
|
+
import { getInstallId } from './install-id';
|
|
11
12
|
import { startMetricsTimer } from './metrics';
|
|
13
|
+
import { startTrackTimer } from './track';
|
|
12
14
|
import { drainNativePending, markNativeJsBridgeReady, setNativeConfig } from './native';
|
|
13
15
|
import { getColdStartMs } from './mobile-vitals';
|
|
16
|
+
import { startHeartbeat, type HeartbeatOptions } from './heartbeat';
|
|
14
17
|
import { startSpan } from '@goliapkg/sentori-core';
|
|
15
18
|
import { startControlChannel } from './control-channel';
|
|
16
19
|
import { startLongTaskMonitor } from './long-task-monitor';
|
|
@@ -92,6 +95,11 @@ export type InitOptions = {
|
|
|
92
95
|
* longTaskMonitor (≥200ms outliers) — sample profiler 看 idle
|
|
93
96
|
* 分布、long-task 看 outliers。 */
|
|
94
97
|
sampleProfiler?: boolean | { flushMs?: number; sampleMs?: number };
|
|
98
|
+
/** Analytics v1 — live-presence heartbeat. Foreground 1/min
|
|
99
|
+
* default; opt out with `false`, or pass an options object
|
|
100
|
+
* to tune. Powers the Audience > Live dashboard.
|
|
101
|
+
* Iron-rule budget: < 1 KB / min, < 1 ms / call. */
|
|
102
|
+
heartbeat?: boolean | HeartbeatOptions;
|
|
95
103
|
/** v0.9.0 #3 — launch-crash loop guard. When two consecutive
|
|
96
104
|
* launches don't reach `markLaunchCompleted()` (typical of an
|
|
97
105
|
* OTA update with a fatal bug), invoke the host callback with
|
|
@@ -193,6 +201,13 @@ export const init = (options: InitOptions): void => {
|
|
|
193
201
|
startNetworkTypeWatch();
|
|
194
202
|
// v0.8.3 — drain custom-metric ring every 30 s.
|
|
195
203
|
startMetricsTimer();
|
|
204
|
+
// v1.1 chunk B — drain `sentori.track()` ring every 30 s.
|
|
205
|
+
startTrackTimer();
|
|
206
|
+
// v1.1 chunk S1 — warm the install-id cache. Fire-and-forget;
|
|
207
|
+
// any event captured before the first resolve simply omits
|
|
208
|
+
// `device.installId`. Subsequent captures pick it up via the
|
|
209
|
+
// sync `peekInstallId()` read in collectDevice().
|
|
210
|
+
void getInstallId();
|
|
196
211
|
// v0.9.1 +S4 — pre-crash sentinel. Off by default; opt-in via
|
|
197
212
|
// `capture.preCrashSentinel: true`.
|
|
198
213
|
if (options.capture?.preCrashSentinel === true) {
|
|
@@ -240,6 +255,14 @@ export const init = (options: InitOptions): void => {
|
|
|
240
255
|
startSession();
|
|
241
256
|
installLifecycleHandler();
|
|
242
257
|
}
|
|
258
|
+
// Analytics v1 — live-presence heartbeat. Foreground 1/min by
|
|
259
|
+
// default; pass `capture.heartbeat = false` to opt out, or
|
|
260
|
+
// `{ intervalMs: 30_000 }` to override. The default budget is well
|
|
261
|
+
// under the "几乎不能造成性能抖动" rule (CLAUDE.md).
|
|
262
|
+
const heartbeatOpt = capture.heartbeat;
|
|
263
|
+
if (heartbeatOpt !== false) {
|
|
264
|
+
startHeartbeat(typeof heartbeatOpt === 'object' ? heartbeatOpt : {});
|
|
265
|
+
}
|
|
243
266
|
|
|
244
267
|
// Drain events persisted from previous session (best-effort):
|
|
245
268
|
// - native crashes from <Documents>/sentori/pending/*.json
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// v1.1 chunk S1 — stable per-install identifier.
|
|
2
|
+
//
|
|
3
|
+
// `getInstallId()` returns a UUIDv7 generated on first call and
|
|
4
|
+
// persisted to device storage. The id survives app restarts and, on
|
|
5
|
+
// iOS specifically, app reinstalls (because the Keychain backend is
|
|
6
|
+
// preserved across uninstall — that's the whole point).
|
|
7
|
+
//
|
|
8
|
+
// Storage tier order:
|
|
9
|
+
// 1. `react-native-keychain` (optional peer; iOS + Android Keystore)
|
|
10
|
+
// 2. AsyncStorage (host already needs this for launch-crash-guard /
|
|
11
|
+
// offline queue, so we don't add a hard new peer dep)
|
|
12
|
+
// 3. Process-memory only (no persistence — covers SSR / tests)
|
|
13
|
+
//
|
|
14
|
+
// Opaque to the host. The id is NOT tied to a `setUser` call; the
|
|
15
|
+
// server doesn't auto-correlate to user identity. It exists purely
|
|
16
|
+
// as a stable device key so the security posture engine (S3) can
|
|
17
|
+
// score per-install signals independently of authentication state.
|
|
18
|
+
|
|
19
|
+
import { uuidV7 } from '@goliapkg/sentori-core';
|
|
20
|
+
|
|
21
|
+
import { isAnyNativeModuleLinked } from './native-loader';
|
|
22
|
+
|
|
23
|
+
const KEYCHAIN_SERVICE = 'sentori.install-id';
|
|
24
|
+
const ASYNC_STORAGE_KEY = '@sentori/install-id';
|
|
25
|
+
|
|
26
|
+
type AsyncStorageLike = {
|
|
27
|
+
getItem: (key: string) => Promise<null | string>;
|
|
28
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type KeychainModule = {
|
|
32
|
+
getGenericPassword: (options: {
|
|
33
|
+
service: string;
|
|
34
|
+
}) => Promise<false | { password: string; username: string }>;
|
|
35
|
+
setGenericPassword: (
|
|
36
|
+
username: string,
|
|
37
|
+
password: string,
|
|
38
|
+
options: { service: string },
|
|
39
|
+
) => Promise<unknown>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let _cached: null | string = null;
|
|
43
|
+
let _inflight: null | Promise<string> = null;
|
|
44
|
+
|
|
45
|
+
function loadKeychain(): KeychainModule | null {
|
|
46
|
+
// react-native-keychain is the recommended secure-storage peer.
|
|
47
|
+
// Optional: if the host hasn't installed it we fall through to
|
|
48
|
+
// AsyncStorage. The Keychain backend is what gives the iOS
|
|
49
|
+
// "survives reinstall" guarantee.
|
|
50
|
+
try {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
|
+
const mod = require('react-native-keychain') as KeychainModule;
|
|
53
|
+
if (typeof mod.getGenericPassword !== 'function') return null;
|
|
54
|
+
return mod;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadAsyncStorage(): AsyncStorageLike | null {
|
|
61
|
+
if (!isAnyNativeModuleLinked(['RNCAsyncStorage', 'AsyncStorageModule'])) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
|
+
const mod = require('@react-native-async-storage/async-storage') as {
|
|
67
|
+
default?: AsyncStorageLike;
|
|
68
|
+
};
|
|
69
|
+
return mod.default ?? (mod as unknown as AsyncStorageLike);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the device's stable install id, generating one if absent.
|
|
77
|
+
* Cached in memory after first resolve so subsequent calls are sync-
|
|
78
|
+
* adjacent (no storage I/O). Idempotent under concurrent calls — a
|
|
79
|
+
* second caller during the first resolve awaits the same promise.
|
|
80
|
+
*/
|
|
81
|
+
export async function getInstallId(): Promise<string> {
|
|
82
|
+
if (_cached !== null) return _cached;
|
|
83
|
+
if (_inflight !== null) return _inflight;
|
|
84
|
+
_inflight = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const kc = loadKeychain();
|
|
87
|
+
if (kc) {
|
|
88
|
+
const existing = await kc
|
|
89
|
+
.getGenericPassword({ service: KEYCHAIN_SERVICE })
|
|
90
|
+
.catch(() => false as const);
|
|
91
|
+
if (existing && existing.password) {
|
|
92
|
+
_cached = existing.password;
|
|
93
|
+
return existing.password;
|
|
94
|
+
}
|
|
95
|
+
const fresh = uuidV7();
|
|
96
|
+
await kc
|
|
97
|
+
.setGenericPassword('sentori', fresh, { service: KEYCHAIN_SERVICE })
|
|
98
|
+
.catch(() => undefined);
|
|
99
|
+
_cached = fresh;
|
|
100
|
+
return fresh;
|
|
101
|
+
}
|
|
102
|
+
const storage = loadAsyncStorage();
|
|
103
|
+
if (storage) {
|
|
104
|
+
const existing = await storage
|
|
105
|
+
.getItem(ASYNC_STORAGE_KEY)
|
|
106
|
+
.catch(() => null);
|
|
107
|
+
if (existing) {
|
|
108
|
+
_cached = existing;
|
|
109
|
+
return existing;
|
|
110
|
+
}
|
|
111
|
+
const fresh = uuidV7();
|
|
112
|
+
await storage.setItem(ASYNC_STORAGE_KEY, fresh).catch(() => undefined);
|
|
113
|
+
_cached = fresh;
|
|
114
|
+
return fresh;
|
|
115
|
+
}
|
|
116
|
+
// No storage available — generate but don't persist. The id is
|
|
117
|
+
// still stable for the lifetime of the process which is good
|
|
118
|
+
// enough for tests / SSR / no-native-modules contexts.
|
|
119
|
+
const fresh = uuidV7();
|
|
120
|
+
_cached = fresh;
|
|
121
|
+
return fresh;
|
|
122
|
+
} finally {
|
|
123
|
+
_inflight = null;
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
return _inflight;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Sync read of the currently-cached install id. `null` before the
|
|
130
|
+
* first `getInstallId()` resolves. Use this in hot paths (event
|
|
131
|
+
* payload assembly) that can't await storage; callers should kick
|
|
132
|
+
* off `getInstallId()` once at startup to warm the cache. */
|
|
133
|
+
export function peekInstallId(): null | string {
|
|
134
|
+
return _cached;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function __resetInstallIdForTests(): void {
|
|
138
|
+
_cached = null;
|
|
139
|
+
_inflight = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** For tests + advanced operator flows that need to seed an id from
|
|
143
|
+
* an external secure-storage migration. */
|
|
144
|
+
export function __setInstallIdForTests(id: string): void {
|
|
145
|
+
_cached = id;
|
|
146
|
+
}
|
package/src/navigation.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getNativeFrameCounters,
|
|
29
29
|
resetNativeFrameCounters,
|
|
30
30
|
} from './native';
|
|
31
|
+
import { track } from './track';
|
|
31
32
|
import { captureStep } from './trail';
|
|
32
33
|
|
|
33
34
|
/** Minimal contract: anything with `addListener('state', cb)` and
|
|
@@ -38,6 +39,17 @@ export type NavigationRefLike = {
|
|
|
38
39
|
getCurrentRoute: () => { name: string } | undefined;
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
/** Process-global last-known route — populated by the navigation
|
|
43
|
+
* span path below, read by the analytics heartbeat so its per-tick
|
|
44
|
+
* payload includes the screen the user is on right now. Initial null
|
|
45
|
+
* when no nav has happened yet (app just launched, dev-launcher,
|
|
46
|
+
* splash). */
|
|
47
|
+
let _lastRoute: null | string = null;
|
|
48
|
+
|
|
49
|
+
export function getLastRoute(): null | string {
|
|
50
|
+
return _lastRoute;
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
/**
|
|
42
54
|
* Subscribe to react-navigation state changes and emit a
|
|
43
55
|
* `react.navigation` span per screen (including the initial one),
|
|
@@ -85,6 +97,7 @@ export function useTraceNavigation(navigationRef: NavigationRefLike): void {
|
|
|
85
97
|
openSpanRef.current = span;
|
|
86
98
|
setActiveSpan(span);
|
|
87
99
|
lastRouteRef.current = to;
|
|
100
|
+
_lastRoute = to;
|
|
88
101
|
lastRouteEnteredAtRef.current = Date.now();
|
|
89
102
|
// v0.8.0-b — dwell on the previous screen surfaces in the
|
|
90
103
|
// session trail. The leaving span's `durationMs` already
|
|
@@ -98,6 +111,19 @@ export function useTraceNavigation(navigationRef: NavigationRefLike): void {
|
|
|
98
111
|
type: 'navigation',
|
|
99
112
|
},
|
|
100
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 },
|
|
125
|
+
to,
|
|
126
|
+
);
|
|
101
127
|
};
|
|
102
128
|
|
|
103
129
|
const finishOpenSpanWithDwell = (): null | number => {
|