@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/feedback-widget.tsx
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
// v0.9.1 #9 — Feedback Widget.
|
|
2
|
-
//
|
|
3
|
-
// `<FeedbackButton trigger="shake|manual|fab" />` — drop into the
|
|
4
|
-
// app root, opens a modal prompt that submits via the existing
|
|
5
|
-
// `sentori.sendUserFeedback` API. Shake detection is opt-in and
|
|
6
|
-
// requires `expo-sensors` (Accelerometer) — falls back to manual
|
|
7
|
-
// trigger if not installed. The button can also be controlled
|
|
8
|
-
// programmatically via a ref: `feedbackRef.current.open()`.
|
|
9
|
-
//
|
|
10
|
-
// Aesthetic: very plain Modal + Text + TextInput so it adopts the
|
|
11
|
-
// host app's color scheme without forcing a sentori theme on it.
|
|
12
|
-
|
|
13
|
-
import React, {
|
|
14
|
-
forwardRef,
|
|
15
|
-
useCallback,
|
|
16
|
-
useEffect,
|
|
17
|
-
useImperativeHandle,
|
|
18
|
-
useRef,
|
|
19
|
-
useState,
|
|
20
|
-
} from 'react';
|
|
21
|
-
import {
|
|
22
|
-
Modal,
|
|
23
|
-
Pressable,
|
|
24
|
-
StyleSheet,
|
|
25
|
-
Text,
|
|
26
|
-
TextInput,
|
|
27
|
-
View,
|
|
28
|
-
} from 'react-native';
|
|
29
|
-
|
|
30
|
-
import { sendUserFeedback } from './capture';
|
|
31
|
-
import { isAnyNativeModuleLinked } from './native-loader';
|
|
32
|
-
|
|
33
|
-
type Trigger = 'fab' | 'manual' | 'shake';
|
|
34
|
-
|
|
35
|
-
export type FeedbackButtonHandle = {
|
|
36
|
-
/** Open the feedback modal. Returns immediately. */
|
|
37
|
-
open: (defaults?: { body?: string; eventId?: string; title?: string }) => void;
|
|
38
|
-
/** Close the modal if open. */
|
|
39
|
-
close: () => void;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type FeedbackButtonProps = {
|
|
43
|
-
/** When to surface the prompt. Default `'fab'`. */
|
|
44
|
-
trigger?: Trigger;
|
|
45
|
-
/** Pass the eventId from the last `captureException` to tie the
|
|
46
|
-
* report to that crash. Optional. */
|
|
47
|
-
eventId?: string;
|
|
48
|
-
/** Localized strings. Defaults are English. */
|
|
49
|
-
labels?: {
|
|
50
|
-
title?: string;
|
|
51
|
-
bodyPlaceholder?: string;
|
|
52
|
-
emailPlaceholder?: string;
|
|
53
|
-
submit?: string;
|
|
54
|
-
cancel?: string;
|
|
55
|
-
sent?: string;
|
|
56
|
-
};
|
|
57
|
-
/** Shake sensitivity in m/s² above gravity. Default 18 (≈ a normal
|
|
58
|
-
* intentional shake; lower triggers more easily). Only used when
|
|
59
|
-
* `trigger="shake"`. */
|
|
60
|
-
shakeThreshold?: number;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export const FeedbackButton = forwardRef<FeedbackButtonHandle, FeedbackButtonProps>(
|
|
64
|
-
function FeedbackButton(props, ref) {
|
|
65
|
-
const trigger: Trigger = props.trigger ?? 'fab';
|
|
66
|
-
const [open, setOpen] = useState(false);
|
|
67
|
-
const [title, setTitle] = useState('');
|
|
68
|
-
const [body, setBody] = useState('');
|
|
69
|
-
const [email, setEmail] = useState('');
|
|
70
|
-
const [submitting, setSubmitting] = useState(false);
|
|
71
|
-
const [sent, setSent] = useState(false);
|
|
72
|
-
const eventIdRef = useRef<string | undefined>(props.eventId);
|
|
73
|
-
|
|
74
|
-
useImperativeHandle(
|
|
75
|
-
ref,
|
|
76
|
-
() => ({
|
|
77
|
-
open: (defaults) => {
|
|
78
|
-
setTitle(defaults?.title ?? '');
|
|
79
|
-
setBody(defaults?.body ?? '');
|
|
80
|
-
eventIdRef.current = defaults?.eventId ?? props.eventId;
|
|
81
|
-
setSent(false);
|
|
82
|
-
setOpen(true);
|
|
83
|
-
},
|
|
84
|
-
close: () => setOpen(false),
|
|
85
|
-
}),
|
|
86
|
-
[props.eventId],
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Shake detection — opt-in. We load expo-sensors lazily so apps
|
|
90
|
-
// that don't install it never pay the bundle cost. Same native
|
|
91
|
-
// module guard as netinfo: if the JS package is in node_modules
|
|
92
|
-
// but native bridge isn't linked we'd otherwise crash inside the
|
|
93
|
-
// sensor emitter.
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
if (trigger !== 'shake') return;
|
|
96
|
-
if (!isAnyNativeModuleLinked(['ExponentAccelerometer', 'EXAccelerometer', 'ExpoAccelerometer'])) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
let sub: { remove: () => void } | null = null;
|
|
100
|
-
try {
|
|
101
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
102
|
-
const mod = require('expo-sensors') as {
|
|
103
|
-
Accelerometer?: {
|
|
104
|
-
addListener: (cb: (d: { x: number; y: number; z: number }) => void) => {
|
|
105
|
-
remove: () => void;
|
|
106
|
-
};
|
|
107
|
-
setUpdateInterval: (ms: number) => void;
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
const A = mod.Accelerometer;
|
|
111
|
-
if (!A) return;
|
|
112
|
-
A.setUpdateInterval(100);
|
|
113
|
-
const thr = props.shakeThreshold ?? 18;
|
|
114
|
-
let lastTriggerAt = 0;
|
|
115
|
-
sub = A.addListener(({ x, y, z }) => {
|
|
116
|
-
const mag = Math.sqrt(x * x + y * y + z * z) * 9.81; // g → m/s²
|
|
117
|
-
const now = Date.now();
|
|
118
|
-
if (mag > thr && now - lastTriggerAt > 1500) {
|
|
119
|
-
lastTriggerAt = now;
|
|
120
|
-
setSent(false);
|
|
121
|
-
setOpen(true);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
} catch {
|
|
125
|
-
// expo-sensors not installed → silently fall back to manual
|
|
126
|
-
}
|
|
127
|
-
return () => {
|
|
128
|
-
if (sub) sub.remove();
|
|
129
|
-
};
|
|
130
|
-
}, [trigger, props.shakeThreshold]);
|
|
131
|
-
|
|
132
|
-
const onSubmit = useCallback(async () => {
|
|
133
|
-
if (!title.trim() || !body.trim()) return;
|
|
134
|
-
setSubmitting(true);
|
|
135
|
-
try {
|
|
136
|
-
await sendUserFeedback({
|
|
137
|
-
title: title.trim().slice(0, 200),
|
|
138
|
-
body: body.trim().slice(0, 8000),
|
|
139
|
-
email: email.trim() || undefined,
|
|
140
|
-
eventId: eventIdRef.current,
|
|
141
|
-
});
|
|
142
|
-
setSent(true);
|
|
143
|
-
setTimeout(() => setOpen(false), 1200);
|
|
144
|
-
} finally {
|
|
145
|
-
setSubmitting(false);
|
|
146
|
-
}
|
|
147
|
-
}, [title, body, email]);
|
|
148
|
-
|
|
149
|
-
const L = {
|
|
150
|
-
bodyPlaceholder: 'What happened?',
|
|
151
|
-
cancel: 'Cancel',
|
|
152
|
-
emailPlaceholder: 'email (optional)',
|
|
153
|
-
sent: 'Thanks — report sent.',
|
|
154
|
-
submit: 'Send',
|
|
155
|
-
title: 'Report a problem',
|
|
156
|
-
...props.labels,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
return (
|
|
160
|
-
<>
|
|
161
|
-
{trigger === 'fab' && (
|
|
162
|
-
<Pressable
|
|
163
|
-
accessibilityLabel="Open feedback"
|
|
164
|
-
onPress={() => {
|
|
165
|
-
setSent(false);
|
|
166
|
-
setOpen(true);
|
|
167
|
-
}}
|
|
168
|
-
style={styles.fab}
|
|
169
|
-
>
|
|
170
|
-
<Text style={styles.fabText}>?</Text>
|
|
171
|
-
</Pressable>
|
|
172
|
-
)}
|
|
173
|
-
<Modal animationType="fade" transparent visible={open} onRequestClose={() => setOpen(false)}>
|
|
174
|
-
<View style={styles.backdrop}>
|
|
175
|
-
<View style={styles.card}>
|
|
176
|
-
{sent ? (
|
|
177
|
-
<Text style={styles.sentMessage}>{L.sent}</Text>
|
|
178
|
-
) : (
|
|
179
|
-
<>
|
|
180
|
-
<Text style={styles.heading}>{L.title}</Text>
|
|
181
|
-
<TextInput
|
|
182
|
-
autoCapitalize="sentences"
|
|
183
|
-
onChangeText={setTitle}
|
|
184
|
-
placeholder="Subject"
|
|
185
|
-
placeholderTextColor="#888"
|
|
186
|
-
style={styles.titleInput}
|
|
187
|
-
value={title}
|
|
188
|
-
/>
|
|
189
|
-
<TextInput
|
|
190
|
-
multiline
|
|
191
|
-
onChangeText={setBody}
|
|
192
|
-
placeholder={L.bodyPlaceholder}
|
|
193
|
-
placeholderTextColor="#888"
|
|
194
|
-
style={styles.bodyInput}
|
|
195
|
-
textAlignVertical="top"
|
|
196
|
-
value={body}
|
|
197
|
-
/>
|
|
198
|
-
<TextInput
|
|
199
|
-
autoCapitalize="none"
|
|
200
|
-
keyboardType="email-address"
|
|
201
|
-
onChangeText={setEmail}
|
|
202
|
-
placeholder={L.emailPlaceholder}
|
|
203
|
-
placeholderTextColor="#888"
|
|
204
|
-
style={styles.emailInput}
|
|
205
|
-
value={email}
|
|
206
|
-
/>
|
|
207
|
-
<View style={styles.actions}>
|
|
208
|
-
<Pressable onPress={() => setOpen(false)} style={styles.cancelBtn}>
|
|
209
|
-
<Text style={styles.cancelText}>{L.cancel}</Text>
|
|
210
|
-
</Pressable>
|
|
211
|
-
<Pressable
|
|
212
|
-
disabled={submitting || !title.trim() || !body.trim()}
|
|
213
|
-
onPress={onSubmit}
|
|
214
|
-
style={[styles.submitBtn, submitting && styles.submitBtnDisabled]}
|
|
215
|
-
>
|
|
216
|
-
<Text style={styles.submitText}>
|
|
217
|
-
{submitting ? '…' : L.submit}
|
|
218
|
-
</Text>
|
|
219
|
-
</Pressable>
|
|
220
|
-
</View>
|
|
221
|
-
</>
|
|
222
|
-
)}
|
|
223
|
-
</View>
|
|
224
|
-
</View>
|
|
225
|
-
</Modal>
|
|
226
|
-
</>
|
|
227
|
-
);
|
|
228
|
-
},
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
const styles = StyleSheet.create({
|
|
232
|
-
actions: {
|
|
233
|
-
flexDirection: 'row',
|
|
234
|
-
gap: 8,
|
|
235
|
-
justifyContent: 'flex-end',
|
|
236
|
-
},
|
|
237
|
-
backdrop: {
|
|
238
|
-
alignItems: 'center',
|
|
239
|
-
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
240
|
-
flex: 1,
|
|
241
|
-
justifyContent: 'center',
|
|
242
|
-
paddingHorizontal: 24,
|
|
243
|
-
},
|
|
244
|
-
bodyInput: {
|
|
245
|
-
backgroundColor: '#f7f7f8',
|
|
246
|
-
borderColor: '#e5e5e8',
|
|
247
|
-
borderRadius: 6,
|
|
248
|
-
borderWidth: 1,
|
|
249
|
-
color: '#111',
|
|
250
|
-
fontSize: 14,
|
|
251
|
-
marginBottom: 8,
|
|
252
|
-
minHeight: 100,
|
|
253
|
-
paddingHorizontal: 12,
|
|
254
|
-
paddingVertical: 10,
|
|
255
|
-
},
|
|
256
|
-
cancelBtn: {
|
|
257
|
-
paddingHorizontal: 14,
|
|
258
|
-
paddingVertical: 8,
|
|
259
|
-
},
|
|
260
|
-
cancelText: { color: '#666', fontSize: 14 },
|
|
261
|
-
card: {
|
|
262
|
-
backgroundColor: '#fff',
|
|
263
|
-
borderRadius: 10,
|
|
264
|
-
padding: 16,
|
|
265
|
-
width: '100%',
|
|
266
|
-
},
|
|
267
|
-
emailInput: {
|
|
268
|
-
backgroundColor: '#f7f7f8',
|
|
269
|
-
borderColor: '#e5e5e8',
|
|
270
|
-
borderRadius: 6,
|
|
271
|
-
borderWidth: 1,
|
|
272
|
-
color: '#111',
|
|
273
|
-
fontSize: 14,
|
|
274
|
-
marginBottom: 14,
|
|
275
|
-
paddingHorizontal: 12,
|
|
276
|
-
paddingVertical: 8,
|
|
277
|
-
},
|
|
278
|
-
fab: {
|
|
279
|
-
alignItems: 'center',
|
|
280
|
-
backgroundColor: '#111',
|
|
281
|
-
borderRadius: 24,
|
|
282
|
-
bottom: 28,
|
|
283
|
-
elevation: 4,
|
|
284
|
-
height: 48,
|
|
285
|
-
justifyContent: 'center',
|
|
286
|
-
position: 'absolute',
|
|
287
|
-
right: 18,
|
|
288
|
-
shadowColor: '#000',
|
|
289
|
-
shadowOffset: { height: 2, width: 0 },
|
|
290
|
-
shadowOpacity: 0.18,
|
|
291
|
-
shadowRadius: 4,
|
|
292
|
-
width: 48,
|
|
293
|
-
},
|
|
294
|
-
fabText: { color: '#fff', fontSize: 22, fontWeight: '500' },
|
|
295
|
-
heading: { color: '#111', fontSize: 18, fontWeight: '500', marginBottom: 12 },
|
|
296
|
-
sentMessage: { color: '#111', fontSize: 14, paddingVertical: 18, textAlign: 'center' },
|
|
297
|
-
submitBtn: {
|
|
298
|
-
backgroundColor: '#111',
|
|
299
|
-
borderRadius: 6,
|
|
300
|
-
paddingHorizontal: 16,
|
|
301
|
-
paddingVertical: 8,
|
|
302
|
-
},
|
|
303
|
-
submitBtnDisabled: { opacity: 0.5 },
|
|
304
|
-
submitText: { color: '#fff', fontSize: 14, fontWeight: '500' },
|
|
305
|
-
titleInput: {
|
|
306
|
-
backgroundColor: '#f7f7f8',
|
|
307
|
-
borderColor: '#e5e5e8',
|
|
308
|
-
borderRadius: 6,
|
|
309
|
-
borderWidth: 1,
|
|
310
|
-
color: '#111',
|
|
311
|
-
fontSize: 14,
|
|
312
|
-
marginBottom: 8,
|
|
313
|
-
paddingHorizontal: 12,
|
|
314
|
-
paddingVertical: 8,
|
|
315
|
-
},
|
|
316
|
-
});
|
package/src/feedback.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// v2.0 W3 — `feedback` subpath. The feedback widget pulls in a
|
|
2
|
-
// component tree (react + tsx) and an internal viewer route, so
|
|
3
|
-
// importing it eagerly from the SDK's top-level barrel pays a
|
|
4
|
-
// bundle cost every consumer pays — even ones that never render
|
|
5
|
-
// the widget.
|
|
6
|
-
//
|
|
7
|
-
// Subpath import `@goliapkg/sentori-react-native/feedback`
|
|
8
|
-
// resolves to just the widget surface. Hosts that don't render
|
|
9
|
-
// the feedback button pay zero bundle delta; hosts that do reach
|
|
10
|
-
// for it import via this module and get exactly what they need.
|
|
11
|
-
//
|
|
12
|
-
// Top-level (`index.ts`) still re-exports `FeedbackButton` for one
|
|
13
|
-
// more release cycle so v1 callsites don't break on upgrade. v3
|
|
14
|
-
// will drop the top-level re-export — see
|
|
15
|
-
// `docs/recipes/v1-to-v2-migration.md` for the deprecation timeline.
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
FeedbackButton,
|
|
19
|
-
type FeedbackButtonHandle,
|
|
20
|
-
type FeedbackButtonProps,
|
|
21
|
-
} from './feedback-widget'
|
package/src/heartbeat.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
// Analytics v1 — concurrent-user heartbeat.
|
|
2
|
-
//
|
|
3
|
-
// Once per minute, while the app is in the foreground, POST a tiny
|
|
4
|
-
// `{ sessionId, userId?, release, route?, os?, ts }` body to
|
|
5
|
-
// `/v1/heartbeat`. The server keeps a per-project Valkey ZSET keyed
|
|
6
|
-
// by member (user.id when set, sessionId otherwise) and the
|
|
7
|
-
// dashboard's `Live` page reads the set to render concurrent-user
|
|
8
|
-
// count + per-dim breakdowns.
|
|
9
|
-
//
|
|
10
|
-
// Iron-rule budget (CLAUDE.md):
|
|
11
|
-
// - 1 POST / min foreground only — never fires when backgrounded
|
|
12
|
-
// - ~200 B body
|
|
13
|
-
// - fire-and-forget; never blocks the JS thread, no retries
|
|
14
|
-
// - bounce suppression: if AppState flaps active → inactive → active
|
|
15
|
-
// inside 30 s, we still fire at most one heartbeat per 30 s
|
|
16
|
-
//
|
|
17
|
-
// The heartbeat is independent of the session ping in
|
|
18
|
-
// `session-tracker.ts`. Session pings fire only at session close
|
|
19
|
-
// (transport-batched); the heartbeat exists *during* the session to
|
|
20
|
-
// signal presence.
|
|
21
|
-
|
|
22
|
-
import { logger } from '@goliapkg/sentori-core';
|
|
23
|
-
|
|
24
|
-
import { getConfig } from './config';
|
|
25
|
-
import { getUser } from './capture';
|
|
26
|
-
import { getLastRoute } from './navigation';
|
|
27
|
-
import { uuidV7 } from './uuid';
|
|
28
|
-
|
|
29
|
-
declare const __DEV__: boolean | undefined;
|
|
30
|
-
|
|
31
|
-
const DEFAULT_INTERVAL_MS = 60_000;
|
|
32
|
-
const MIN_GAP_MS = 30_000;
|
|
33
|
-
|
|
34
|
-
type AppStateLike = {
|
|
35
|
-
addEventListener: (
|
|
36
|
-
event: 'change',
|
|
37
|
-
handler: (state: string) => void
|
|
38
|
-
) => { remove: () => void };
|
|
39
|
-
currentState?: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
let _running = false;
|
|
43
|
-
let _timer: ReturnType<typeof setInterval> | null = null;
|
|
44
|
-
let _appStateSub: null | { remove: () => void } = null;
|
|
45
|
-
let _sessionId: null | string = null;
|
|
46
|
-
let _lastBeatTs = 0;
|
|
47
|
-
let _intervalMs = DEFAULT_INTERVAL_MS;
|
|
48
|
-
|
|
49
|
-
export type HeartbeatOptions = {
|
|
50
|
-
/** Override the default 60 s interval. Floor 10 s — anything below
|
|
51
|
-
* trips the perf rule and the server's rate-limit anyway. */
|
|
52
|
-
intervalMs?: number;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export function startHeartbeat(opts: HeartbeatOptions = {}): void {
|
|
56
|
-
if (_running) return;
|
|
57
|
-
_running = true;
|
|
58
|
-
_intervalMs = Math.max(10_000, opts.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
59
|
-
_sessionId = uuidV7();
|
|
60
|
-
|
|
61
|
-
// AppState gate — only beat while app is in the foreground.
|
|
62
|
-
let AppState: AppStateLike | undefined;
|
|
63
|
-
try {
|
|
64
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
65
|
-
AppState = (require('react-native') as { AppState?: AppStateLike }).AppState;
|
|
66
|
-
} catch {
|
|
67
|
-
// Not in RN runtime (tests). The interval still runs; the gate
|
|
68
|
-
// is just permissive. Suppression below still applies.
|
|
69
|
-
AppState = undefined;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const isForeground = (): boolean => {
|
|
73
|
-
if (!AppState) return true;
|
|
74
|
-
return (AppState.currentState ?? 'active') === 'active';
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const beat = () => {
|
|
78
|
-
if (!_running) return;
|
|
79
|
-
if (!isForeground()) return;
|
|
80
|
-
const now = Date.now();
|
|
81
|
-
if (now - _lastBeatTs < MIN_GAP_MS) return;
|
|
82
|
-
_lastBeatTs = now;
|
|
83
|
-
void send();
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// First beat as soon as we start (so the dashboard sees the user
|
|
87
|
-
// immediately, not 60 s after launch). Subsequent fires on the
|
|
88
|
-
// interval. AppState transitions can poke an immediate beat too —
|
|
89
|
-
// an active resume is a meaningful presence event.
|
|
90
|
-
beat();
|
|
91
|
-
_timer = setInterval(beat, _intervalMs);
|
|
92
|
-
|
|
93
|
-
if (AppState && typeof AppState.addEventListener === 'function') {
|
|
94
|
-
_appStateSub = AppState.addEventListener('change', (state) => {
|
|
95
|
-
if (state === 'active') beat();
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function stopHeartbeat(): void {
|
|
101
|
-
_running = false;
|
|
102
|
-
if (_timer !== null) {
|
|
103
|
-
clearInterval(_timer);
|
|
104
|
-
_timer = null;
|
|
105
|
-
}
|
|
106
|
-
if (_appStateSub) {
|
|
107
|
-
_appStateSub.remove();
|
|
108
|
-
_appStateSub = null;
|
|
109
|
-
}
|
|
110
|
-
_sessionId = null;
|
|
111
|
-
_lastBeatTs = 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function send(): Promise<void> {
|
|
115
|
-
const config = getConfig();
|
|
116
|
-
if (!config) return;
|
|
117
|
-
const user = getUser();
|
|
118
|
-
const body: Record<string, unknown> = {
|
|
119
|
-
sessionId: _sessionId ?? '',
|
|
120
|
-
release: config.release,
|
|
121
|
-
ts: Date.now(),
|
|
122
|
-
};
|
|
123
|
-
if (user?.id) body.userId = user.id;
|
|
124
|
-
const route = getLastRoute();
|
|
125
|
-
if (route) body.route = route;
|
|
126
|
-
const os = readOsString();
|
|
127
|
-
if (os) body.os = os;
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
await fetch(`${config.ingestUrl}/v1/heartbeat`, {
|
|
131
|
-
body: JSON.stringify(body),
|
|
132
|
-
headers: {
|
|
133
|
-
Authorization: `Bearer ${config.token}`,
|
|
134
|
-
'Content-Type': 'application/json',
|
|
135
|
-
},
|
|
136
|
-
method: 'POST',
|
|
137
|
-
});
|
|
138
|
-
} catch (e) {
|
|
139
|
-
logger.debug('heartbeat', 'failed (best-effort, normal on offline)', e);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function readOsString(): null | string {
|
|
144
|
-
try {
|
|
145
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
146
|
-
const RN = require('react-native') as {
|
|
147
|
-
Platform: { OS: string; Version: string | number };
|
|
148
|
-
};
|
|
149
|
-
return `${RN.Platform.OS} ${RN.Platform.Version}`;
|
|
150
|
-
} catch {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function __resetHeartbeatForTests(): void {
|
|
156
|
-
stopHeartbeat();
|
|
157
|
-
}
|
package/src/install-id.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
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
|
-
}
|