@bugcapture/react-native
v0.2.1
Published
BugCapture for React Native — one-tap bug reports with screenshot, logs and device context, filed to the same backend as the browser extension.
Maintainers
Readme
@bugcapture/react-native
BugCapture for React Native. A tester taps a button, types a title and description, hits Save — and a bug report with a screenshot and full device context lands in the same dashboard the browser extension writes to, ready to file to Jira, ClickUp or GitHub.
This is M1. It writes a real report end to end. The queue (M2), console/network
capture (M3) and touch trails (M4) come next — see
docs/FEATURE_mobile_sdk.md for the full milestone list.
⚠️ Run the M0 spike before integrating
The SDK's single biggest technical risk is react-native-view-shot on your RN
version and architecture. If it doesn't work on Fabric/New Architecture, the
screenshot approach has to change — and everything else here is fine but far less
useful. Find out first; it takes ten minutes.
import { captureScreen } from "react-native-view-shot";
<Button title="spike" onPress={async () => {
const uri = await captureScreen({ format: "jpg", quality: 0.6, result: "tmpfile", width: 1080 });
console.log("captured:", uri); // open it — is it the screen, or black?
}} />Check on a real device, not just the simulator, and specifically on a screen
with whatever your app uses (camera, maps, video, WebView). Also confirm
react-native-keychain survives an app restart.
If the screenshot comes back black or throws, stop and tell me — only
src/capture/screenshot.ts needs to change, but it needs to change before M2.
Install
npm install @bugcapture/react-native \
react-native-view-shot react-native-device-info react-native-keychain \
react-native-blob-util @react-native-community/netinfo
cd ios && pod installreact-native-blob-util and @react-native-community/netinfo are optional but
recommended: blob-util streams the screenshot upload instead of buffering it as
base64 (which matters against the 5 MB cap), and netinfo records the connection
type. The SDK degrades quietly without either.
Not compatible with Expo Go — it needs a dev client or prebuild.
Upgrading to 0.2.0 needs a native rebuild
0.2.0 ships an android/ folder, so the package is now autolinked. Rebuild
after upgrading — a Metro reload will not pick it up:
npx expo run:android # or: cd android && ./gradlew assembleDebugSkip it and nothing breaks: every native path degrades to what 0.1.8 did. You
simply do not get shake-to-report or the all-window screenshot. There is no iOS
native module yet, so pod install has nothing new to do.
Integrate
One line at the app entry:
// index.js
import { withBugCapture } from "@bugcapture/react-native";
import App from "./App";
export default withBugCapture(App, {
firebase: {
apiKey: "…",
projectId: "bugcapture-qa",
storageBucket: "bugcapture-qa.firebasestorage.app",
},
environment: "staging",
});Then trigger it from the button next to your existing log button:
import { BugCapture } from "@bugcapture/react-native";
<Pressable onPress={() => BugCapture.show()}>
<BugIcon />
</Pressable>Optionally, tell it which screen the tester is on so reports carry a route instead of an empty URL:
<NavigationContainer onStateChange={BugCapture.onNavigationStateChange}>How testers sign in
Once per install, with the same email and password as the BugCapture dashboard. The refresh token is stored in the Keychain, so they never see the sign-in sheet again unless they sign out.
There is no sign-up in the app — accounts are created on the dashboard. This is deliberate: signing in as themselves is what makes reports appear under their name and what lets Jira/ClickUp filing use their saved integration credentials.
API
| Call | Purpose |
| --- | --- |
| withBugCapture(App, config) | Wrap the root component |
| <BugCaptureOverlay> | Wrap your own <Modal>'s contents so the button — and the screenshot — reach inside that window |
| BugCapture.show({ route? }) | Open the bug sheet (captures the screen first) |
| BugCapture.hide() | Close it |
| BugCapture.setRoute(name) | Set the current screen manually |
| BugCapture.onNavigationStateChange | Spread onto NavigationContainer |
| BugCapture.setFloatingButton(on) | Show/hide the draggable bug button |
| BugCapture.setFloatingButtonOpacity(v) | Fade it, 0.2–1 |
| BugCapture.log(message, level?) | Add a line from your own logger |
| BugCapture.addStep(label) | Add a step to the reproduction trail |
| BugCapture.signIn/signOut/getUser | Session control |
Config
| Key | Default | |
| --- | --- | --- |
| firebase | — | { apiKey, projectId, storageBucket } (required) |
| orgId | — | Which workspace reports go to. Only needed when a tester belongs to more than one — otherwise the sheet asks, or uses the only one |
| environment | "UAT" | Preselects an Environment chip. Dev/UAT/Prod match case-insensitively; anything else is added as a fourth chip |
| floatingButton | false | Start with the draggable bug button visible |
| floatingButtonOpacity | 1 | 0.2–1; lets a tester fade it over a screen it covers |
| captureConsole | true | console.*, uncaught errors, unhandled rejections |
| captureNetwork | true | fetch / axios / XHR / WebSocket |
| captureNetworkBodies | false | Request/response bodies. Off by default — redaction catches tokens, not customer_name |
| logWindowMinutes | 5 | How much history a report carries. 0 = count caps only |
| screenshotWidth | 1080 | Longest edge before JPEG encode |
| captureAsyncStorage | false | Off by default — apps keep auth tokens there |
| shakeToReport | true | Shake the phone to open the sheet. Android, and a no-op until you rebuild. Turn it off for a build that goes to real users |
| shakeSensitivity | 1 | 0.2–3; higher is more sensitive. Clamped so it can never be set into walking range |
| nativeCapture | true | Use the all-window native screenshot when it is available. false forces the JavaScript path |
| extra | {} | Key/values attached to every report |
Shake to report
Shake the phone on any screen and the sheet opens. It is the only trigger that
reaches a native screen, a system dialog or a <Modal> nobody wrapped — a
view-based trigger cannot leave its own window.
On by default, unlike the floating button: a button occupies your UI from the
moment it installs, a shake occupies nothing until someone shakes. Set
shakeToReport: false for a build that goes to real users, where a dropped
phone should not open a QA form.
Needs the native module, so it does nothing until you rebuild. Android only for now.
Marking up the screenshot
Tap the thumbnail on the sheet and the capture opens full screen: Select · Box · Arrow · Pixelate · Hide · Step, four colours, undo, redo and clear. Pinch to zoom, double-tap to toggle, one finger draws.
Two ways to hide something, and they answer different questions. Pixelate is a brush that paints mosaic blocks — it keeps the shape of what was there, useful when the reader needs to know a name was present without reading it. Hide paints an opaque bar and leaves nothing of the original; that is the one for card numbers and OTPs. Blurs have been read back, and a QA screenshot pasted into a ticket is exactly where a half-removed phone number matters.
The marks are burned into the uploaded JPEG, so what reaches Jira is already
redacted. The shape list also rides on report.json under annotations, in
normalised 0..1 coordinates, so the annotation stays editable. Only the
flattened image is ever uploaded — the raw one would put the redaction one
download away from undone.
Marks stay editable. With Select active, tap a mark to pick it, drag to move it, drag a corner or an arrow end to resize it, tap a colour to recolour it, Delete to remove it. Undo and Redo cover all of it.
No freehand pen: it is the one tool that needs a real drawing surface, and it was not worth adding a rendering dependency to this package for.
What the sheet asks for
Title, Description, Create in, Environment (Dev/UAT/Prod), Severity (Blocker–Trivial), Priority (P1–P4) — the same fields and the same defaults as the browser extension, so one bug looks the same wherever it was filed.
Three more appear only when they mean something: Workspace when the tester
belongs to more than one and orgId is not set, Folder when the workspace
has any, and Assignee when it has members.
The floating button
The SDK keeps no preference of its own — setFloatingButton(on) is a runtime
switch and floatingButton is its starting state. Store the choice wherever
your app stores settings and call it again on launch:
useEffect(() => {
AsyncStorage.getItem("floatingBug").then((v) =>
BugCapture.setFloatingButton(v === "1")
);
}, []);It hides itself while the screenshot is taken, so it never appears in a report.
Your own modals
A React Native <Modal> is a separate OS window — an Android Dialog, an iOS
presented view controller. Nothing in your app's view tree can draw over one at
any zIndex or elevation, and captureScreen() photographs the activity
behind it rather than the modal itself. So the button disappears over a modal,
and a screenshot taken there would be of the wrong screen.
Wrap the modal's contents and both problems go away:
import { BugCaptureOverlay } from "@bugcapture/react-native";
<Modal visible={open} transparent onRequestClose={close}>
<BugCaptureOverlay>
…your sheet…
</BugCaptureOverlay>
</Modal>Only one button exists at a time — nested modals hand it inward and back — and its position survives the handover. It is layout-neutral and safe to leave in even with the provider absent.
Do not wrap a payment or credential screen. The button being there invites a screenshot of a screen that should not be photographed, and redaction catches tokens, not card numbers.
This covers modals you own. Alert.alert, native share sheets and anything else
the JS tree never sees are still out of reach — that needs a native trigger.
What this does NOT do
Stated up front so nobody is surprised in a bug triage:
- It is not a crash reporter. A native crash (SIGSEGV, uncaught Java exception) kills the process before anything can be written. Use Crashlytics alongside it.
- Screenshots miss camera, video, MapView and hardware-layer WebViews on the
JavaScript path — those surfaces render black, because nothing can read them
back from the view hierarchy. The native path (0.2.0, Android, after a
rebuild) uses
PixelCopyand does read them. - Screenshots do not include
Alert.alert, native pickers, share sheets, system UI or other apps — even natively. Those are windows the app does not own; reaching them needsMediaProjectionand its consent dialog before every report. - Native-side network is invisible.
<Image>loads, analytics, payment and map SDKs all issue their requests natively, so they never cross the JS bridge and cannot appear in the network tab. Onlyfetch/axios/XHR and WebSocket traffic is captured. Seeing the rest needs a native interceptor (OkHttp / NSURLProtocol), which is a native module rather than a patch. - Console is empty in release builds if
babel-plugin-transform-remove-consoleis enabled — the plugin deletes the calls before this SDK can see them. UseBugCapture.log()from your own logger if you strip console in release. - Individual taps are not recorded. A React Native touch carries a node tag,
not a label, and the only route to one runs through React's internals. Screen
changes, foreground/background, rotation, connectivity and iOS memory warnings
are recorded automatically; use
BugCapture.addStep()for the rest. - Native crashes are invisible, and so is
logcat/NSLogoutput — both live below the JS runtime. - There is no mobile dashboard. No report list, history or editing in the app — reports are read and triaged at https://bugcapture-qa.web.app.
Development
npm test # pure-logic tests: ids, redaction, report assembly
npm run typecheck # tsc --noEmitThe tests cover everything that can be verified without a device — ID collision resistance, PII redaction, and the report/summary contract with the backend. The capture and UI layers need the M0 spike and a real device.
