@bugsession/react-native 0.1.0-beta.2 → 0.1.0-beta.3
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/README.md +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +56 -5
- package/package.json +1 -1
- package/src/index.tsx +69 -16
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Opt-in screen recording, microphone narration and bounded runtime telemetry for
|
|
|
4
4
|
|
|
5
5
|
## Compatibility
|
|
6
6
|
|
|
7
|
-
The SDK supports React 18.3.1–19.x and React Native 0.76–0.88. Expo SDK 54–58 are supported; SDK 52–53 are best effort. CI builds Android against SDK 52
|
|
7
|
+
The SDK supports React 18.3.1–19.x and React Native 0.76–0.88. Expo SDK 54–58 are supported; SDK 52–53 are best effort. CI builds Android against SDK 52–58 and iOS simulators against SDK 52 and 54–58. Each Expo SDK selects its matching React and React Native versions; do not mix versions manually. iOS and Android require a native build and `@bugsession/capture-native`; Expo Go cannot capture screen video. Android uses MediaProjection and asks for screen-capture consent every session. The hosted recorder admits one session at a time.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -56,7 +56,7 @@ Configure once while idle. Wire an automatic stop timer into your recording UI,
|
|
|
56
56
|
|
|
57
57
|
React Navigation integration: call the function returned by `BugSession.navigationListener(navigationRef)` from both `NavigationContainer.onReady` and `onStateChange`. Expo Router apps can call `trackNavigation` when the committed pathname changes. Use `BugSession.Touchable` to instrument a Pressable, or `trackAction` directly. Send caught errors through `BugSession.captureError(error)`.
|
|
58
58
|
|
|
59
|
-
`<BugSession.Trigger />` renders a floating logo button that starts and stops sessions.
|
|
59
|
+
`<BugSession.Trigger />` renders a floating logo button that starts and stops sessions. It is draggable by default, stays within screen bounds, and remembers its moved position across screen changes and trigger remounts while the app remains open. Render it once above the app navigator so it stays visible during navigation. Configure its initial corner with `position` (`bottom-right` by default; also `bottom-left`, `top-right`, or `top-left`) and adjust `offset` in points. Set `draggable={false}` to lock it. Pass `onError` to surface permission, recorder, and upload failures. Supply `children` to replace the default mark.
|
|
60
60
|
|
|
61
61
|
Android uses `AndroidCapture` from `@bugsession/capture-native` plus an `audio` adapter implementing `prepare`, `start`, `stop` (returns a local URI), and `upload`. `prepare` must request microphone permission. `expo-audio` can provide this adapter. Android shows a system screen-capture prompt each session and a persistent capture notification; keep the app foregrounded through upload.
|
|
62
62
|
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type BugSessionOptions = {
|
|
|
20
20
|
export type BugSessionTriggerProps = {
|
|
21
21
|
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
22
22
|
offset?: number;
|
|
23
|
+
draggable?: boolean;
|
|
23
24
|
style?: StyleProp<ViewStyle>;
|
|
24
25
|
accessibilityLabel?: string;
|
|
25
26
|
children?: React.ReactNode;
|
|
@@ -47,5 +48,5 @@ export declare const BugSession: {
|
|
|
47
48
|
name: string;
|
|
48
49
|
screen?: string;
|
|
49
50
|
}): React.JSX.Element;
|
|
50
|
-
Trigger({ position, offset, style, accessibilityLabel, children, onError }: BugSessionTriggerProps): React.JSX.Element;
|
|
51
|
+
Trigger({ position, offset, draggable, style, accessibilityLabel, children, onError }: BugSessionTriggerProps): React.JSX.Element;
|
|
51
52
|
};
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,14 @@ let state = 'idle';
|
|
|
22
22
|
let restore = null;
|
|
23
23
|
let currentScreen = 'Home';
|
|
24
24
|
let rawFetch;
|
|
25
|
+
let triggerPosition = null;
|
|
26
|
+
const triggerSize = 58;
|
|
27
|
+
function clampTriggerPosition(point, width, height) {
|
|
28
|
+
return {
|
|
29
|
+
left: Math.max(0, Math.min(point.left, Math.max(0, width - triggerSize))),
|
|
30
|
+
top: Math.max(0, Math.min(point.top, Math.max(0, height - triggerSize))),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
25
33
|
function authHeaders() {
|
|
26
34
|
return options.recorderToken ? { Authorization: `Bearer ${options.recorderToken}` } : {};
|
|
27
35
|
}
|
|
@@ -219,11 +227,53 @@ exports.BugSession = {
|
|
|
219
227
|
Touchable({ name, screen, onPress, ...props }) {
|
|
220
228
|
return (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { ...props, onPress: e => { exports.BugSession.trackAction({ name, screen }); onPress?.(e); } });
|
|
221
229
|
},
|
|
222
|
-
Trigger({ position = 'bottom-right', offset = 24, style, accessibilityLabel, children, onError }) {
|
|
230
|
+
Trigger({ position = 'bottom-right', offset = 24, draggable = true, style, accessibilityLabel, children, onError }) {
|
|
231
|
+
const { width, height } = (0, react_native_1.useWindowDimensions)();
|
|
232
|
+
const [point, setPoint] = react_1.default.useState(() => triggerPosition
|
|
233
|
+
? clampTriggerPosition(triggerPosition, width, height)
|
|
234
|
+
: clampTriggerPosition({
|
|
235
|
+
left: position.includes('right') ? width - triggerSize - offset : offset,
|
|
236
|
+
top: position.includes('bottom') ? height - triggerSize - offset : offset,
|
|
237
|
+
}, width, height));
|
|
238
|
+
const pointRef = react_1.default.useRef(point);
|
|
239
|
+
pointRef.current = point;
|
|
240
|
+
const drag = react_1.default.useRef(null);
|
|
241
|
+
if (!drag.current)
|
|
242
|
+
drag.current = new react_native_1.Animated.ValueXY();
|
|
243
|
+
const dragStart = react_1.default.useRef(point);
|
|
244
|
+
react_1.default.useEffect(() => {
|
|
245
|
+
const next = clampTriggerPosition(pointRef.current, width, height);
|
|
246
|
+
pointRef.current = next;
|
|
247
|
+
setPoint(next);
|
|
248
|
+
if (triggerPosition)
|
|
249
|
+
triggerPosition = next;
|
|
250
|
+
drag.current?.setValue({ x: 0, y: 0 });
|
|
251
|
+
}, [width, height]);
|
|
252
|
+
const responder = react_1.default.useMemo(() => react_native_1.PanResponder.create({
|
|
253
|
+
onStartShouldSetPanResponder: () => false,
|
|
254
|
+
onMoveShouldSetPanResponder: (_, gesture) => draggable && Math.hypot(gesture.dx, gesture.dy) > 7,
|
|
255
|
+
onPanResponderGrant: () => { dragStart.current = pointRef.current; },
|
|
256
|
+
onPanResponderMove: (_, gesture) => {
|
|
257
|
+
const next = clampTriggerPosition({ left: dragStart.current.left + gesture.dx, top: dragStart.current.top + gesture.dy }, width, height);
|
|
258
|
+
drag.current?.setValue({ x: next.left - dragStart.current.left, y: next.top - dragStart.current.top });
|
|
259
|
+
},
|
|
260
|
+
onPanResponderRelease: (_, gesture) => {
|
|
261
|
+
const next = clampTriggerPosition({ left: dragStart.current.left + gesture.dx, top: dragStart.current.top + gesture.dy }, width, height);
|
|
262
|
+
triggerPosition = next;
|
|
263
|
+
pointRef.current = next;
|
|
264
|
+
setPoint(next);
|
|
265
|
+
drag.current?.setValue({ x: 0, y: 0 });
|
|
266
|
+
},
|
|
267
|
+
onPanResponderTerminate: (_, gesture) => {
|
|
268
|
+
const next = clampTriggerPosition({ left: dragStart.current.left + gesture.dx, top: dragStart.current.top + gesture.dy }, width, height);
|
|
269
|
+
triggerPosition = next;
|
|
270
|
+
pointRef.current = next;
|
|
271
|
+
setPoint(next);
|
|
272
|
+
drag.current?.setValue({ x: 0, y: 0 });
|
|
273
|
+
},
|
|
274
|
+
}), [draggable, width, height]);
|
|
223
275
|
const [busy, setBusy] = react_1.default.useState(false);
|
|
224
276
|
const recording = state === 'recording';
|
|
225
|
-
const edge = position.includes('right') ? { right: offset } : { left: offset };
|
|
226
|
-
const vertical = position.includes('bottom') ? { bottom: offset } : { top: offset };
|
|
227
277
|
const toggle = async () => {
|
|
228
278
|
if (busy)
|
|
229
279
|
return;
|
|
@@ -241,11 +291,12 @@ exports.BugSession = {
|
|
|
241
291
|
setBusy(false);
|
|
242
292
|
}
|
|
243
293
|
};
|
|
244
|
-
return (0, jsx_runtime_1.jsx)(react_native_1.View, { pointerEvents: "box-none", style:
|
|
294
|
+
return (0, jsx_runtime_1.jsx)(react_native_1.View, { pointerEvents: "box-none", style: styles.triggerLayer, children: (0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { ...(draggable ? responder.panHandlers : {}), style: [styles.triggerPosition, { left: point.left, top: point.top, transform: drag.current?.getTranslateTransform() }], children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: accessibilityLabel ?? (recording ? 'Stop BugSession recording' : 'Start BugSession recording'), accessibilityHint: draggable ? 'Drag to move the BugSession button' : undefined, accessibilityState: { busy, selected: recording }, disabled: busy, onPress: () => { void toggle(); }, style: [styles.trigger, recording && styles.triggerRecording, style], children: children ?? (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.triggerMark, children: "b" }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.triggerDot, recording && styles.triggerDotRecording] })] }) }) }) });
|
|
245
295
|
},
|
|
246
296
|
};
|
|
247
297
|
const styles = react_native_1.StyleSheet.create({
|
|
248
|
-
triggerLayer: {
|
|
298
|
+
triggerLayer: { ...react_native_1.StyleSheet.absoluteFill, zIndex: 9999 },
|
|
299
|
+
triggerPosition: { position: 'absolute' },
|
|
249
300
|
trigger: { width: 58, height: 58, borderRadius: 29, backgroundColor: '#173B34', alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: '#000', shadowOpacity: 0.24, shadowRadius: 8, shadowOffset: { width: 0, height: 4 } },
|
|
250
301
|
triggerRecording: { backgroundColor: '#B9382F' },
|
|
251
302
|
triggerMark: { color: '#F4F4EC', fontSize: 31, fontWeight: '800', lineHeight: 38 },
|
package/package.json
CHANGED
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Pressable, StyleSheet, Text, View, type PressableProps, type StyleProp, type ViewStyle, Platform } from 'react-native';
|
|
2
|
+
import { Animated, PanResponder, Pressable, StyleSheet, Text, View, useWindowDimensions, type PressableProps, type StyleProp, type ViewStyle, Platform } from 'react-native';
|
|
3
3
|
import { Collector, instrumentFetch, safeText, safeUrl, SessionClock } from './core';
|
|
4
4
|
import { alignNativeRecording, type NativeCaptureAdapter } from './capture';
|
|
5
5
|
export type { NativeCaptureAdapter } from './capture';
|
|
@@ -15,6 +15,7 @@ export type BugSessionOptions = { recorderUrl: string; recorderToken?: string; a
|
|
|
15
15
|
export type BugSessionTriggerProps = {
|
|
16
16
|
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
17
17
|
offset?: number;
|
|
18
|
+
draggable?: boolean;
|
|
18
19
|
style?: StyleProp<ViewStyle>;
|
|
19
20
|
accessibilityLabel?: string;
|
|
20
21
|
children?: React.ReactNode;
|
|
@@ -29,6 +30,14 @@ let state: 'idle' | 'starting' | 'recording' | 'stopping' = 'idle';
|
|
|
29
30
|
let restore: (() => void) | null = null;
|
|
30
31
|
let currentScreen = 'Home';
|
|
31
32
|
let rawFetch: typeof fetch;
|
|
33
|
+
let triggerPosition: { left: number; top: number } | null = null;
|
|
34
|
+
const triggerSize = 58;
|
|
35
|
+
function clampTriggerPosition(point: { left: number; top: number }, width: number, height: number) {
|
|
36
|
+
return {
|
|
37
|
+
left: Math.max(0, Math.min(point.left, Math.max(0, width - triggerSize))),
|
|
38
|
+
top: Math.max(0, Math.min(point.top, Math.max(0, height - triggerSize))),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
32
41
|
function authHeaders(): Record<string, string> {
|
|
33
42
|
return options.recorderToken ? { Authorization: `Bearer ${options.recorderToken}` } : {};
|
|
34
43
|
}
|
|
@@ -186,11 +195,51 @@ export const BugSession = {
|
|
|
186
195
|
Touchable({ name, screen, onPress, ...props }: PressableProps & { name: string; screen?: string }) {
|
|
187
196
|
return <Pressable {...props} onPress={e => { BugSession.trackAction({ name, screen }); onPress?.(e); }} />;
|
|
188
197
|
},
|
|
189
|
-
Trigger({ position = 'bottom-right', offset = 24, style, accessibilityLabel, children, onError }: BugSessionTriggerProps) {
|
|
198
|
+
Trigger({ position = 'bottom-right', offset = 24, draggable = true, style, accessibilityLabel, children, onError }: BugSessionTriggerProps) {
|
|
199
|
+
const { width, height } = useWindowDimensions();
|
|
200
|
+
const [point, setPoint] = React.useState(() => triggerPosition
|
|
201
|
+
? clampTriggerPosition(triggerPosition, width, height)
|
|
202
|
+
: clampTriggerPosition({
|
|
203
|
+
left: position.includes('right') ? width - triggerSize - offset : offset,
|
|
204
|
+
top: position.includes('bottom') ? height - triggerSize - offset : offset,
|
|
205
|
+
}, width, height));
|
|
206
|
+
const pointRef = React.useRef(point);
|
|
207
|
+
pointRef.current = point;
|
|
208
|
+
const drag = React.useRef<Animated.ValueXY | null>(null);
|
|
209
|
+
if (!drag.current) drag.current = new Animated.ValueXY();
|
|
210
|
+
const dragStart = React.useRef(point);
|
|
211
|
+
React.useEffect(() => {
|
|
212
|
+
const next = clampTriggerPosition(pointRef.current, width, height);
|
|
213
|
+
pointRef.current = next;
|
|
214
|
+
setPoint(next);
|
|
215
|
+
if (triggerPosition) triggerPosition = next;
|
|
216
|
+
drag.current?.setValue({ x: 0, y: 0 });
|
|
217
|
+
}, [width, height]);
|
|
218
|
+
const responder = React.useMemo(() => PanResponder.create({
|
|
219
|
+
onStartShouldSetPanResponder: () => false,
|
|
220
|
+
onMoveShouldSetPanResponder: (_, gesture) => draggable && Math.hypot(gesture.dx, gesture.dy) > 7,
|
|
221
|
+
onPanResponderGrant: () => { dragStart.current = pointRef.current; },
|
|
222
|
+
onPanResponderMove: (_, gesture) => {
|
|
223
|
+
const next = clampTriggerPosition({ left: dragStart.current.left + gesture.dx, top: dragStart.current.top + gesture.dy }, width, height);
|
|
224
|
+
drag.current?.setValue({ x: next.left - dragStart.current.left, y: next.top - dragStart.current.top });
|
|
225
|
+
},
|
|
226
|
+
onPanResponderRelease: (_, gesture) => {
|
|
227
|
+
const next = clampTriggerPosition({ left: dragStart.current.left + gesture.dx, top: dragStart.current.top + gesture.dy }, width, height);
|
|
228
|
+
triggerPosition = next;
|
|
229
|
+
pointRef.current = next;
|
|
230
|
+
setPoint(next);
|
|
231
|
+
drag.current?.setValue({ x: 0, y: 0 });
|
|
232
|
+
},
|
|
233
|
+
onPanResponderTerminate: (_, gesture) => {
|
|
234
|
+
const next = clampTriggerPosition({ left: dragStart.current.left + gesture.dx, top: dragStart.current.top + gesture.dy }, width, height);
|
|
235
|
+
triggerPosition = next;
|
|
236
|
+
pointRef.current = next;
|
|
237
|
+
setPoint(next);
|
|
238
|
+
drag.current?.setValue({ x: 0, y: 0 });
|
|
239
|
+
},
|
|
240
|
+
}), [draggable, width, height]);
|
|
190
241
|
const [busy, setBusy] = React.useState(false);
|
|
191
242
|
const recording = state === 'recording';
|
|
192
|
-
const edge = position.includes('right') ? { right: offset } : { left: offset };
|
|
193
|
-
const vertical = position.includes('bottom') ? { bottom: offset } : { top: offset };
|
|
194
243
|
const toggle = async () => {
|
|
195
244
|
if (busy) return;
|
|
196
245
|
setBusy(true);
|
|
@@ -200,23 +249,27 @@ export const BugSession = {
|
|
|
200
249
|
} catch (error) { onError?.(error); }
|
|
201
250
|
finally { setBusy(false); }
|
|
202
251
|
};
|
|
203
|
-
return <View pointerEvents="box-none" style={
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
252
|
+
return <View pointerEvents="box-none" style={styles.triggerLayer}>
|
|
253
|
+
<Animated.View {...(draggable ? responder.panHandlers : {})} style={[styles.triggerPosition, { left: point.left, top: point.top, transform: drag.current?.getTranslateTransform() }]}>
|
|
254
|
+
<Pressable
|
|
255
|
+
accessibilityRole="button"
|
|
256
|
+
accessibilityLabel={accessibilityLabel ?? (recording ? 'Stop BugSession recording' : 'Start BugSession recording')}
|
|
257
|
+
accessibilityHint={draggable ? 'Drag to move the BugSession button' : undefined}
|
|
258
|
+
accessibilityState={{ busy, selected: recording }}
|
|
259
|
+
disabled={busy}
|
|
260
|
+
onPress={() => { void toggle(); }}
|
|
261
|
+
style={[styles.trigger, recording && styles.triggerRecording, style]}
|
|
262
|
+
>
|
|
263
|
+
{children ?? <><Text style={styles.triggerMark}>b</Text><View style={[styles.triggerDot, recording && styles.triggerDotRecording]} /></>}
|
|
264
|
+
</Pressable>
|
|
265
|
+
</Animated.View>
|
|
214
266
|
</View>;
|
|
215
267
|
},
|
|
216
268
|
};
|
|
217
269
|
|
|
218
270
|
const styles = StyleSheet.create({
|
|
219
|
-
triggerLayer: {
|
|
271
|
+
triggerLayer: { ...StyleSheet.absoluteFill, zIndex: 9999 },
|
|
272
|
+
triggerPosition: { position: 'absolute' },
|
|
220
273
|
trigger: { width: 58, height: 58, borderRadius: 29, backgroundColor: '#173B34', alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: '#000', shadowOpacity: 0.24, shadowRadius: 8, shadowOffset: { width: 0, height: 4 } },
|
|
221
274
|
triggerRecording: { backgroundColor: '#B9382F' },
|
|
222
275
|
triggerMark: { color: '#F4F4EC', fontSize: 31, fontWeight: '800', lineHeight: 38 },
|