@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.
Files changed (284) hide show
  1. package/android/build.gradle +21 -0
  2. package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
  3. package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
  4. package/android/src/test/resources/robolectric.properties +10 -0
  5. package/lib/config.d.ts +16 -47
  6. package/lib/config.d.ts.map +1 -1
  7. package/lib/config.js +3 -0
  8. package/lib/config.js.map +1 -1
  9. package/lib/device.d.ts +5 -0
  10. package/lib/device.d.ts.map +1 -0
  11. package/lib/device.js +29 -0
  12. package/lib/device.js.map +1 -0
  13. package/lib/emit-hooks.d.ts +7 -0
  14. package/lib/emit-hooks.d.ts.map +1 -0
  15. package/lib/emit-hooks.js +24 -0
  16. package/lib/emit-hooks.js.map +1 -0
  17. package/lib/error-boundary.d.ts.map +1 -1
  18. package/lib/error-boundary.js +1 -1
  19. package/lib/error-boundary.js.map +1 -1
  20. package/lib/feature-flags.d.ts.map +1 -1
  21. package/lib/handlers/dev-symbolicate.d.ts +1 -1
  22. package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
  23. package/lib/handlers/dev-symbolicate.js +2 -0
  24. package/lib/handlers/dev-symbolicate.js.map +1 -1
  25. package/lib/handlers/global.js +1 -1
  26. package/lib/handlers/global.js.map +1 -1
  27. package/lib/handlers/lifecycle.js +3 -3
  28. package/lib/handlers/lifecycle.js.map +1 -1
  29. package/lib/handlers/network.d.ts +2 -2
  30. package/lib/handlers/network.d.ts.map +1 -1
  31. package/lib/handlers/network.js +69 -187
  32. package/lib/handlers/network.js.map +1 -1
  33. package/lib/handlers/promise.js +1 -1
  34. package/lib/handlers/promise.js.map +1 -1
  35. package/lib/handlers/screenshot.d.ts.map +1 -1
  36. package/lib/handlers/screenshot.js +3 -12
  37. package/lib/handlers/screenshot.js.map +1 -1
  38. package/lib/index.d.ts +31 -109
  39. package/lib/index.d.ts.map +1 -1
  40. package/lib/index.js +51 -103
  41. package/lib/index.js.map +1 -1
  42. package/lib/init.d.ts +3 -150
  43. package/lib/init.d.ts.map +1 -1
  44. package/lib/init.js +88 -261
  45. package/lib/init.js.map +1 -1
  46. package/lib/long-task-monitor.d.ts +1 -7
  47. package/lib/long-task-monitor.d.ts.map +1 -1
  48. package/lib/long-task-monitor.js +37 -58
  49. package/lib/long-task-monitor.js.map +1 -1
  50. package/lib/mobile-vitals.d.ts +3 -31
  51. package/lib/mobile-vitals.d.ts.map +1 -1
  52. package/lib/mobile-vitals.js +22 -61
  53. package/lib/mobile-vitals.js.map +1 -1
  54. package/lib/native-pending.d.ts +3 -0
  55. package/lib/native-pending.d.ts.map +1 -0
  56. package/lib/native-pending.js +64 -0
  57. package/lib/native-pending.js.map +1 -0
  58. package/lib/navigation.d.ts +3 -17
  59. package/lib/navigation.d.ts.map +1 -1
  60. package/lib/navigation.js +32 -133
  61. package/lib/navigation.js.map +1 -1
  62. package/lib/netinfo.d.ts +1 -1
  63. package/lib/netinfo.d.ts.map +1 -1
  64. package/lib/netinfo.js +2 -2
  65. package/lib/netinfo.js.map +1 -1
  66. package/lib/push.d.ts.map +1 -1
  67. package/lib/push.js +2 -4
  68. package/lib/push.js.map +1 -1
  69. package/lib/rage-tap.d.ts.map +1 -1
  70. package/lib/rage-tap.js +28 -23
  71. package/lib/rage-tap.js.map +1 -1
  72. package/lib/replay.d.ts.map +1 -1
  73. package/lib/replay.js +3 -24
  74. package/lib/replay.js.map +1 -1
  75. package/lib/scope.d.ts +7 -0
  76. package/lib/scope.d.ts.map +1 -0
  77. package/lib/scope.js +41 -0
  78. package/lib/scope.js.map +1 -0
  79. package/lib/transport.d.ts +19 -92
  80. package/lib/transport.d.ts.map +1 -1
  81. package/lib/transport.js +75 -288
  82. package/lib/transport.js.map +1 -1
  83. package/lib/verbs.d.ts +16 -0
  84. package/lib/verbs.d.ts.map +1 -0
  85. package/lib/verbs.js +153 -0
  86. package/lib/verbs.js.map +1 -0
  87. package/package.json +5 -5
  88. package/src/__tests__/iron-rule.test.ts +128 -0
  89. package/src/__tests__/screenshot.test.ts +0 -2
  90. package/src/__tests__/transport.test.ts +66 -213
  91. package/src/__tests__/verbs.test.ts +91 -0
  92. package/src/config.ts +19 -47
  93. package/src/device.ts +36 -0
  94. package/src/emit-hooks.ts +30 -0
  95. package/src/error-boundary.tsx +1 -1
  96. package/src/handlers/dev-symbolicate.ts +2 -1
  97. package/src/handlers/global.ts +1 -1
  98. package/src/handlers/lifecycle.ts +3 -3
  99. package/src/handlers/network.ts +95 -211
  100. package/src/handlers/promise.ts +1 -1
  101. package/src/handlers/screenshot.ts +3 -11
  102. package/src/index.ts +81 -215
  103. package/src/init.ts +94 -446
  104. package/src/long-task-monitor.ts +42 -64
  105. package/src/mobile-vitals.ts +22 -82
  106. package/src/native-pending.ts +92 -0
  107. package/src/navigation.ts +34 -142
  108. package/src/netinfo.ts +3 -3
  109. package/src/push.ts +2 -5
  110. package/src/rage-tap.tsx +29 -26
  111. package/src/replay.ts +3 -20
  112. package/src/scope.ts +50 -0
  113. package/src/transport.ts +84 -371
  114. package/src/verbs.ts +198 -0
  115. package/lib/breadcrumbs.d.ts +0 -18
  116. package/lib/breadcrumbs.d.ts.map +0 -1
  117. package/lib/breadcrumbs.js +0 -42
  118. package/lib/breadcrumbs.js.map +0 -1
  119. package/lib/bundle-info.d.ts +0 -12
  120. package/lib/bundle-info.d.ts.map +0 -1
  121. package/lib/bundle-info.js +0 -73
  122. package/lib/bundle-info.js.map +0 -1
  123. package/lib/capture.d.ts +0 -140
  124. package/lib/capture.d.ts.map +0 -1
  125. package/lib/capture.js +0 -511
  126. package/lib/capture.js.map +0 -1
  127. package/lib/compat/sentry.d.ts +0 -126
  128. package/lib/compat/sentry.d.ts.map +0 -1
  129. package/lib/compat/sentry.js +0 -332
  130. package/lib/compat/sentry.js.map +0 -1
  131. package/lib/control-channel.d.ts +0 -6
  132. package/lib/control-channel.d.ts.map +0 -1
  133. package/lib/control-channel.js +0 -96
  134. package/lib/control-channel.js.map +0 -1
  135. package/lib/expo-compat.d.ts +0 -270
  136. package/lib/expo-compat.d.ts.map +0 -1
  137. package/lib/expo-compat.js +0 -500
  138. package/lib/expo-compat.js.map +0 -1
  139. package/lib/feedback-widget.d.ts +0 -35
  140. package/lib/feedback-widget.d.ts.map +0 -1
  141. package/lib/feedback-widget.js +0 -193
  142. package/lib/feedback-widget.js.map +0 -1
  143. package/lib/feedback.d.ts +0 -2
  144. package/lib/feedback.d.ts.map +0 -1
  145. package/lib/feedback.js +0 -17
  146. package/lib/feedback.js.map +0 -1
  147. package/lib/heartbeat.d.ts +0 -9
  148. package/lib/heartbeat.d.ts.map +0 -1
  149. package/lib/heartbeat.js +0 -138
  150. package/lib/heartbeat.js.map +0 -1
  151. package/lib/index.js.bak +0 -64
  152. package/lib/install-id.d.ts +0 -17
  153. package/lib/install-id.d.ts.map +0 -1
  154. package/lib/install-id.js +0 -125
  155. package/lib/install-id.js.map +0 -1
  156. package/lib/lifecycle.d.ts +0 -28
  157. package/lib/lifecycle.d.ts.map +0 -1
  158. package/lib/lifecycle.js +0 -73
  159. package/lib/lifecycle.js.map +0 -1
  160. package/lib/mask.d.ts +0 -16
  161. package/lib/mask.d.ts.map +0 -1
  162. package/lib/mask.js +0 -36
  163. package/lib/mask.js.map +0 -1
  164. package/lib/measure.d.ts +0 -4
  165. package/lib/measure.d.ts.map +0 -1
  166. package/lib/measure.js +0 -25
  167. package/lib/measure.js.map +0 -1
  168. package/lib/metrics.d.ts +0 -24
  169. package/lib/metrics.d.ts.map +0 -1
  170. package/lib/metrics.js +0 -81
  171. package/lib/metrics.js.map +0 -1
  172. package/lib/pre-crash-sentinel.d.ts +0 -12
  173. package/lib/pre-crash-sentinel.d.ts.map +0 -1
  174. package/lib/pre-crash-sentinel.js +0 -116
  175. package/lib/pre-crash-sentinel.js.map +0 -1
  176. package/lib/report-security.d.ts +0 -40
  177. package/lib/report-security.d.ts.map +0 -1
  178. package/lib/report-security.js +0 -159
  179. package/lib/report-security.js.map +0 -1
  180. package/lib/runtime-metrics-fps.d.ts +0 -19
  181. package/lib/runtime-metrics-fps.d.ts.map +0 -1
  182. package/lib/runtime-metrics-fps.js +0 -128
  183. package/lib/runtime-metrics-fps.js.map +0 -1
  184. package/lib/runtime-metrics-heap.d.ts +0 -13
  185. package/lib/runtime-metrics-heap.d.ts.map +0 -1
  186. package/lib/runtime-metrics-heap.js +0 -67
  187. package/lib/runtime-metrics-heap.js.map +0 -1
  188. package/lib/runtime-metrics-network.d.ts +0 -23
  189. package/lib/runtime-metrics-network.d.ts.map +0 -1
  190. package/lib/runtime-metrics-network.js +0 -99
  191. package/lib/runtime-metrics-network.js.map +0 -1
  192. package/lib/runtime-metrics.d.ts +0 -43
  193. package/lib/runtime-metrics.d.ts.map +0 -1
  194. package/lib/runtime-metrics.js +0 -115
  195. package/lib/runtime-metrics.js.map +0 -1
  196. package/lib/sample-profiler.d.ts +0 -21
  197. package/lib/sample-profiler.d.ts.map +0 -1
  198. package/lib/sample-profiler.js +0 -152
  199. package/lib/sample-profiler.js.map +0 -1
  200. package/lib/sentinel-context.d.ts +0 -4
  201. package/lib/sentinel-context.d.ts.map +0 -1
  202. package/lib/sentinel-context.js +0 -52
  203. package/lib/sentinel-context.js.map +0 -1
  204. package/lib/session-tracker.d.ts +0 -6
  205. package/lib/session-tracker.d.ts.map +0 -1
  206. package/lib/session-tracker.js +0 -50
  207. package/lib/session-tracker.js.map +0 -1
  208. package/lib/stack.d.ts +0 -3
  209. package/lib/stack.d.ts.map +0 -1
  210. package/lib/stack.js +0 -7
  211. package/lib/stack.js.map +0 -1
  212. package/lib/state-snapshots.d.ts +0 -33
  213. package/lib/state-snapshots.d.ts.map +0 -1
  214. package/lib/state-snapshots.js +0 -109
  215. package/lib/state-snapshots.js.map +0 -1
  216. package/lib/track.d.ts +0 -34
  217. package/lib/track.d.ts.map +0 -1
  218. package/lib/track.js +0 -107
  219. package/lib/track.js.map +0 -1
  220. package/lib/trail.d.ts +0 -12
  221. package/lib/trail.d.ts.map +0 -1
  222. package/lib/trail.js +0 -28
  223. package/lib/trail.js.map +0 -1
  224. package/lib/trust-score.d.ts +0 -20
  225. package/lib/trust-score.d.ts.map +0 -1
  226. package/lib/trust-score.js +0 -151
  227. package/lib/trust-score.js.map +0 -1
  228. package/lib/types.d.ts +0 -2
  229. package/lib/types.d.ts.map +0 -1
  230. package/lib/types.js +0 -4
  231. package/lib/types.js.map +0 -1
  232. package/lib/uuid.d.ts +0 -2
  233. package/lib/uuid.d.ts.map +0 -1
  234. package/lib/uuid.js +0 -3
  235. package/lib/uuid.js.map +0 -1
  236. package/src/__tests__/before-send.test.ts +0 -72
  237. package/src/__tests__/breadcrumbs.test.ts +0 -44
  238. package/src/__tests__/capture-replay.test.ts +0 -150
  239. package/src/__tests__/compat-sentry.test.ts +0 -121
  240. package/src/__tests__/control-channel.test.ts +0 -111
  241. package/src/__tests__/install-id.test.ts +0 -60
  242. package/src/__tests__/long-task-monitor.test.ts +0 -59
  243. package/src/__tests__/measure.test.ts +0 -45
  244. package/src/__tests__/navigation.test.ts +0 -175
  245. package/src/__tests__/push.test.ts +0 -238
  246. package/src/__tests__/report-security.test.ts +0 -106
  247. package/src/__tests__/runtime-metrics-instruments.test.ts +0 -109
  248. package/src/__tests__/runtime-metrics-network.test.ts +0 -78
  249. package/src/__tests__/sample-profiler.test.ts +0 -59
  250. package/src/__tests__/stack.test.ts +0 -45
  251. package/src/__tests__/state-snapshots.test.ts +0 -119
  252. package/src/__tests__/tracing.test.ts +0 -201
  253. package/src/__tests__/track.test.ts +0 -91
  254. package/src/__tests__/uuid.test.ts +0 -41
  255. package/src/breadcrumbs.ts +0 -57
  256. package/src/bundle-info.ts +0 -95
  257. package/src/capture.ts +0 -660
  258. package/src/compat/sentry.ts +0 -490
  259. package/src/control-channel.ts +0 -98
  260. package/src/expo-compat.ts +0 -698
  261. package/src/feedback-widget.tsx +0 -316
  262. package/src/feedback.ts +0 -21
  263. package/src/heartbeat.ts +0 -157
  264. package/src/install-id.ts +0 -146
  265. package/src/lifecycle.ts +0 -76
  266. package/src/mask.ts +0 -41
  267. package/src/measure.ts +0 -28
  268. package/src/metrics.ts +0 -94
  269. package/src/pre-crash-sentinel.ts +0 -140
  270. package/src/report-security.ts +0 -165
  271. package/src/runtime-metrics-fps.ts +0 -137
  272. package/src/runtime-metrics-heap.ts +0 -78
  273. package/src/runtime-metrics-network.ts +0 -103
  274. package/src/runtime-metrics.ts +0 -122
  275. package/src/sample-profiler.ts +0 -169
  276. package/src/sentinel-context.ts +0 -60
  277. package/src/session-tracker.ts +0 -53
  278. package/src/stack.ts +0 -10
  279. package/src/state-snapshots.ts +0 -133
  280. package/src/track.ts +0 -123
  281. package/src/trail.ts +0 -32
  282. package/src/trust-score.ts +0 -176
  283. package/src/types.ts +0 -21
  284. package/src/uuid.ts +0 -2
@@ -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
- }