@goliapkg/sentori-react-native 3.1.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 +5 -5
- 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/capture.ts
DELETED
|
@@ -1,660 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type BeforeSendHook,
|
|
3
|
-
type CaptureMessageOptions,
|
|
4
|
-
hashIdentities,
|
|
5
|
-
type LinkBy,
|
|
6
|
-
logger,
|
|
7
|
-
type MessageLevel,
|
|
8
|
-
safeFn,
|
|
9
|
-
sealTrail,
|
|
10
|
-
shouldSample,
|
|
11
|
-
} from '@goliapkg/sentori-core';
|
|
12
|
-
|
|
13
|
-
import { base64Utf8 } from './base64';
|
|
14
|
-
import {
|
|
15
|
-
__peekBreadcrumbCount,
|
|
16
|
-
addBreadcrumb,
|
|
17
|
-
getBreadcrumbs,
|
|
18
|
-
} from './breadcrumbs';
|
|
19
|
-
import { getBundleInfo } from './bundle-info';
|
|
20
|
-
import { getConfig, isInitialized } from './config';
|
|
21
|
-
import { getFeatureFlagSnapshot } from './feature-flags';
|
|
22
|
-
import { drainReplay, isReplayRunning } from './replay';
|
|
23
|
-
import { clearStateSnapshots, getStateSnapshots } from './state-snapshots';
|
|
24
|
-
import { symbolicateErrorViaMetro } from './handlers/dev-symbolicate';
|
|
25
|
-
import { captureScreenshot } from './handlers/screenshot';
|
|
26
|
-
import { markSessionErrored } from './session-tracker';
|
|
27
|
-
import { parseStack } from './stack';
|
|
28
|
-
import { getTrailBuffer } from './trail';
|
|
29
|
-
import { enqueue, sendUserReport, uploadAttachment } from './transport';
|
|
30
|
-
import { uuidV7 } from './uuid';
|
|
31
|
-
import { peekInstallId } from './install-id';
|
|
32
|
-
import { getCachedNetworkType } from './netinfo';
|
|
33
|
-
import { getRecentNativeException } from './native';
|
|
34
|
-
import type { App, AttachmentMeta, Device, Event, SentoriError, Tags, User } from './types';
|
|
35
|
-
|
|
36
|
-
export { captureStep, __resetTrailForTests } from './trail';
|
|
37
|
-
|
|
38
|
-
declare const __DEV__: boolean | undefined;
|
|
39
|
-
|
|
40
|
-
let _user: User | null = null;
|
|
41
|
-
|
|
42
|
-
/** v2.0 — global scope tags merged onto every captured event
|
|
43
|
-
* (captureException + captureMessage). Set via `setTag` /
|
|
44
|
-
* `setTags`; reset by passing `{}`. Per-call tags win on conflict. */
|
|
45
|
-
let _scopeTags: Record<string, string> = {};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* v2.0 — set a single scope tag merged onto every subsequent
|
|
49
|
-
* capture. Per-call `extras.tags` / `opts.tags` win over scope tags.
|
|
50
|
-
*
|
|
51
|
-
* sentori.setTag('rollout', 'dark-mode-v2');
|
|
52
|
-
* sentori.captureException(err); // event.tags carries rollout
|
|
53
|
-
*/
|
|
54
|
-
export const setTag = (key: string, value: string): void => {
|
|
55
|
-
_scopeTags[key] = String(value);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* v2.0 — bulk variant of setTag. Existing tags are merged with
|
|
60
|
-
* the input record (`Object.assign` style).
|
|
61
|
-
*/
|
|
62
|
-
export const setTags = (record: Record<string, string>): void => {
|
|
63
|
-
for (const [k, v] of Object.entries(record)) _scopeTags[k] = String(v);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/** Internal — merge global scope tags into a per-call tag record. */
|
|
67
|
-
function mergeScopeTags(perCall: Tags | undefined): Tags | undefined {
|
|
68
|
-
if (Object.keys(_scopeTags).length === 0) return perCall;
|
|
69
|
-
return { ..._scopeTags, ...(perCall ?? {}) };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const __resetScopeForTests = (): void => {
|
|
73
|
-
_user = null;
|
|
74
|
-
_scopeTags = {};
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// Phase 42 sub-D.08 — per-session screenshot quota. Defaults: 10 in
|
|
78
|
-
// prod, unlimited (-1 sentinel) in dev so test loops + react-error-
|
|
79
|
-
// overlay reruns don't run out partway through the session.
|
|
80
|
-
const SCREENSHOT_PROD_LIMIT = 10;
|
|
81
|
-
let _screenshotsTaken = 0;
|
|
82
|
-
|
|
83
|
-
function screenshotBudget(): number {
|
|
84
|
-
return typeof __DEV__ !== 'undefined' && __DEV__ ? -1 : SCREENSHOT_PROD_LIMIT;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export const __resetScreenshotBudgetForTests = (): void => {
|
|
88
|
-
_screenshotsTaken = 0;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Attach a stable user identifier to events captured after this call.
|
|
93
|
-
*
|
|
94
|
-
* PII policy (Phase 16 sub-D): the User shape is intentionally limited
|
|
95
|
-
* to `{ id?, anonymous? }` — no email, name, IP, or other identifying
|
|
96
|
-
* fields. Use a hashed / pseudonymous id (e.g. uuid v4 stored in
|
|
97
|
-
* AsyncStorage on first launch). The server schema enforces the same
|
|
98
|
-
* shape, so any extra fields you tack on at the JS layer would be
|
|
99
|
-
* rejected with `validationFailed` and never persisted.
|
|
100
|
-
*
|
|
101
|
-
* Pass `null` to clear (e.g. on sign-out).
|
|
102
|
-
*/
|
|
103
|
-
/**
|
|
104
|
-
* Public user identity API.
|
|
105
|
-
*
|
|
106
|
-
* sentori.setUser({ id: 'usr_123', name: 'Lihao', linkBy: {
|
|
107
|
-
* email: 'lihao@example.com',
|
|
108
|
-
* googleSub: '108293…',
|
|
109
|
-
* } })
|
|
110
|
-
*
|
|
111
|
-
* The `linkBy` map is for **cross-project lookup** (see v2.3 design,
|
|
112
|
-
* §5 in docs/design/sdk-v2.3-redesign.md). Each value gets hashed
|
|
113
|
-
* client-side via `crypto.subtle.digest('SHA-256', …)` BEFORE
|
|
114
|
-
* leaving the device. Raw email / phone / sub **never** reach
|
|
115
|
-
* Sentori; the server can't recover them.
|
|
116
|
-
*
|
|
117
|
-
* Hashing is async (WebCrypto is async). `setUser` itself returns
|
|
118
|
-
* void synchronously so host code stays one-line; the hash work
|
|
119
|
-
* runs in the background and is committed to scope when ready. If
|
|
120
|
-
* a `captureException` fires BEFORE the hash settles, the event
|
|
121
|
-
* carries `id` + `name` only (no linkHashes that cycle) — the next
|
|
122
|
-
* event picks them up. This is fine in practice because the host
|
|
123
|
-
* sets the user once near startup, well before any captures.
|
|
124
|
-
*
|
|
125
|
-
* If `crypto.subtle` is unavailable in the runtime, the hash
|
|
126
|
-
* promise rejects and `linkBy` is silently dropped (NEVER rule —
|
|
127
|
-
* SDK failure must not propagate to host code).
|
|
128
|
-
*/
|
|
129
|
-
type SetUserInput = (User & { linkBy?: LinkBy }) | null;
|
|
130
|
-
|
|
131
|
-
export const setUser = (input: SetUserInput): void => {
|
|
132
|
-
if (input == null) {
|
|
133
|
-
// `null` (explicit clear) or `undefined` (callers occasionally
|
|
134
|
-
// pass through optional state). Both clear the scope user.
|
|
135
|
-
_user = null;
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
// Stage 1: commit id + name + anonymous immediately so the next
|
|
139
|
-
// captured event picks them up even if hashing is still in flight.
|
|
140
|
-
const { linkBy: rawLinkBy, ...stable } = input;
|
|
141
|
-
_user = { ...stable };
|
|
142
|
-
|
|
143
|
-
// Stage 2: hash any linkBy values async; commit to scope when done.
|
|
144
|
-
if (rawLinkBy && Object.keys(rawLinkBy).length > 0) {
|
|
145
|
-
void hashIdentities(rawLinkBy)
|
|
146
|
-
.then((linkHashes) => {
|
|
147
|
-
// Re-merge to guard against setUser being called again
|
|
148
|
-
// between stage 1 and stage 2 — if the user changed, drop
|
|
149
|
-
// the now-stale hash.
|
|
150
|
-
if (_user && _user.id === stable.id) {
|
|
151
|
-
_user = { ..._user, linkHashes };
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
.catch((e) => {
|
|
155
|
-
logger.warn('identity', 'linkBy hash failed; identities dropped:', e);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
export const getUser = (): User | null => _user;
|
|
161
|
-
|
|
162
|
-
/** v1.1 +S7 升级 — read just the current user id for the control
|
|
163
|
-
* channel poll. Returns `undefined` until `setUser({ id })` runs. */
|
|
164
|
-
export const getCurrentUserId = (): string | undefined => _user?.id ?? undefined;
|
|
165
|
-
|
|
166
|
-
export type CaptureExtras = {
|
|
167
|
-
tags?: Tags;
|
|
168
|
-
user?: User;
|
|
169
|
-
fingerprint?: string[];
|
|
170
|
-
/** Phase 42 sub-D.07: per-call screenshot override. `false` skips
|
|
171
|
-
* screenshot capture even when `init({ capture: { screenshot:
|
|
172
|
-
* true } })` is on — handy for sensitive screens. Defaults to
|
|
173
|
-
* whatever `config.screenshotsEnabled` says. */
|
|
174
|
-
screenshot?: boolean;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* v0.8.2 — submit an end-user-supplied bug report. Use this when the
|
|
179
|
-
* host app surfaces a "Report a problem" form. Pass `eventId` if the
|
|
180
|
-
* user is reporting a specific crash they just saw — the server links
|
|
181
|
-
* the report to that event's issue automatically.
|
|
182
|
-
*
|
|
183
|
-
* Returns `{ id, issueId }` on success or `null` on any failure
|
|
184
|
-
* (network down, ingest token revoked, validation rejected). Doesn't
|
|
185
|
-
* throw.
|
|
186
|
-
*/
|
|
187
|
-
export const sendUserFeedback = async (input: {
|
|
188
|
-
body: string;
|
|
189
|
-
email?: string;
|
|
190
|
-
eventId?: string;
|
|
191
|
-
name?: string;
|
|
192
|
-
title: string;
|
|
193
|
-
}): Promise<null | { id: string; issueId: null | string }> => {
|
|
194
|
-
if (!isInitialized()) return null;
|
|
195
|
-
const config = getConfig();
|
|
196
|
-
if (!config) return null;
|
|
197
|
-
return sendUserReport(config.ingestUrl, config.token, input);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export const captureError = (error: Error, extras?: CaptureExtras): void => {
|
|
201
|
-
if (!isInitialized()) return;
|
|
202
|
-
const config = getConfig();
|
|
203
|
-
if (!config) return;
|
|
204
|
-
|
|
205
|
-
// Phase 44 sub-B: client-side sampling. Skip the whole pipeline
|
|
206
|
-
// (no screenshot capture either) when the sample dice come up
|
|
207
|
-
// wrong. Default rate = null = keep, so existing callers unaffected.
|
|
208
|
-
if (!shouldSample(config.errorSampleRate)) {
|
|
209
|
-
addBreadcrumb({ type: 'custom', data: { reason: 'sampled-out', kind: 'error' } });
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const flags = getFeatureFlagSnapshot();
|
|
214
|
-
const bundle = getBundleInfo();
|
|
215
|
-
const crumbs = getBreadcrumbs();
|
|
216
|
-
const event: Event = {
|
|
217
|
-
id: uuidV7(),
|
|
218
|
-
timestamp: new Date().toISOString(),
|
|
219
|
-
kind: 'error',
|
|
220
|
-
platform: 'javascript',
|
|
221
|
-
release: config.release,
|
|
222
|
-
environment: config.environment,
|
|
223
|
-
device: collectDevice(),
|
|
224
|
-
app: collectApp(config.release),
|
|
225
|
-
user: extras?.user ?? _user,
|
|
226
|
-
tags: mergeScopeTags(extras?.tags),
|
|
227
|
-
...(flags ? { flags } : {}),
|
|
228
|
-
...(bundle ? { bundle } : {}),
|
|
229
|
-
breadcrumbs: crumbs,
|
|
230
|
-
error: errorToObject(error),
|
|
231
|
-
fingerprint: extras?.fingerprint,
|
|
232
|
-
};
|
|
233
|
-
// v0.9.8 — dev-only diagnostic. Insight saw `breadcrumbs: []` on
|
|
234
|
-
// every event in 0.9.7 despite handlers being installed; this line
|
|
235
|
-
// makes it visible in Metro that the snapshot at captureException
|
|
236
|
-
// time really is empty (no breadcrumb events fired yet) vs. having
|
|
237
|
-
// been silently dropped on the wire. Production builds gate out.
|
|
238
|
-
logger.debug('capture', 'captureException',
|
|
239
|
-
'eventId=', event.id,
|
|
240
|
-
'breadcrumbs=', crumbs.length,
|
|
241
|
-
'wantScreenshot=', config.screenshotsEnabled && extras?.screenshot !== false,
|
|
242
|
-
'wantSessionTrail=', config.sessionTrailEnabled,
|
|
243
|
-
'wantReplay=', isReplayRunning(),
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
// Phase 26 sub-B: a captured error promotes the current session to
|
|
247
|
-
// `errored` so the next AppState=background ping reports unhealthy.
|
|
248
|
-
markSessionErrored();
|
|
249
|
-
|
|
250
|
-
// Phase 42 sub-D.07: opt-in screenshot. Default off; per-call
|
|
251
|
-
// `extras.screenshot: false` always wins so callers can mute it
|
|
252
|
-
// on a sensitive flow even when init has it on globally.
|
|
253
|
-
const wantScreenshot =
|
|
254
|
-
config.screenshotsEnabled && extras?.screenshot !== false && allowScreenshot();
|
|
255
|
-
|
|
256
|
-
// Phase 40 sub-E: in dev there's no uploaded source map, so ask
|
|
257
|
-
// Metro to symbolicate the stack before we send it (best-effort,
|
|
258
|
-
// short timeout). Release builds skip straight to enqueue and let
|
|
259
|
-
// the server symbolicate at ingest against the uploaded map.
|
|
260
|
-
const pipeline = async (): Promise<void> => {
|
|
261
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
262
|
-
await symbolicateErrorViaMetro(event.error!).catch(() => {});
|
|
263
|
-
}
|
|
264
|
-
if (wantScreenshot) {
|
|
265
|
-
await captureAndAttachScreenshot(event);
|
|
266
|
-
}
|
|
267
|
-
const trail = getTrailBuffer();
|
|
268
|
-
if (config.sessionTrailEnabled && trail.size() > 0) {
|
|
269
|
-
await captureAndAttachSessionTrail(event);
|
|
270
|
-
}
|
|
271
|
-
// v0.9.2 +S2 — state time-travel attachment. Only if anything has
|
|
272
|
-
// been bound or recorded; cleared on success so the next crash's
|
|
273
|
-
// ring doesn't carry stale entries.
|
|
274
|
-
const stateSnapshots = getStateSnapshots();
|
|
275
|
-
if (stateSnapshots.length > 0) {
|
|
276
|
-
await captureAndAttachStateSnapshots(event, stateSnapshots);
|
|
277
|
-
clearStateSnapshots();
|
|
278
|
-
}
|
|
279
|
-
// v0.9.6 #2 — wireframe replay attachment. drainReplay clears the
|
|
280
|
-
// ring as a side effect so next session's replay starts fresh.
|
|
281
|
-
const replayNdjson = drainReplay();
|
|
282
|
-
if (replayNdjson.length > 0) {
|
|
283
|
-
await captureAndAttachReplay(event, replayNdjson);
|
|
284
|
-
} else if (typeof __DEV__ !== 'undefined' && __DEV__ && isReplayRunning()) {
|
|
285
|
-
// rc.4 — explicit "replay was on but ring drained empty" signal.
|
|
286
|
-
// Without this, "kinds=screenshot,sessionTrail" looks
|
|
287
|
-
// indistinguishable from `replay: off` even though the ticks
|
|
288
|
-
// were healthy upstream. Insight 2026-05-18 verify shape made
|
|
289
|
-
// this gap painful to triage.
|
|
290
|
-
logger.debug('capture', 'replay drain empty (no frames buffered at captureException)',
|
|
291
|
-
'eventId=', event.id,
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
logger.debug('capture', 'enqueue',
|
|
295
|
-
'eventId=', event.id,
|
|
296
|
-
'attachments=', event.attachments?.length ?? 0,
|
|
297
|
-
'kinds=', (event.attachments ?? []).map((a) => a.kind).join(',') || '(none)',
|
|
298
|
-
'breadcrumbsAtEnqueue=', __peekBreadcrumbCount(),
|
|
299
|
-
);
|
|
300
|
-
// v2.3 — host beforeSend hook. Sync; drops on null, falls back
|
|
301
|
-
// to unmutated event on throw or non-event return.
|
|
302
|
-
const finalEvent = applyBeforeSend(event, config.beforeSend);
|
|
303
|
-
if (finalEvent === null) return;
|
|
304
|
-
enqueue(finalEvent);
|
|
305
|
-
};
|
|
306
|
-
void pipeline();
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* v2.3 — invoke the host's `beforeSend` hook (if any) under the
|
|
311
|
-
* NEVER rule. Returns the (possibly mutated) event, or null to
|
|
312
|
-
* drop. A throw / non-event return is treated as a no-op
|
|
313
|
-
* (one-shot warn + send unmodified) so a buggy host hook can't
|
|
314
|
-
* stall captures.
|
|
315
|
-
*/
|
|
316
|
-
let _beforeSendThrewWarned = false;
|
|
317
|
-
function applyBeforeSend(event: Event, hook: BeforeSendHook | undefined): Event | null {
|
|
318
|
-
if (!hook) return event;
|
|
319
|
-
try {
|
|
320
|
-
const result = hook(event);
|
|
321
|
-
if (result === null) {
|
|
322
|
-
logger.debug('capture', 'beforeSend dropped event', 'eventId=', event.id);
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
if (typeof result !== 'object' || !result || typeof (result as Event).id !== 'string') {
|
|
326
|
-
// Host returned a non-event shape; fall back.
|
|
327
|
-
if (!_beforeSendThrewWarned) {
|
|
328
|
-
_beforeSendThrewWarned = true;
|
|
329
|
-
logger.warn('capture', 'beforeSend returned non-event shape; falling back to unmodified event');
|
|
330
|
-
}
|
|
331
|
-
return event;
|
|
332
|
-
}
|
|
333
|
-
return result;
|
|
334
|
-
} catch (e) {
|
|
335
|
-
if (!_beforeSendThrewWarned) {
|
|
336
|
-
_beforeSendThrewWarned = true;
|
|
337
|
-
logger.warn('capture', 'beforeSend threw; falling back to unmodified event', e);
|
|
338
|
-
}
|
|
339
|
-
return event;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** v0.9.6 #2 — upload the wireframe replay ring as a `replay`
|
|
344
|
-
* attachment. Plain NDJSON (one snapshot per line) — server may
|
|
345
|
-
* gzip on storage; the network upload is base64.
|
|
346
|
-
*
|
|
347
|
-
* rc.4: route through `base64Utf8` so non-Latin-1 text inside any
|
|
348
|
-
* walked TextView (Japanese / Chinese / em-dash etc.) doesn't blow
|
|
349
|
-
* up the Hermes-spec `btoa`. The pre-rc.4 inline `btoa(ndjson)` path
|
|
350
|
-
* threw `InvalidCharacterError` on those code points, the
|
|
351
|
-
* surrounding catch swallowed it silently, and the replay
|
|
352
|
-
* attachment never landed. Insight 2026-05-18 verify caught it
|
|
353
|
-
* after rc.3's walker fix surfaced deep TextView content. Dev
|
|
354
|
-
* logs replace the silent catch so the next failure shape is
|
|
355
|
-
* visible. */
|
|
356
|
-
async function captureAndAttachReplay(event: Event, ndjson: string): Promise<void> {
|
|
357
|
-
try {
|
|
358
|
-
const base64 = base64Utf8(ndjson);
|
|
359
|
-
const meta = await uploadAttachment(
|
|
360
|
-
event.id,
|
|
361
|
-
'replay',
|
|
362
|
-
{ base64, mediaType: 'application/x-ndjson' },
|
|
363
|
-
{ source: 'js' },
|
|
364
|
-
);
|
|
365
|
-
if (!meta) {
|
|
366
|
-
logger.debug('capture', 'replay upload returned null',
|
|
367
|
-
'eventId=', event.id,
|
|
368
|
-
'ndjsonBytes=', ndjson.length,
|
|
369
|
-
);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (!event.attachments) event.attachments = [];
|
|
373
|
-
event.attachments.push(meta);
|
|
374
|
-
} catch (e) {
|
|
375
|
-
logger.debug('capture', 'replay attachment threw',
|
|
376
|
-
'eventId=', event.id,
|
|
377
|
-
'ndjsonBytes=', ndjson.length,
|
|
378
|
-
e,
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/** v0.9.2 +S2 — upload the rolling state-snapshot ring as a
|
|
384
|
-
* `stateSnapshot` attachment so the dashboard time-travel viewer can
|
|
385
|
-
* scrub through diffs alongside the breadcrumb timeline. */
|
|
386
|
-
async function captureAndAttachStateSnapshots(
|
|
387
|
-
event: Event,
|
|
388
|
-
snapshots: ReturnType<typeof getStateSnapshots>,
|
|
389
|
-
): Promise<void> {
|
|
390
|
-
try {
|
|
391
|
-
const payload = JSON.stringify({ snapshots });
|
|
392
|
-
const base64 = base64Utf8(payload);
|
|
393
|
-
const meta = await uploadAttachment(
|
|
394
|
-
event.id,
|
|
395
|
-
'stateSnapshot',
|
|
396
|
-
{ base64, mediaType: 'application/json' },
|
|
397
|
-
{ source: 'js' },
|
|
398
|
-
);
|
|
399
|
-
if (meta) {
|
|
400
|
-
if (!event.attachments) event.attachments = [];
|
|
401
|
-
event.attachments.push(meta);
|
|
402
|
-
}
|
|
403
|
-
} catch {
|
|
404
|
-
// best-effort
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Phase 46 — seal the trail buffer, upload it as a `sessionTrail`
|
|
410
|
-
* attachment, attach the ref. Best-effort: any failure leaves a
|
|
411
|
-
* breadcrumb and lets the event ship without the trail.
|
|
412
|
-
*
|
|
413
|
-
* The trail is **always cleared** after `captureException`, even if
|
|
414
|
-
* upload fails — we don't want a stale 30-step buffer leaking into
|
|
415
|
-
* the next crash's trail.
|
|
416
|
-
*/
|
|
417
|
-
async function captureAndAttachSessionTrail(event: Event): Promise<void> {
|
|
418
|
-
const trail = getTrailBuffer();
|
|
419
|
-
const payload = sealTrail(trail);
|
|
420
|
-
trail.clear();
|
|
421
|
-
const json = JSON.stringify(payload);
|
|
422
|
-
const base64 = base64Utf8(json);
|
|
423
|
-
const attachment = await uploadAttachment(
|
|
424
|
-
event.id,
|
|
425
|
-
'sessionTrail',
|
|
426
|
-
{ base64, mediaType: 'application/json' },
|
|
427
|
-
{ source: 'js' },
|
|
428
|
-
);
|
|
429
|
-
if (!attachment) {
|
|
430
|
-
addBreadcrumb({ type: 'custom', data: { reason: 'session-trail-upload-failed' } });
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
if (!event.attachments) event.attachments = [];
|
|
434
|
-
event.attachments.push(attachment);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export const captureException = captureError;
|
|
438
|
-
|
|
439
|
-
const DEFAULT_MESSAGE_LEVEL: MessageLevel = 'info';
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Manually report an issue without an Error instance.
|
|
443
|
-
*
|
|
444
|
-
* Routes to the dashboard Issues module — distinct from `track`
|
|
445
|
-
* (analytics) and `recordMetric` (numeric). Use for "operator should
|
|
446
|
-
* look at this" signals: a fallback that fired, an unexpected state,
|
|
447
|
-
* a feature flag rollout that crossed a threshold.
|
|
448
|
-
*
|
|
449
|
-
* sentori.captureMessage('Payment provider returned 500, used fallback')
|
|
450
|
-
* sentori.captureMessage('Detected impossible state in session reducer', {
|
|
451
|
-
* level: 'error',
|
|
452
|
-
* tags: { reducer: 'session' },
|
|
453
|
-
* })
|
|
454
|
-
*
|
|
455
|
-
* Wrapped in `safeFn` per the NEVER rule — any internal failure is
|
|
456
|
-
* swallowed and (optionally) self-reported; the host app never sees
|
|
457
|
-
* a thrown error.
|
|
458
|
-
*/
|
|
459
|
-
export const captureMessage = safeFn(
|
|
460
|
-
'captureMessage',
|
|
461
|
-
(message: string, opts: CaptureMessageOptions = {}): void => {
|
|
462
|
-
if (!isInitialized()) return;
|
|
463
|
-
if (typeof message !== 'string' || message.length === 0) return;
|
|
464
|
-
const config = getConfig();
|
|
465
|
-
if (!config) return;
|
|
466
|
-
|
|
467
|
-
if (!shouldSample(config.messageSampleRate)) {
|
|
468
|
-
addBreadcrumb({ type: 'custom', data: { reason: 'sampled-out', kind: 'message' } });
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const flags = getFeatureFlagSnapshot();
|
|
473
|
-
const bundle = getBundleInfo();
|
|
474
|
-
const crumbs = opts.breadcrumbs ?? getBreadcrumbs();
|
|
475
|
-
|
|
476
|
-
const event: Event = {
|
|
477
|
-
id: uuidV7(),
|
|
478
|
-
timestamp: new Date().toISOString(),
|
|
479
|
-
kind: 'message',
|
|
480
|
-
level: opts.level ?? DEFAULT_MESSAGE_LEVEL,
|
|
481
|
-
message,
|
|
482
|
-
platform: 'javascript',
|
|
483
|
-
release: config.release,
|
|
484
|
-
environment: config.environment,
|
|
485
|
-
device: collectDevice(),
|
|
486
|
-
app: collectApp(config.release),
|
|
487
|
-
user: opts.user ?? _user,
|
|
488
|
-
tags: mergeScopeTags(opts.tags),
|
|
489
|
-
...(flags ? { flags } : {}),
|
|
490
|
-
...(bundle ? { bundle } : {}),
|
|
491
|
-
breadcrumbs: crumbs,
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
const finalEvent = applyBeforeSend(event, config.beforeSend);
|
|
495
|
-
if (finalEvent === null) return;
|
|
496
|
-
enqueue(finalEvent);
|
|
497
|
-
},
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
/** rc.4 — test hook. The real replay attach path is internal so we
|
|
501
|
-
* don't bloat the public surface, but the encoding bug Insight hit
|
|
502
|
-
* on 2026-05-18 needs a behaviour-level test that exercises the
|
|
503
|
-
* same code path captureException runs in production. */
|
|
504
|
-
export const __captureAndAttachReplayForTests = captureAndAttachReplay;
|
|
505
|
-
|
|
506
|
-
/** v2.3 — test hook for the beforeSend dispatcher. The dispatcher
|
|
507
|
-
* is internal so the unit test reaches it directly without
|
|
508
|
-
* needing to stub the whole transport pipeline. */
|
|
509
|
-
export const __applyBeforeSendForTests = applyBeforeSend;
|
|
510
|
-
|
|
511
|
-
/** Phase 42 sub-D.08: per-session screenshot quota gate. */
|
|
512
|
-
function allowScreenshot(): boolean {
|
|
513
|
-
const budget = screenshotBudget();
|
|
514
|
-
if (budget < 0) return true; // dev: unlimited
|
|
515
|
-
if (_screenshotsTaken >= budget) return false;
|
|
516
|
-
_screenshotsTaken += 1;
|
|
517
|
-
return true;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Phase 42 sub-D.06/07: take a screenshot, upload it, push the
|
|
522
|
-
* server-issued ref into `event.attachments`. Every step is
|
|
523
|
-
* best-effort — on any failure we leave a breadcrumb and let the
|
|
524
|
-
* event ship without a thumbnail.
|
|
525
|
-
*/
|
|
526
|
-
async function captureAndAttachScreenshot(event: Event): Promise<void> {
|
|
527
|
-
let blob: Awaited<ReturnType<typeof captureScreenshot>> = null;
|
|
528
|
-
try {
|
|
529
|
-
blob = await captureScreenshot();
|
|
530
|
-
} catch (e) {
|
|
531
|
-
logger.debug('capture', 'screenshot capture threw', e);
|
|
532
|
-
}
|
|
533
|
-
if (!blob) {
|
|
534
|
-
logger.debug('capture', 'screenshot blob null — native module missing or capture returned null',
|
|
535
|
-
'eventId=', event.id,
|
|
536
|
-
);
|
|
537
|
-
addBreadcrumb({ type: 'custom', data: { reason: 'screenshot-capture-failed' } });
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
logger.debug('capture', 'screenshot blob ok, uploading',
|
|
541
|
-
'eventId=', event.id,
|
|
542
|
-
'mediaType=', blob.mediaType,
|
|
543
|
-
'base64Bytes=', blob.base64.length,
|
|
544
|
-
);
|
|
545
|
-
const attachment: AttachmentMeta | null = await uploadAttachment(
|
|
546
|
-
event.id,
|
|
547
|
-
'screenshot',
|
|
548
|
-
blob,
|
|
549
|
-
{ source: 'js' },
|
|
550
|
-
);
|
|
551
|
-
if (!attachment) {
|
|
552
|
-
logger.debug('capture', 'screenshot upload returned null', 'eventId=', event.id);
|
|
553
|
-
addBreadcrumb({ type: 'custom', data: { reason: 'screenshot-upload-failed' } });
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
if (!event.attachments) event.attachments = [];
|
|
557
|
-
event.attachments.push(attachment);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const errorToObject = (error: Error): SentoriError => {
|
|
561
|
-
const causeRaw = (error as { cause?: unknown }).cause;
|
|
562
|
-
let cause: SentoriError | null = null;
|
|
563
|
-
if (causeRaw instanceof Error) {
|
|
564
|
-
cause = errorToObject(causeRaw);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// v0.9.5 #8 — TurboModule swallowed-exception bridge. If the host
|
|
568
|
-
// wrapped a native call with `@try @catch + recordException`, the
|
|
569
|
-
// native ring may hold a fresh entry (< 1 s old). Synthesize that
|
|
570
|
-
// as a `cause` so the JS event includes the original native stack.
|
|
571
|
-
if (cause === null) {
|
|
572
|
-
const recent = getRecentNativeException();
|
|
573
|
-
if (recent && recent.ageMs <= 1500) {
|
|
574
|
-
cause = {
|
|
575
|
-
type: recent.name || 'NativeException',
|
|
576
|
-
message: recent.reason,
|
|
577
|
-
stack: recent.stack.map((line, i) => ({
|
|
578
|
-
function: line.trim(),
|
|
579
|
-
file: '<native>',
|
|
580
|
-
inApp: false,
|
|
581
|
-
line: i + 1,
|
|
582
|
-
})),
|
|
583
|
-
cause: null,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
type: error.name || 'Error',
|
|
590
|
-
message: error.message,
|
|
591
|
-
stack: parseStack(error.stack),
|
|
592
|
-
cause,
|
|
593
|
-
};
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
const collectDevice = (): Device => {
|
|
597
|
-
let os: Device['os'] = 'other';
|
|
598
|
-
let osVersion = '0';
|
|
599
|
-
let locale: string | undefined;
|
|
600
|
-
const networkType = getCachedNetworkType();
|
|
601
|
-
try {
|
|
602
|
-
const RN = require('react-native') as {
|
|
603
|
-
NativeModules: {
|
|
604
|
-
I18nManager?: { localeIdentifier?: string };
|
|
605
|
-
SettingsManager?: {
|
|
606
|
-
settings?: { AppleLanguages?: string[]; AppleLocale?: string };
|
|
607
|
-
};
|
|
608
|
-
};
|
|
609
|
-
Platform: { OS: string; Version: string | number };
|
|
610
|
-
};
|
|
611
|
-
const rnOS = RN.Platform.OS;
|
|
612
|
-
os = rnOS === 'ios' || rnOS === 'android' || rnOS === 'web' ? rnOS : 'other';
|
|
613
|
-
osVersion = String(RN.Platform.Version);
|
|
614
|
-
// v0.8.0-a — RN reads user locale through native modules. These
|
|
615
|
-
// are stable RN-internal modules (SettingsManager since 0.4,
|
|
616
|
-
// I18nManager since 0.16) so we can read them directly without
|
|
617
|
-
// an extra peer dep. iOS returns e.g. "en_US"; Android returns
|
|
618
|
-
// e.g. "en_US" via `getDefault().toString()`. `AppleLocale` is
|
|
619
|
-
// the format the user picked in Settings; `AppleLanguages[0]`
|
|
620
|
-
// is the resolved language priority — prefer the former.
|
|
621
|
-
if (rnOS === 'ios') {
|
|
622
|
-
const s = RN.NativeModules.SettingsManager?.settings;
|
|
623
|
-
locale = s?.AppleLocale ?? s?.AppleLanguages?.[0];
|
|
624
|
-
} else if (rnOS === 'android') {
|
|
625
|
-
locale = RN.NativeModules.I18nManager?.localeIdentifier;
|
|
626
|
-
}
|
|
627
|
-
} catch {
|
|
628
|
-
// not in RN runtime (jest, bun test)
|
|
629
|
-
}
|
|
630
|
-
const device: Device = { os, osVersion };
|
|
631
|
-
if (locale) device.locale = locale;
|
|
632
|
-
if (networkType) device.networkType = networkType;
|
|
633
|
-
// v1.1 chunk S1 — stable per-install id. Read sync from the
|
|
634
|
-
// in-memory cache populated by the install-id module's first
|
|
635
|
-
// resolve (kicked off from init()). `null` before init resolves —
|
|
636
|
-
// the field is omitted in that case rather than blocking event
|
|
637
|
-
// assembly, which sits on the JS thread.
|
|
638
|
-
const installId = peekInstallId();
|
|
639
|
-
if (installId) device.installId = installId;
|
|
640
|
-
return device;
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
const collectApp = (release: string): App => {
|
|
644
|
-
const m = /^(?:[^@]+@)?([^+]+)(?:\+(.+))?$/.exec(release);
|
|
645
|
-
const version = m?.[1] ?? '0.0.0';
|
|
646
|
-
const build = m?.[2];
|
|
647
|
-
|
|
648
|
-
let rnVersion = 'unknown';
|
|
649
|
-
try {
|
|
650
|
-
rnVersion = (require('react-native/package.json') as { version: string }).version;
|
|
651
|
-
} catch {
|
|
652
|
-
// not in RN runtime
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return {
|
|
656
|
-
version,
|
|
657
|
-
build,
|
|
658
|
-
framework: { name: 'react-native', version: rnVersion },
|
|
659
|
-
};
|
|
660
|
-
};
|