@goliapkg/sentori-react-native 0.8.0 → 0.8.2

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.
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ type Trigger = 'fab' | 'manual' | 'shake';
3
+ export type FeedbackButtonHandle = {
4
+ /** Open the feedback modal. Returns immediately. */
5
+ open: (defaults?: {
6
+ body?: string;
7
+ eventId?: string;
8
+ title?: string;
9
+ }) => void;
10
+ /** Close the modal if open. */
11
+ close: () => void;
12
+ };
13
+ export type FeedbackButtonProps = {
14
+ /** When to surface the prompt. Default `'fab'`. */
15
+ trigger?: Trigger;
16
+ /** Pass the eventId from the last `captureException` to tie the
17
+ * report to that crash. Optional. */
18
+ eventId?: string;
19
+ /** Localized strings. Defaults are English. */
20
+ labels?: {
21
+ title?: string;
22
+ bodyPlaceholder?: string;
23
+ emailPlaceholder?: string;
24
+ submit?: string;
25
+ cancel?: string;
26
+ sent?: string;
27
+ };
28
+ /** Shake sensitivity in m/s² above gravity. Default 18 (≈ a normal
29
+ * intentional shake; lower triggers more easily). Only used when
30
+ * `trigger="shake"`. */
31
+ shakeThreshold?: number;
32
+ };
33
+ export declare const FeedbackButton: React.ForwardRefExoticComponent<FeedbackButtonProps & React.RefAttributes<FeedbackButtonHandle>>;
34
+ export {};
35
+ //# sourceMappingURL=feedback-widget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback-widget.d.ts","sourceRoot":"","sources":["../src/feedback-widget.tsx"],"names":[],"mappings":"AAYA,OAAO,KAON,MAAM,OAAO,CAAC;AAYf,KAAK,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1C,MAAM,MAAM,oBAAoB,GAAG;IACjC,oDAAoD;IACpD,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/E,+BAA+B;IAC/B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,mDAAmD;IACnD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;0CACsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF;;6BAEyB;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,eAAO,MAAM,cAAc,kGAgK1B,CAAC"}
@@ -0,0 +1,186 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // v0.9.1 #9 — Feedback Widget.
3
+ //
4
+ // `<FeedbackButton trigger="shake|manual|fab" />` — drop into the
5
+ // app root, opens a modal prompt that submits via the existing
6
+ // `sentori.sendUserFeedback` API. Shake detection is opt-in and
7
+ // requires `expo-sensors` (Accelerometer) — falls back to manual
8
+ // trigger if not installed. The button can also be controlled
9
+ // programmatically via a ref: `feedbackRef.current.open()`.
10
+ //
11
+ // Aesthetic: very plain Modal + Text + TextInput so it adopts the
12
+ // host app's color scheme without forcing a sentori theme on it.
13
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from 'react';
14
+ import { Modal, Pressable, StyleSheet, Text, TextInput, View, } from 'react-native';
15
+ import { sendUserFeedback } from './capture';
16
+ export const FeedbackButton = forwardRef(function FeedbackButton(props, ref) {
17
+ const trigger = props.trigger ?? 'fab';
18
+ const [open, setOpen] = useState(false);
19
+ const [title, setTitle] = useState('');
20
+ const [body, setBody] = useState('');
21
+ const [email, setEmail] = useState('');
22
+ const [submitting, setSubmitting] = useState(false);
23
+ const [sent, setSent] = useState(false);
24
+ const eventIdRef = useRef(props.eventId);
25
+ useImperativeHandle(ref, () => ({
26
+ open: (defaults) => {
27
+ setTitle(defaults?.title ?? '');
28
+ setBody(defaults?.body ?? '');
29
+ eventIdRef.current = defaults?.eventId ?? props.eventId;
30
+ setSent(false);
31
+ setOpen(true);
32
+ },
33
+ close: () => setOpen(false),
34
+ }), [props.eventId]);
35
+ // Shake detection — opt-in. We load expo-sensors lazily so apps
36
+ // that don't install it never pay the bundle cost.
37
+ useEffect(() => {
38
+ if (trigger !== 'shake')
39
+ return;
40
+ let sub = null;
41
+ try {
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
43
+ const mod = require('expo-sensors');
44
+ const A = mod.Accelerometer;
45
+ if (!A)
46
+ return;
47
+ A.setUpdateInterval(100);
48
+ const thr = props.shakeThreshold ?? 18;
49
+ let lastTriggerAt = 0;
50
+ sub = A.addListener(({ x, y, z }) => {
51
+ const mag = Math.sqrt(x * x + y * y + z * z) * 9.81; // g → m/s²
52
+ const now = Date.now();
53
+ if (mag > thr && now - lastTriggerAt > 1500) {
54
+ lastTriggerAt = now;
55
+ setSent(false);
56
+ setOpen(true);
57
+ }
58
+ });
59
+ }
60
+ catch {
61
+ // expo-sensors not installed → silently fall back to manual
62
+ }
63
+ return () => {
64
+ if (sub)
65
+ sub.remove();
66
+ };
67
+ }, [trigger, props.shakeThreshold]);
68
+ const onSubmit = useCallback(async () => {
69
+ if (!title.trim() || !body.trim())
70
+ return;
71
+ setSubmitting(true);
72
+ try {
73
+ await sendUserFeedback({
74
+ title: title.trim().slice(0, 200),
75
+ body: body.trim().slice(0, 8000),
76
+ email: email.trim() || undefined,
77
+ eventId: eventIdRef.current,
78
+ });
79
+ setSent(true);
80
+ setTimeout(() => setOpen(false), 1200);
81
+ }
82
+ finally {
83
+ setSubmitting(false);
84
+ }
85
+ }, [title, body, email]);
86
+ const L = {
87
+ bodyPlaceholder: 'What happened?',
88
+ cancel: 'Cancel',
89
+ emailPlaceholder: 'email (optional)',
90
+ sent: 'Thanks — report sent.',
91
+ submit: 'Send',
92
+ title: 'Report a problem',
93
+ ...props.labels,
94
+ };
95
+ return (_jsxs(_Fragment, { children: [trigger === 'fab' && (_jsx(Pressable, { accessibilityLabel: "Open feedback", onPress: () => {
96
+ setSent(false);
97
+ setOpen(true);
98
+ }, style: styles.fab, children: _jsx(Text, { style: styles.fabText, children: "?" }) })), _jsx(Modal, { animationType: "fade", transparent: true, visible: open, onRequestClose: () => setOpen(false), children: _jsx(View, { style: styles.backdrop, children: _jsx(View, { style: styles.card, children: sent ? (_jsx(Text, { style: styles.sentMessage, children: L.sent })) : (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.heading, children: L.title }), _jsx(TextInput, { autoCapitalize: "sentences", onChangeText: setTitle, placeholder: "Subject", placeholderTextColor: "#888", style: styles.titleInput, value: title }), _jsx(TextInput, { multiline: true, onChangeText: setBody, placeholder: L.bodyPlaceholder, placeholderTextColor: "#888", style: styles.bodyInput, textAlignVertical: "top", value: body }), _jsx(TextInput, { autoCapitalize: "none", keyboardType: "email-address", onChangeText: setEmail, placeholder: L.emailPlaceholder, placeholderTextColor: "#888", style: styles.emailInput, value: email }), _jsxs(View, { style: styles.actions, children: [_jsx(Pressable, { onPress: () => setOpen(false), style: styles.cancelBtn, children: _jsx(Text, { style: styles.cancelText, children: L.cancel }) }), _jsx(Pressable, { disabled: submitting || !title.trim() || !body.trim(), onPress: onSubmit, style: [styles.submitBtn, submitting && styles.submitBtnDisabled], children: _jsx(Text, { style: styles.submitText, children: submitting ? '…' : L.submit }) })] })] })) }) }) })] }));
99
+ });
100
+ const styles = StyleSheet.create({
101
+ actions: {
102
+ flexDirection: 'row',
103
+ gap: 8,
104
+ justifyContent: 'flex-end',
105
+ },
106
+ backdrop: {
107
+ alignItems: 'center',
108
+ backgroundColor: 'rgba(0,0,0,0.5)',
109
+ flex: 1,
110
+ justifyContent: 'center',
111
+ paddingHorizontal: 24,
112
+ },
113
+ bodyInput: {
114
+ backgroundColor: '#f7f7f8',
115
+ borderColor: '#e5e5e8',
116
+ borderRadius: 6,
117
+ borderWidth: 1,
118
+ color: '#111',
119
+ fontSize: 14,
120
+ marginBottom: 8,
121
+ minHeight: 100,
122
+ paddingHorizontal: 12,
123
+ paddingVertical: 10,
124
+ },
125
+ cancelBtn: {
126
+ paddingHorizontal: 14,
127
+ paddingVertical: 8,
128
+ },
129
+ cancelText: { color: '#666', fontSize: 14 },
130
+ card: {
131
+ backgroundColor: '#fff',
132
+ borderRadius: 10,
133
+ padding: 16,
134
+ width: '100%',
135
+ },
136
+ emailInput: {
137
+ backgroundColor: '#f7f7f8',
138
+ borderColor: '#e5e5e8',
139
+ borderRadius: 6,
140
+ borderWidth: 1,
141
+ color: '#111',
142
+ fontSize: 14,
143
+ marginBottom: 14,
144
+ paddingHorizontal: 12,
145
+ paddingVertical: 8,
146
+ },
147
+ fab: {
148
+ alignItems: 'center',
149
+ backgroundColor: '#111',
150
+ borderRadius: 24,
151
+ bottom: 28,
152
+ elevation: 4,
153
+ height: 48,
154
+ justifyContent: 'center',
155
+ position: 'absolute',
156
+ right: 18,
157
+ shadowColor: '#000',
158
+ shadowOffset: { height: 2, width: 0 },
159
+ shadowOpacity: 0.18,
160
+ shadowRadius: 4,
161
+ width: 48,
162
+ },
163
+ fabText: { color: '#fff', fontSize: 22, fontWeight: '500' },
164
+ heading: { color: '#111', fontSize: 18, fontWeight: '500', marginBottom: 12 },
165
+ sentMessage: { color: '#111', fontSize: 14, paddingVertical: 18, textAlign: 'center' },
166
+ submitBtn: {
167
+ backgroundColor: '#111',
168
+ borderRadius: 6,
169
+ paddingHorizontal: 16,
170
+ paddingVertical: 8,
171
+ },
172
+ submitBtnDisabled: { opacity: 0.5 },
173
+ submitText: { color: '#fff', fontSize: 14, fontWeight: '500' },
174
+ titleInput: {
175
+ backgroundColor: '#f7f7f8',
176
+ borderColor: '#e5e5e8',
177
+ borderRadius: 6,
178
+ borderWidth: 1,
179
+ color: '#111',
180
+ fontSize: 14,
181
+ marginBottom: 8,
182
+ paddingHorizontal: 12,
183
+ paddingVertical: 8,
184
+ },
185
+ });
186
+ //# sourceMappingURL=feedback-widget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feedback-widget.js","sourceRoot":"","sources":["../src/feedback-widget.tsx"],"names":[],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ErrorBoundary } from './error-boundary';
2
+ import { type FeedbackButtonHandle, type FeedbackButtonProps } from './feedback-widget';
2
3
  import { clearMaskQuery, registerMaskQuery } from './mask';
3
4
  import { measureFn } from './measure';
4
5
  import { startMoment } from '@goliapkg/sentori-core';
@@ -31,6 +32,7 @@ export declare const sentori: {
31
32
  clearAllFeatureFlags: () => void;
32
33
  getFeatureFlags: () => Record<string, string>;
33
34
  ErrorBoundary: typeof ErrorBoundary;
35
+ FeedbackButton: import("react").ForwardRefExoticComponent<FeedbackButtonProps & import("react").RefAttributes<FeedbackButtonHandle>>;
34
36
  RageTapCapture: typeof RageTapCapture;
35
37
  registerMaskQuery: typeof registerMaskQuery;
36
38
  clearMaskQuery: typeof clearMaskQuery;
@@ -43,6 +45,7 @@ export { init, init as initSentori } from './init';
43
45
  export { addBreadcrumb } from './breadcrumbs';
44
46
  export { captureError, captureException, captureStep, getUser, sendUserFeedback, setUser, } from './capture';
45
47
  export { ErrorBoundary } from './error-boundary';
48
+ export { FeedbackButton, type FeedbackButtonHandle, type FeedbackButtonProps } from './feedback-widget';
46
49
  export { clearAllFeatureFlags, clearFeatureFlag, getFeatureFlags, setFeatureFlag, } from './feature-flags';
47
50
  export { clearMaskQuery, registerMaskQuery } from './mask';
48
51
  export { flushMetrics, recordMetric } from './metrics';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAOjD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAO5C,eAAO,MAAM,OAAO;;;;;;;;;;aA4EmpB,CAAC;eAAmB,CAAC;YAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;CApD5sB,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,gBAAgB,EAChB,OAAO,GACR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAE1E,YAAY,EACV,KAAK,EACL,YAAY,EACZ,KAAK,EACL,UAAU,EACV,cAAc,EACd,MAAM,EACN,QAAQ,EACR,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAkB,KAAK,oBAAoB,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAOxG,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAO5C,eAAO,MAAM,OAAO;;;;;;;;;;aA8E+a,CAAC;eAAmB,CAAC;YAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;CArDxe,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,gBAAgB,EAChB,OAAO,GACR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,KAAK,oBAAoB,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxG,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAE1E,YAAY,EACV,KAAK,EACL,YAAY,EACZ,KAAK,EACL,UAAU,EACV,cAAc,EACd,MAAM,EACN,QAAQ,EACR,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,MAAM,SAAS,CAAC"}
package/lib/index.js CHANGED
@@ -2,6 +2,7 @@ import { init } from './init';
2
2
  import { addBreadcrumb } from './breadcrumbs';
3
3
  import { captureError, captureException, captureStep, getUser, sendUserFeedback, setUser, } from './capture';
4
4
  import { ErrorBoundary } from './error-boundary';
5
+ import { FeedbackButton } from './feedback-widget';
5
6
  import { clearAllFeatureFlags, clearFeatureFlag, getFeatureFlags, setFeatureFlag, } from './feature-flags';
6
7
  import { clearMaskQuery, registerMaskQuery } from './mask';
7
8
  import { measureFn } from './measure';
@@ -27,6 +28,7 @@ export const sentori = {
27
28
  clearAllFeatureFlags,
28
29
  getFeatureFlags,
29
30
  ErrorBoundary,
31
+ FeedbackButton,
30
32
  RageTapCapture,
31
33
  registerMaskQuery,
32
34
  clearMaskQuery,
@@ -39,6 +41,7 @@ export { init, init as initSentori } from './init';
39
41
  export { addBreadcrumb } from './breadcrumbs';
40
42
  export { captureError, captureException, captureStep, getUser, sendUserFeedback, setUser, } from './capture';
41
43
  export { ErrorBoundary } from './error-boundary';
44
+ export { FeedbackButton } from './feedback-widget';
42
45
  export { clearAllFeatureFlags, clearFeatureFlag, getFeatureFlags, setFeatureFlag, } from './feature-flags';
43
46
  export { clearMaskQuery, registerMaskQuery } from './mask';
44
47
  export { flushMetrics, recordMetric } from './metrics';
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,gBAAgB,EAChB,OAAO,GACR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,IAAI;IACJ,aAAa;IACb,OAAO;IACP,OAAO;IACP,YAAY;IACZ,gBAAgB;IAChB,WAAW;IACX,gBAAgB;IAChB,YAAY;IACZ,YAAY;IACZ,SAAS;IACT,WAAW;IACX,cAAc;IACd,gBAAgB;IAChB,oBAAoB;IACpB,eAAe;IACf,aAAa;IACb,cAAc;IACd,iBAAiB;IACjB,cAAc;IACd,YAAY;IACZ,UAAU;IACV,kBAAkB;CACnB,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,gBAAgB,EAChB,OAAO,GACR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,YAAY,EAAyB,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAA0B,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,gBAAgB,EAChB,OAAO,GACR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAuD,MAAM,mBAAmB,CAAC;AACxG,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,IAAI;IACJ,aAAa;IACb,OAAO;IACP,OAAO;IACP,YAAY;IACZ,gBAAgB;IAChB,WAAW;IACX,gBAAgB;IAChB,YAAY;IACZ,YAAY;IACZ,SAAS;IACT,WAAW;IACX,cAAc;IACd,gBAAgB;IAChB,oBAAoB;IACpB,eAAe;IACf,aAAa;IACb,cAAc;IACd,cAAc;IACd,iBAAiB;IACjB,cAAc;IACd,YAAY;IACZ,UAAU;IACV,kBAAkB;CACnB,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACX,OAAO,EACP,gBAAgB,EAChB,OAAO,GACR,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAuD,MAAM,mBAAmB,CAAC;AACxG,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,YAAY,EAAyB,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAA0B,kBAAkB,EAAE,MAAM,cAAc,CAAC"}
package/lib/init.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type PreCrashChannel } from './pre-crash-sentinel';
1
2
  import type { AttachmentMeta } from './types';
2
3
  export type InitOptions = {
3
4
  /** Project token starting with `st_pk_`. Required. */
@@ -36,6 +37,13 @@ export type InitOptions = {
36
37
  * the buffer is sealed and uploaded as a `sessionTrail`
37
38
  * attachment. Defaults to false. */
38
39
  sessionTrail?: boolean;
40
+ /** v0.9.1 +S4 — pre-crash sentinel. Subscribes to JS-thread
41
+ * frame timing; when ≥ 50% of a 60-frame window misses the
42
+ * budget (default 32 ms / < 30 fps), emits a `kind: nearCrash`
43
+ * event proactively so dashboards see the "about-to-die"
44
+ * signal before an actual crash. */
45
+ preCrashSentinel?: boolean;
46
+ sentinelChannels?: PreCrashChannel[];
39
47
  /** v0.9.0 #3 — launch-crash loop guard. When two consecutive
40
48
  * launches don't reach `markLaunchCompleted()` (typical of an
41
49
  * OTA update with a fatal bug), invoke the host callback with
package/lib/init.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAkB,cAAc,EAA2B,MAAM,SAAS,CAAC;AAIvF,MAAM,MAAM,WAAW,GAAG;IACxB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,OAAO,CAAC,EACJ,OAAO,GACP;YACE;;qEAEyD;YACzD,OAAO,CAAC,EAAE,OAAO,CAAC;SACnB,CAAC;QACN;;8DAEsD;QACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB;;;;;;;uBAOe;QACf,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB;;;6CAGqC;QACrC,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB;;;sEAG8D;QAC9D,gBAAgB,CAAC,EAAE;YACjB,OAAO,EAAE,OAAO,CAAC;YACjB,qBAAqB,CAAC,EAAE,CACtB,IAAI,EAAE,OAAO,sBAAsB,EAAE,eAAe,KAElD,OAAO,sBAAsB,EAAE,iBAAiB,GAChD,OAAO,CAAC,OAAO,sBAAsB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;YACnB,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH,CAAC;IACF;;;;;gEAK4D;IAC5D,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;KACxB,CAAC;CACH,CAAC;AAIF,eAAO,MAAM,IAAI,GAAI,SAAS,WAAW,KAAG,IAoH3C,CAAC;AAiBF,YAAY,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAaA,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQnF,OAAO,KAAK,EAAkB,cAAc,EAA2B,MAAM,SAAS,CAAC;AAIvF,MAAM,MAAM,WAAW,GAAG;IACxB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,OAAO,CAAC,EAAE;QACR,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,OAAO,CAAC,EACJ,OAAO,GACP;YACE;;qEAEyD;YACzD,OAAO,CAAC,EAAE,OAAO,CAAC;SACnB,CAAC;QACN;;8DAEsD;QACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB;;;;;;;uBAOe;QACf,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB;;;6CAGqC;QACrC,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB;;;;6CAIqC;QACrC,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;QACrC;;;sEAG8D;QAC9D,gBAAgB,CAAC,EAAE;YACjB,OAAO,EAAE,OAAO,CAAC;YACjB,qBAAqB,CAAC,EAAE,CACtB,IAAI,EAAE,OAAO,sBAAsB,EAAE,eAAe,KAElD,OAAO,sBAAsB,EAAE,iBAAiB,GAChD,OAAO,CAAC,OAAO,sBAAsB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;YACnB,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH,CAAC;IACF;;;;;gEAK4D;IAC5D,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;KACxB,CAAC;CACH,CAAC;AAIF,eAAO,MAAM,IAAI,GAAI,SAAS,WAAW,KAAG,IA4H3C,CAAC;AAiBF,YAAY,EAAE,cAAc,EAAE,CAAC"}
package/lib/init.js CHANGED
@@ -8,6 +8,7 @@ import { markLaunchCompleted, runLaunchCrashGuard, } from './launch-crash-guard'
8
8
  import { startMetricsTimer } from './metrics';
9
9
  import { drainNativePending, setNativeConfig } from './native';
10
10
  import { startNetworkTypeWatch } from './netinfo';
11
+ import { startPreCrashSentinel } from './pre-crash-sentinel';
11
12
  import { startSession } from './session-tracker';
12
13
  import { drainOfflineQueue, enqueue, startTransport, uploadAttachment, } from './transport';
13
14
  const DEFAULT_INGEST_URL = 'https://ingest.sentori.golia.jp';
@@ -53,6 +54,14 @@ export const init = (options) => {
53
54
  startNetworkTypeWatch();
54
55
  // v0.8.3 — drain custom-metric ring every 30 s.
55
56
  startMetricsTimer();
57
+ // v0.9.1 +S4 — pre-crash sentinel. Off by default; opt-in via
58
+ // `capture.preCrashSentinel: true`.
59
+ if (options.capture?.preCrashSentinel === true) {
60
+ startPreCrashSentinel({
61
+ enabled: true,
62
+ channels: options.capture.sentinelChannels,
63
+ });
64
+ }
56
65
  const capture = options.capture ?? {};
57
66
  if (capture.globalErrors !== false)
58
67
  installGlobalHandler();
package/lib/init.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export type PreCrashChannel = 'frame-budget-overrun' | 'memory-pressure' | 'oom-warning' | 'storage-low';
2
+ export type PreCrashSentinelOptions = {
3
+ enabled: boolean;
4
+ channels?: PreCrashChannel[];
5
+ /** Lower → more sensitive. Default 32 ms (< 30 fps). */
6
+ frameBudgetMs?: number;
7
+ /** Fraction of frames in the window that must miss budget. Default 0.5. */
8
+ tripRatio?: number;
9
+ };
10
+ export declare function startPreCrashSentinel(opts: PreCrashSentinelOptions): void;
11
+ export declare function stopPreCrashSentinel(): void;
12
+ //# sourceMappingURL=pre-crash-sentinel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pre-crash-sentinel.d.ts","sourceRoot":"","sources":["../src/pre-crash-sentinel.ts"],"names":[],"mappings":"AAmCA,MAAM,MAAM,eAAe,GACvB,sBAAsB,GACtB,iBAAiB,GACjB,aAAa,GACb,aAAa,CAAC;AAElB,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;IAC7B,wDAAwD;IACxD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,uBAAuB,GAAG,IAAI,CAUzE;AAED,wBAAgB,oBAAoB,IAAI,IAAI,CAK3C"}
@@ -0,0 +1,116 @@
1
+ // v0.9.1 +S4 — pre-crash sentinel.
2
+ //
3
+ // Predictive (vs reactive) telemetry. Subscribes to JS-thread frame
4
+ // timing via requestAnimationFrame and, when a rolling 60-frame
5
+ // window has ≥ 50% of frames slower than 32 ms (i.e. < 30 fps for
6
+ // half the window), emits an `event.kind = nearCrash` to the server.
7
+ // Backend stores it in the same events stream so the dashboard shows
8
+ // "X user sessions had near-crash signals 4 minutes before the
9
+ // actual NSException".
10
+ //
11
+ // Memory pressure / OOM / storage low signals will need native
12
+ // system observers and ship in v1.0. v0.9.1 covers the most common
13
+ // runaway-render-loop case purely from JS.
14
+ import { startSpan } from '@goliapkg/sentori-core';
15
+ import { getBundleInfo } from './bundle-info';
16
+ import { collectDeviceForSentinel, getAppForSentinel } from './sentinel-context';
17
+ import { getConfig, isInitialized } from './config';
18
+ import { enqueue } from './transport';
19
+ import { uuidV7 } from './uuid';
20
+ const FRAME_BUDGET_MS = 32; // < 30 fps
21
+ const WINDOW_FRAMES = 60; // ~1 s at 60 fps
22
+ const TRIP_RATIO = 0.5;
23
+ const COOLDOWN_MS = 60_000; // don't spam: one nearCrash event per minute
24
+ let _running = false;
25
+ let _lastFrameAt = 0;
26
+ let _slowFrames = 0;
27
+ let _totalFrames = 0;
28
+ let _lastEmitAt = 0;
29
+ let _channels = new Set();
30
+ export function startPreCrashSentinel(opts) {
31
+ if (!opts.enabled || _running)
32
+ return;
33
+ _running = true;
34
+ _channels = new Set(opts.channels ?? ['frame-budget-overrun']);
35
+ if (_channels.has('frame-budget-overrun')) {
36
+ startFrameBudgetWatch(opts.frameBudgetMs ?? FRAME_BUDGET_MS, opts.tripRatio ?? TRIP_RATIO);
37
+ }
38
+ // Native channels (memory-pressure, oom-warning, storage-low) hook
39
+ // through a TODO native module in v1.0.
40
+ }
41
+ export function stopPreCrashSentinel() {
42
+ _running = false;
43
+ _slowFrames = 0;
44
+ _totalFrames = 0;
45
+ _channels.clear();
46
+ }
47
+ function startFrameBudgetWatch(budgetMs, tripRatio) {
48
+ if (typeof requestAnimationFrame !== 'function')
49
+ return;
50
+ _lastFrameAt = Date.now();
51
+ function tick() {
52
+ if (!_running)
53
+ return;
54
+ const now = Date.now();
55
+ const delta = now - _lastFrameAt;
56
+ _lastFrameAt = now;
57
+ if (delta >= budgetMs)
58
+ _slowFrames++;
59
+ _totalFrames++;
60
+ if (_totalFrames >= WINDOW_FRAMES) {
61
+ const ratio = _slowFrames / _totalFrames;
62
+ if (ratio >= tripRatio && now - _lastEmitAt > COOLDOWN_MS) {
63
+ _lastEmitAt = now;
64
+ emitNearCrash({
65
+ slowFrames: _slowFrames,
66
+ totalFrames: _totalFrames,
67
+ ratio,
68
+ windowMs: WINDOW_FRAMES * (budgetMs / 2), // approximate
69
+ channel: 'frame-budget-overrun',
70
+ });
71
+ }
72
+ _slowFrames = 0;
73
+ _totalFrames = 0;
74
+ }
75
+ requestAnimationFrame(tick);
76
+ }
77
+ requestAnimationFrame(tick);
78
+ }
79
+ function emitNearCrash(data) {
80
+ if (!isInitialized())
81
+ return;
82
+ const config = getConfig();
83
+ if (!config)
84
+ return;
85
+ const span = startSpan('sentori.nearCrash', {
86
+ name: data.channel,
87
+ tags: {
88
+ 'nearCrash.channel': data.channel,
89
+ 'nearCrash.ratio': data.ratio.toFixed(3),
90
+ 'nearCrash.slow_frames': String(data.slowFrames),
91
+ 'nearCrash.total_frames': String(data.totalFrames),
92
+ },
93
+ });
94
+ span.finish({ status: 'ok' });
95
+ const event = {
96
+ id: uuidV7(),
97
+ timestamp: new Date().toISOString(),
98
+ kind: 'nearCrash',
99
+ platform: 'javascript',
100
+ release: config.release,
101
+ environment: config.environment,
102
+ device: collectDeviceForSentinel(),
103
+ app: getAppForSentinel(config.release),
104
+ ...(getBundleInfo() ? { bundle: getBundleInfo() } : {}),
105
+ error: {
106
+ type: 'NearCrash',
107
+ message: `frame budget overrun: ${(data.ratio * 100).toFixed(0)}% of last ${data.totalFrames} frames slow`,
108
+ stack: [],
109
+ },
110
+ tags: {
111
+ 'nearCrash.channel': data.channel,
112
+ },
113
+ };
114
+ enqueue(event);
115
+ }
116
+ //# sourceMappingURL=pre-crash-sentinel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pre-crash-sentinel.js","sourceRoot":"","sources":["../src/pre-crash-sentinel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { App, Device } from './types';
2
+ export declare const collectDeviceForSentinel: () => Device;
3
+ export declare const getAppForSentinel: (release: string) => App;
4
+ //# sourceMappingURL=sentinel-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinel-context.d.ts","sourceRoot":"","sources":["../src/sentinel-context.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAE3C,eAAO,MAAM,wBAAwB,QAAO,MAgC3C,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,GAkBnD,CAAC"}
@@ -0,0 +1,52 @@
1
+ // Shared device + app collectors. Used by both capture.ts (errors)
2
+ // and pre-crash-sentinel.ts (nearCrash). Same shape so the dashboard
3
+ // can render both kinds of events through the same UI components.
4
+ import { getCachedNetworkType } from './netinfo';
5
+ export const collectDeviceForSentinel = () => {
6
+ let os = 'other';
7
+ let osVersion = '0';
8
+ let locale;
9
+ const networkType = getCachedNetworkType();
10
+ try {
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const RN = require('react-native');
13
+ const rnOS = RN.Platform.OS;
14
+ os = rnOS === 'android' || rnOS === 'ios' || rnOS === 'web' ? rnOS : 'other';
15
+ osVersion = String(RN.Platform.Version);
16
+ if (rnOS === 'ios') {
17
+ const s = RN.NativeModules.SettingsManager?.settings;
18
+ locale = s?.AppleLocale ?? s?.AppleLanguages?.[0];
19
+ }
20
+ else if (rnOS === 'android') {
21
+ locale = RN.NativeModules.I18nManager?.localeIdentifier;
22
+ }
23
+ }
24
+ catch {
25
+ // not in RN runtime
26
+ }
27
+ const device = { os, osVersion };
28
+ if (locale)
29
+ device.locale = locale;
30
+ if (networkType)
31
+ device.networkType = networkType;
32
+ return device;
33
+ };
34
+ export const getAppForSentinel = (release) => {
35
+ const m = /^(?:[^@]+@)?([^+]+)(?:\+(.+))?$/.exec(release);
36
+ const version = m?.[1] ?? '0.0.0';
37
+ const build = m?.[2];
38
+ let rnVersion = 'unknown';
39
+ try {
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ rnVersion = require('react-native/package.json').version;
42
+ }
43
+ catch {
44
+ // not in RN runtime
45
+ }
46
+ return {
47
+ build,
48
+ framework: { name: 'react-native', version: rnVersion },
49
+ version,
50
+ };
51
+ };
52
+ //# sourceMappingURL=sentinel-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentinel-context.js","sourceRoot":"","sources":["../src/sentinel-context.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,qEAAqE;AACrE,kEAAkE;AAElE,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAGjD,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAW,EAAE;IACnD,IAAI,EAAE,GAAiB,OAAO,CAAC;IAC/B,IAAI,SAAS,GAAG,GAAG,CAAC;IACpB,IAAI,MAA0B,CAAC;IAC/B,MAAM,WAAW,GAAG,oBAAoB,EAAE,CAAC;IAC3C,IAAI,CAAC;QACH,iEAAiE;QACjE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAQhC,CAAC;QACF,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,EAAE,GAAG,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;QAC7E,SAAS,GAAG,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;YACnB,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,eAAe,EAAE,QAAQ,CAAC;YACrD,MAAM,GAAG,CAAC,EAAE,WAAW,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC;aAAM,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,gBAAgB,CAAC;QAC1D,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oBAAoB;IACtB,CAAC;IACD,MAAM,MAAM,GAAW,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC;IACzC,IAAI,MAAM;QAAE,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IACnC,IAAI,WAAW;QAAE,MAAM,CAAC,WAAW,GAAG,WAAW,CAAC;IAClD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,OAAe,EAAO,EAAE;IACxD,MAAM,CAAC,GAAG,iCAAiC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC;IAClC,MAAM,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAErB,IAAI,SAAS,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC;QACH,iEAAiE;QACjE,SAAS,GAAI,OAAO,CAAC,2BAA2B,CAAyB,CAAC,OAAO,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,oBAAoB;IACtB,CAAC;IAED,OAAO;QACL,KAAK;QACL,SAAS,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE;QACvD,OAAO;KACR,CAAC;AACJ,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react-native",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",
@@ -53,6 +53,9 @@
53
53
  "expo-modules-core": {
54
54
  "optional": true
55
55
  },
56
+ "expo-sensors": {
57
+ "optional": true
58
+ },
56
59
  "expo-updates": {
57
60
  "optional": true
58
61
  },
@@ -64,6 +67,7 @@
64
67
  "@react-native-async-storage/async-storage": ">=1.23",
65
68
  "@react-native-community/netinfo": ">=11.0",
66
69
  "expo-modules-core": ">=2.0",
70
+ "expo-sensors": ">=14.0",
67
71
  "expo-updates": ">=0.27",
68
72
  "react-native-code-push": ">=8.0"
69
73
  },
@@ -76,6 +80,6 @@
76
80
  "access": "public"
77
81
  },
78
82
  "dependencies": {
79
- "@goliapkg/sentori-core": "0.8.0"
83
+ "@goliapkg/sentori-core": "0.8.1"
80
84
  }
81
85
  }
@@ -0,0 +1,309 @@
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
+
32
+ type Trigger = 'fab' | 'manual' | 'shake';
33
+
34
+ export type FeedbackButtonHandle = {
35
+ /** Open the feedback modal. Returns immediately. */
36
+ open: (defaults?: { body?: string; eventId?: string; title?: string }) => void;
37
+ /** Close the modal if open. */
38
+ close: () => void;
39
+ };
40
+
41
+ export type FeedbackButtonProps = {
42
+ /** When to surface the prompt. Default `'fab'`. */
43
+ trigger?: Trigger;
44
+ /** Pass the eventId from the last `captureException` to tie the
45
+ * report to that crash. Optional. */
46
+ eventId?: string;
47
+ /** Localized strings. Defaults are English. */
48
+ labels?: {
49
+ title?: string;
50
+ bodyPlaceholder?: string;
51
+ emailPlaceholder?: string;
52
+ submit?: string;
53
+ cancel?: string;
54
+ sent?: string;
55
+ };
56
+ /** Shake sensitivity in m/s² above gravity. Default 18 (≈ a normal
57
+ * intentional shake; lower triggers more easily). Only used when
58
+ * `trigger="shake"`. */
59
+ shakeThreshold?: number;
60
+ };
61
+
62
+ export const FeedbackButton = forwardRef<FeedbackButtonHandle, FeedbackButtonProps>(
63
+ function FeedbackButton(props, ref) {
64
+ const trigger: Trigger = props.trigger ?? 'fab';
65
+ const [open, setOpen] = useState(false);
66
+ const [title, setTitle] = useState('');
67
+ const [body, setBody] = useState('');
68
+ const [email, setEmail] = useState('');
69
+ const [submitting, setSubmitting] = useState(false);
70
+ const [sent, setSent] = useState(false);
71
+ const eventIdRef = useRef<string | undefined>(props.eventId);
72
+
73
+ useImperativeHandle(
74
+ ref,
75
+ () => ({
76
+ open: (defaults) => {
77
+ setTitle(defaults?.title ?? '');
78
+ setBody(defaults?.body ?? '');
79
+ eventIdRef.current = defaults?.eventId ?? props.eventId;
80
+ setSent(false);
81
+ setOpen(true);
82
+ },
83
+ close: () => setOpen(false),
84
+ }),
85
+ [props.eventId],
86
+ );
87
+
88
+ // Shake detection — opt-in. We load expo-sensors lazily so apps
89
+ // that don't install it never pay the bundle cost.
90
+ useEffect(() => {
91
+ if (trigger !== 'shake') return;
92
+ let sub: { remove: () => void } | null = null;
93
+ try {
94
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
95
+ const mod = require('expo-sensors') as {
96
+ Accelerometer?: {
97
+ addListener: (cb: (d: { x: number; y: number; z: number }) => void) => {
98
+ remove: () => void;
99
+ };
100
+ setUpdateInterval: (ms: number) => void;
101
+ };
102
+ };
103
+ const A = mod.Accelerometer;
104
+ if (!A) return;
105
+ A.setUpdateInterval(100);
106
+ const thr = props.shakeThreshold ?? 18;
107
+ let lastTriggerAt = 0;
108
+ sub = A.addListener(({ x, y, z }) => {
109
+ const mag = Math.sqrt(x * x + y * y + z * z) * 9.81; // g → m/s²
110
+ const now = Date.now();
111
+ if (mag > thr && now - lastTriggerAt > 1500) {
112
+ lastTriggerAt = now;
113
+ setSent(false);
114
+ setOpen(true);
115
+ }
116
+ });
117
+ } catch {
118
+ // expo-sensors not installed → silently fall back to manual
119
+ }
120
+ return () => {
121
+ if (sub) sub.remove();
122
+ };
123
+ }, [trigger, props.shakeThreshold]);
124
+
125
+ const onSubmit = useCallback(async () => {
126
+ if (!title.trim() || !body.trim()) return;
127
+ setSubmitting(true);
128
+ try {
129
+ await sendUserFeedback({
130
+ title: title.trim().slice(0, 200),
131
+ body: body.trim().slice(0, 8000),
132
+ email: email.trim() || undefined,
133
+ eventId: eventIdRef.current,
134
+ });
135
+ setSent(true);
136
+ setTimeout(() => setOpen(false), 1200);
137
+ } finally {
138
+ setSubmitting(false);
139
+ }
140
+ }, [title, body, email]);
141
+
142
+ const L = {
143
+ bodyPlaceholder: 'What happened?',
144
+ cancel: 'Cancel',
145
+ emailPlaceholder: 'email (optional)',
146
+ sent: 'Thanks — report sent.',
147
+ submit: 'Send',
148
+ title: 'Report a problem',
149
+ ...props.labels,
150
+ };
151
+
152
+ return (
153
+ <>
154
+ {trigger === 'fab' && (
155
+ <Pressable
156
+ accessibilityLabel="Open feedback"
157
+ onPress={() => {
158
+ setSent(false);
159
+ setOpen(true);
160
+ }}
161
+ style={styles.fab}
162
+ >
163
+ <Text style={styles.fabText}>?</Text>
164
+ </Pressable>
165
+ )}
166
+ <Modal animationType="fade" transparent visible={open} onRequestClose={() => setOpen(false)}>
167
+ <View style={styles.backdrop}>
168
+ <View style={styles.card}>
169
+ {sent ? (
170
+ <Text style={styles.sentMessage}>{L.sent}</Text>
171
+ ) : (
172
+ <>
173
+ <Text style={styles.heading}>{L.title}</Text>
174
+ <TextInput
175
+ autoCapitalize="sentences"
176
+ onChangeText={setTitle}
177
+ placeholder="Subject"
178
+ placeholderTextColor="#888"
179
+ style={styles.titleInput}
180
+ value={title}
181
+ />
182
+ <TextInput
183
+ multiline
184
+ onChangeText={setBody}
185
+ placeholder={L.bodyPlaceholder}
186
+ placeholderTextColor="#888"
187
+ style={styles.bodyInput}
188
+ textAlignVertical="top"
189
+ value={body}
190
+ />
191
+ <TextInput
192
+ autoCapitalize="none"
193
+ keyboardType="email-address"
194
+ onChangeText={setEmail}
195
+ placeholder={L.emailPlaceholder}
196
+ placeholderTextColor="#888"
197
+ style={styles.emailInput}
198
+ value={email}
199
+ />
200
+ <View style={styles.actions}>
201
+ <Pressable onPress={() => setOpen(false)} style={styles.cancelBtn}>
202
+ <Text style={styles.cancelText}>{L.cancel}</Text>
203
+ </Pressable>
204
+ <Pressable
205
+ disabled={submitting || !title.trim() || !body.trim()}
206
+ onPress={onSubmit}
207
+ style={[styles.submitBtn, submitting && styles.submitBtnDisabled]}
208
+ >
209
+ <Text style={styles.submitText}>
210
+ {submitting ? '…' : L.submit}
211
+ </Text>
212
+ </Pressable>
213
+ </View>
214
+ </>
215
+ )}
216
+ </View>
217
+ </View>
218
+ </Modal>
219
+ </>
220
+ );
221
+ },
222
+ );
223
+
224
+ const styles = StyleSheet.create({
225
+ actions: {
226
+ flexDirection: 'row',
227
+ gap: 8,
228
+ justifyContent: 'flex-end',
229
+ },
230
+ backdrop: {
231
+ alignItems: 'center',
232
+ backgroundColor: 'rgba(0,0,0,0.5)',
233
+ flex: 1,
234
+ justifyContent: 'center',
235
+ paddingHorizontal: 24,
236
+ },
237
+ bodyInput: {
238
+ backgroundColor: '#f7f7f8',
239
+ borderColor: '#e5e5e8',
240
+ borderRadius: 6,
241
+ borderWidth: 1,
242
+ color: '#111',
243
+ fontSize: 14,
244
+ marginBottom: 8,
245
+ minHeight: 100,
246
+ paddingHorizontal: 12,
247
+ paddingVertical: 10,
248
+ },
249
+ cancelBtn: {
250
+ paddingHorizontal: 14,
251
+ paddingVertical: 8,
252
+ },
253
+ cancelText: { color: '#666', fontSize: 14 },
254
+ card: {
255
+ backgroundColor: '#fff',
256
+ borderRadius: 10,
257
+ padding: 16,
258
+ width: '100%',
259
+ },
260
+ emailInput: {
261
+ backgroundColor: '#f7f7f8',
262
+ borderColor: '#e5e5e8',
263
+ borderRadius: 6,
264
+ borderWidth: 1,
265
+ color: '#111',
266
+ fontSize: 14,
267
+ marginBottom: 14,
268
+ paddingHorizontal: 12,
269
+ paddingVertical: 8,
270
+ },
271
+ fab: {
272
+ alignItems: 'center',
273
+ backgroundColor: '#111',
274
+ borderRadius: 24,
275
+ bottom: 28,
276
+ elevation: 4,
277
+ height: 48,
278
+ justifyContent: 'center',
279
+ position: 'absolute',
280
+ right: 18,
281
+ shadowColor: '#000',
282
+ shadowOffset: { height: 2, width: 0 },
283
+ shadowOpacity: 0.18,
284
+ shadowRadius: 4,
285
+ width: 48,
286
+ },
287
+ fabText: { color: '#fff', fontSize: 22, fontWeight: '500' },
288
+ heading: { color: '#111', fontSize: 18, fontWeight: '500', marginBottom: 12 },
289
+ sentMessage: { color: '#111', fontSize: 14, paddingVertical: 18, textAlign: 'center' },
290
+ submitBtn: {
291
+ backgroundColor: '#111',
292
+ borderRadius: 6,
293
+ paddingHorizontal: 16,
294
+ paddingVertical: 8,
295
+ },
296
+ submitBtnDisabled: { opacity: 0.5 },
297
+ submitText: { color: '#fff', fontSize: 14, fontWeight: '500' },
298
+ titleInput: {
299
+ backgroundColor: '#f7f7f8',
300
+ borderColor: '#e5e5e8',
301
+ borderRadius: 6,
302
+ borderWidth: 1,
303
+ color: '#111',
304
+ fontSize: 14,
305
+ marginBottom: 8,
306
+ paddingHorizontal: 12,
307
+ paddingVertical: 8,
308
+ },
309
+ });
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  setUser,
10
10
  } from './capture';
11
11
  import { ErrorBoundary } from './error-boundary';
12
+ import { FeedbackButton, type FeedbackButtonHandle, type FeedbackButtonProps } from './feedback-widget';
12
13
  import {
13
14
  clearAllFeatureFlags,
14
15
  clearFeatureFlag,
@@ -44,6 +45,7 @@ export const sentori = {
44
45
  clearAllFeatureFlags,
45
46
  getFeatureFlags,
46
47
  ErrorBoundary,
48
+ FeedbackButton,
47
49
  RageTapCapture,
48
50
  registerMaskQuery,
49
51
  clearMaskQuery,
@@ -65,6 +67,7 @@ export {
65
67
  setUser,
66
68
  } from './capture';
67
69
  export { ErrorBoundary } from './error-boundary';
70
+ export { FeedbackButton, type FeedbackButtonHandle, type FeedbackButtonProps } from './feedback-widget';
68
71
  export {
69
72
  clearAllFeatureFlags,
70
73
  clearFeatureFlag,
package/src/init.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import { startMetricsTimer } from './metrics';
12
12
  import { drainNativePending, setNativeConfig } from './native';
13
13
  import { startNetworkTypeWatch } from './netinfo';
14
+ import { startPreCrashSentinel, type PreCrashChannel } from './pre-crash-sentinel';
14
15
  import { startSession } from './session-tracker';
15
16
  import {
16
17
  drainOfflineQueue,
@@ -61,6 +62,13 @@ export type InitOptions = {
61
62
  * the buffer is sealed and uploaded as a `sessionTrail`
62
63
  * attachment. Defaults to false. */
63
64
  sessionTrail?: boolean;
65
+ /** v0.9.1 +S4 — pre-crash sentinel. Subscribes to JS-thread
66
+ * frame timing; when ≥ 50% of a 60-frame window misses the
67
+ * budget (default 32 ms / < 30 fps), emits a `kind: nearCrash`
68
+ * event proactively so dashboards see the "about-to-die"
69
+ * signal before an actual crash. */
70
+ preCrashSentinel?: boolean;
71
+ sentinelChannels?: PreCrashChannel[];
64
72
  /** v0.9.0 #3 — launch-crash loop guard. When two consecutive
65
73
  * launches don't reach `markLaunchCompleted()` (typical of an
66
74
  * OTA update with a fatal bug), invoke the host callback with
@@ -142,6 +150,14 @@ export const init = (options: InitOptions): void => {
142
150
  startNetworkTypeWatch();
143
151
  // v0.8.3 — drain custom-metric ring every 30 s.
144
152
  startMetricsTimer();
153
+ // v0.9.1 +S4 — pre-crash sentinel. Off by default; opt-in via
154
+ // `capture.preCrashSentinel: true`.
155
+ if (options.capture?.preCrashSentinel === true) {
156
+ startPreCrashSentinel({
157
+ enabled: true,
158
+ channels: options.capture.sentinelChannels,
159
+ });
160
+ }
145
161
 
146
162
  const capture = options.capture ?? {};
147
163
  if (capture.globalErrors !== false) installGlobalHandler();
@@ -0,0 +1,140 @@
1
+ // v0.9.1 +S4 — pre-crash sentinel.
2
+ //
3
+ // Predictive (vs reactive) telemetry. Subscribes to JS-thread frame
4
+ // timing via requestAnimationFrame and, when a rolling 60-frame
5
+ // window has ≥ 50% of frames slower than 32 ms (i.e. < 30 fps for
6
+ // half the window), emits an `event.kind = nearCrash` to the server.
7
+ // Backend stores it in the same events stream so the dashboard shows
8
+ // "X user sessions had near-crash signals 4 minutes before the
9
+ // actual NSException".
10
+ //
11
+ // Memory pressure / OOM / storage low signals will need native
12
+ // system observers and ship in v1.0. v0.9.1 covers the most common
13
+ // runaway-render-loop case purely from JS.
14
+
15
+ import { startSpan } from '@goliapkg/sentori-core';
16
+
17
+ import { getBundleInfo } from './bundle-info';
18
+ import { collectDeviceForSentinel, getAppForSentinel } from './sentinel-context';
19
+ import { getConfig, isInitialized } from './config';
20
+ import { enqueue } from './transport';
21
+ import { uuidV7 } from './uuid';
22
+ import type { Event } from './types';
23
+
24
+ const FRAME_BUDGET_MS = 32; // < 30 fps
25
+ const WINDOW_FRAMES = 60; // ~1 s at 60 fps
26
+ const TRIP_RATIO = 0.5;
27
+ const COOLDOWN_MS = 60_000; // don't spam: one nearCrash event per minute
28
+
29
+ let _running = false;
30
+ let _lastFrameAt = 0;
31
+ let _slowFrames = 0;
32
+ let _totalFrames = 0;
33
+ let _lastEmitAt = 0;
34
+ let _channels: Set<string> = new Set();
35
+
36
+ export type PreCrashChannel =
37
+ | 'frame-budget-overrun'
38
+ | 'memory-pressure' // native, v1.0
39
+ | 'oom-warning' // native, v1.0
40
+ | 'storage-low'; // native, v1.0
41
+
42
+ export type PreCrashSentinelOptions = {
43
+ enabled: boolean;
44
+ channels?: PreCrashChannel[];
45
+ /** Lower → more sensitive. Default 32 ms (< 30 fps). */
46
+ frameBudgetMs?: number;
47
+ /** Fraction of frames in the window that must miss budget. Default 0.5. */
48
+ tripRatio?: number;
49
+ };
50
+
51
+ export function startPreCrashSentinel(opts: PreCrashSentinelOptions): void {
52
+ if (!opts.enabled || _running) return;
53
+ _running = true;
54
+ _channels = new Set(opts.channels ?? ['frame-budget-overrun']);
55
+
56
+ if (_channels.has('frame-budget-overrun')) {
57
+ startFrameBudgetWatch(opts.frameBudgetMs ?? FRAME_BUDGET_MS, opts.tripRatio ?? TRIP_RATIO);
58
+ }
59
+ // Native channels (memory-pressure, oom-warning, storage-low) hook
60
+ // through a TODO native module in v1.0.
61
+ }
62
+
63
+ export function stopPreCrashSentinel(): void {
64
+ _running = false;
65
+ _slowFrames = 0;
66
+ _totalFrames = 0;
67
+ _channels.clear();
68
+ }
69
+
70
+ function startFrameBudgetWatch(budgetMs: number, tripRatio: number): void {
71
+ if (typeof requestAnimationFrame !== 'function') return;
72
+ _lastFrameAt = Date.now();
73
+ function tick() {
74
+ if (!_running) return;
75
+ const now = Date.now();
76
+ const delta = now - _lastFrameAt;
77
+ _lastFrameAt = now;
78
+ if (delta >= budgetMs) _slowFrames++;
79
+ _totalFrames++;
80
+ if (_totalFrames >= WINDOW_FRAMES) {
81
+ const ratio = _slowFrames / _totalFrames;
82
+ if (ratio >= tripRatio && now - _lastEmitAt > COOLDOWN_MS) {
83
+ _lastEmitAt = now;
84
+ emitNearCrash({
85
+ slowFrames: _slowFrames,
86
+ totalFrames: _totalFrames,
87
+ ratio,
88
+ windowMs: WINDOW_FRAMES * (budgetMs / 2), // approximate
89
+ channel: 'frame-budget-overrun',
90
+ });
91
+ }
92
+ _slowFrames = 0;
93
+ _totalFrames = 0;
94
+ }
95
+ requestAnimationFrame(tick);
96
+ }
97
+ requestAnimationFrame(tick);
98
+ }
99
+
100
+ function emitNearCrash(data: {
101
+ slowFrames: number;
102
+ totalFrames: number;
103
+ ratio: number;
104
+ windowMs: number;
105
+ channel: PreCrashChannel;
106
+ }): void {
107
+ if (!isInitialized()) return;
108
+ const config = getConfig();
109
+ if (!config) return;
110
+ const span = startSpan('sentori.nearCrash', {
111
+ name: data.channel,
112
+ tags: {
113
+ 'nearCrash.channel': data.channel,
114
+ 'nearCrash.ratio': data.ratio.toFixed(3),
115
+ 'nearCrash.slow_frames': String(data.slowFrames),
116
+ 'nearCrash.total_frames': String(data.totalFrames),
117
+ },
118
+ });
119
+ span.finish({ status: 'ok' });
120
+ const event: Event = {
121
+ id: uuidV7(),
122
+ timestamp: new Date().toISOString(),
123
+ kind: 'nearCrash',
124
+ platform: 'javascript',
125
+ release: config.release,
126
+ environment: config.environment,
127
+ device: collectDeviceForSentinel(),
128
+ app: getAppForSentinel(config.release),
129
+ ...(getBundleInfo() ? { bundle: getBundleInfo() as { id: string } } : {}),
130
+ error: {
131
+ type: 'NearCrash',
132
+ message: `frame budget overrun: ${(data.ratio * 100).toFixed(0)}% of last ${data.totalFrames} frames slow`,
133
+ stack: [],
134
+ },
135
+ tags: {
136
+ 'nearCrash.channel': data.channel,
137
+ },
138
+ };
139
+ enqueue(event);
140
+ }
@@ -0,0 +1,60 @@
1
+ // Shared device + app collectors. Used by both capture.ts (errors)
2
+ // and pre-crash-sentinel.ts (nearCrash). Same shape so the dashboard
3
+ // can render both kinds of events through the same UI components.
4
+
5
+ import { getCachedNetworkType } from './netinfo';
6
+ import type { App, Device } from './types';
7
+
8
+ export const collectDeviceForSentinel = (): Device => {
9
+ let os: Device['os'] = 'other';
10
+ let osVersion = '0';
11
+ let locale: string | undefined;
12
+ const networkType = getCachedNetworkType();
13
+ try {
14
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
15
+ const RN = require('react-native') as {
16
+ NativeModules: {
17
+ I18nManager?: { localeIdentifier?: string };
18
+ SettingsManager?: {
19
+ settings?: { AppleLanguages?: string[]; AppleLocale?: string };
20
+ };
21
+ };
22
+ Platform: { OS: string; Version: number | string };
23
+ };
24
+ const rnOS = RN.Platform.OS;
25
+ os = rnOS === 'android' || rnOS === 'ios' || rnOS === 'web' ? rnOS : 'other';
26
+ osVersion = String(RN.Platform.Version);
27
+ if (rnOS === 'ios') {
28
+ const s = RN.NativeModules.SettingsManager?.settings;
29
+ locale = s?.AppleLocale ?? s?.AppleLanguages?.[0];
30
+ } else if (rnOS === 'android') {
31
+ locale = RN.NativeModules.I18nManager?.localeIdentifier;
32
+ }
33
+ } catch {
34
+ // not in RN runtime
35
+ }
36
+ const device: Device = { os, osVersion };
37
+ if (locale) device.locale = locale;
38
+ if (networkType) device.networkType = networkType;
39
+ return device;
40
+ };
41
+
42
+ export const getAppForSentinel = (release: string): App => {
43
+ const m = /^(?:[^@]+@)?([^+]+)(?:\+(.+))?$/.exec(release);
44
+ const version = m?.[1] ?? '0.0.0';
45
+ const build = m?.[2];
46
+
47
+ let rnVersion = 'unknown';
48
+ try {
49
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
50
+ rnVersion = (require('react-native/package.json') as { version: string }).version;
51
+ } catch {
52
+ // not in RN runtime
53
+ }
54
+
55
+ return {
56
+ build,
57
+ framework: { name: 'react-native', version: rnVersion },
58
+ version,
59
+ };
60
+ };