npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@outcode/bug-reporter-native

v2.1.1

Published

In-app bug reporter for React Native / Expo: a draggable floating button that captures the screen, lets users annotate it (draw, arrow, box, blur, text), set severity/type, and file to ClickUp or any custom backend.

Readme

@outcode/bug-reporter-native

npm version npm downloads license: MIT types: TypeScript

In-app bug reporter for React Native & Expo. A draggable floating button captures the current screen, lets users annotate it (draw, arrow, box, blur, text), set a severity and type, and file a report to ClickUp or any custom backend.

Part of the OutCode Bug Reporter monorepo. For the web (React, Angular, Vue, Svelte, Solid, vanilla), see @outcode/bug-reporter-web.

Features

  • 🐛 Draggable floating button that snaps to the screen edge
  • ✏️ Annotate — pen, arrow, box, blur/redact, and text on the captured screenshot
  • 🧭 Auto-captured context — platform, OS, screen, density, orientation, language, color scheme… consent-free
  • 🎨 Themeable — Indigo / Noir / Mint presets or your own tokens
  • 📡 Offline retry queue, breadcrumb capture, screenshot compression
  • 🧩 Pluggable backends — ClickUp out of the box, or any repository / HTTP endpoint
  • 📳 Shake to report — optional, sensor-agnostic, and inert until you wire an accelerometer
  • 📦 ESM + CJS, fully typed; native peers are externalized (never bundled)

Install

npm install @outcode/bug-reporter-native @outcode/bug-reporter-core
# peers (use `npx expo install …` on Expo):
npm install react-native-svg react-native-view-shot react-native-safe-area-context
# Optional — only for shake to report. Pick the one that matches your setup;
# the library installs neither, and without one shake is simply inert.
npm install react-native-sensors     # bare React Native
npx expo install expo-sensors        # Expo

Requires a dev build / bare workflow — react-native-view-shot ships native code that is not in the Expo Go sandbox. Screen capture silently returns nothing in Expo Go; run a dev build (expo run:ios / expo run:android / EAS Build).

Quick start

Wrap your app so the reporter can screenshot the current screen, then mount the button:

import {
  BugReporterButton,
  BugReporterCaptureRefProvider,
  BugReporterScreenCaptureView,
} from '@outcode/bug-reporter-native';
import { OutcodeBackendBugReporterRepository } from '@outcode/bug-reporter-core';

export default function App() {
  return (
    <BugReporterCaptureRefProvider>
      <BugReporterScreenCaptureView style={{ flex: 1 }}>
        {/* …your navigator / screens… */}
      </BugReporterScreenCaptureView>

      <BugReporterButton
        config={{
          appName: 'My Mobile App',
          appVersion: '1.0.0',
          theme: 'indigo',
          // Recommended: reports go through the OutCode bug service, so no ClickUp token ships in the client.
          repository: new OutcodeBackendBugReporterRepository({
            endpoint: 'https://your-bug-service.example.com/api/bugs/ingest', // your bug service URL
            apiKey,     // a scoped `bug:create` key
            targetKey,  // the project routing key from the bot's "Bug Trends" tab
          }),
          // Internal/trusted builds can talk to ClickUp directly instead (ships the token in the client):
          // repository: new ClickUpBugReporterRepository({ apiKey, listId }),
        }}
      />
    </BugReporterCaptureRefProvider>
  );
}

Reporting a bug while a modal or dropdown is open

In-tree overlays — dropdowns, menus, popovers a library renders inside your view tree — are handled by mounting order. The button sits at elevation: 24 (the Material ceiling that dialogs and menus use), so mount <BugReporterButton> last at your app root, as in the example, and it paints above them.

A host Modal is different, and no styling fixes it. React Native presents a Modal in a separate native window above the entire app, and modals stack by presentation order — so nothing in your view tree can rise above one. (Hosting the button in a transparent Modal of its own doesn't work either: RN's modal view controller installs a plain full-screen UIView with no hitTest override, so that window swallows every touch and your app becomes untappable — pointerEvents can't give them back. On Android a visible Modal also captures the hardware back button.)

Use the ref instead: trigger the flow from inside your own modal, and the reporter's overlays — presented after yours — land on top:

import { useRef } from 'react';
import { BugReporterButton, type BugReporterButtonHandle } from '@outcode/bug-reporter-native';

const reporter = useRef<BugReporterButtonHandle>(null);

<BugReporterButton ref={reporter} config={config} />
// inside your own modal:
<Button title="Report a bug" onPress={() => reporter.current?.open()} />

Note that native capture is view-based: it snapshots the view you wrapped in BugReporterScreenCaptureView. A Modal lives in its own native window outside that view, so the screenshot shows the screen behind your modal, not the modal itself. Describe the dialog in the report, or wrap the modal's content in its own capture view if you need it in the shot.

Shake to report

Shaking the device opens the reporter, so a tester doesn't have to find the floating button — and a build can hide the button entirely and still be reportable.

React Native ships no accelerometer, so rather than force a native dependency on every consumer the sensor is pluggable. Install one of the optional peers above and pass the matching adapter:

import { rnSensorsAccelerometerProvider } from '@outcode/bug-reporter-native/react-native-sensors';
// Expo: import { expoAccelerometerProvider } from '@outcode/bug-reporter-native/expo-sensors';

<BugReporterButton config={config} accelerometerProvider={rnSensorsAccelerometerProvider} />

Or register one once at startup, if you mount the button in several places:

import { setDefaultAccelerometerProvider } from '@outcode/bug-reporter-native';
import { rnSensorsAccelerometerProvider } from '@outcode/bug-reporter-native/react-native-sensors';

setDefaultAccelerometerProvider(rnSensorsAccelerometerProvider);

With no provider, shake does nothing and the floating button remains the way in. Nothing throws over a missing sensor.

Shake-only builds

Pair it with showButton: false to ship the reporter with no visible bug button — reachable by a gesture briefed testers know about, invisible to everyone else:

const config = { appName: 'My App', showButton: __DEV__, /* … */ };

Shake is unavailable to anyone who can't shake the device, so it should never be the only way in — keep a settings row that calls ref.current?.open().

Bring your own motion stream

AccelerometerProvider is just a subscribe function, so any source works. Samples must be in g (≈1.0 at rest) — divide m/s² readings by the exported GRAVITY:

import { GRAVITY, type AccelerometerProvider } from '@outcode/bug-reporter-core';

const myProvider: AccelerometerProvider = (onSample, intervalMs) => {
  const sub = mySensor.subscribe(({ x, y, z }) =>
    onSample({ x: x / GRAVITY, y: y / GRAVITY, z: z / GRAVITY })
  );
  return { remove: () => sub.unsubscribe() };
};

This is also the workaround if your bundler can't resolve the adapter subpaths (see below).

Tuning

config.shake holds sensitivity knobs — it is not an on/off switch (the provider is):

| Option | Default | | | --- | --- | --- | | threshold | 1.2 | How far past resting gravity a reading must swing to count as a jolt, in g. | | minDurationMs | 1000 | How long the shaking must be sustained. | | requiredJolts | 4 | Jolts needed within one run. | | maxGapMs | 400 | A longer pause ends the run and starts a new one. | | cooldownMs | 3000 | Quiet period after a reported shake. | | joltGapMs | 100 | Minimum gap between counted jolts, so one peak isn't counted twice at 60Hz. | | intervalMs | 60 | Sampling interval requested from the provider. |

Requiring a duration rather than just a count is what separates a shake from a knock: three jolts can land in 200ms when a phone is set down hard, so asking for a second of continuous shaking means the user has to mean it. The gap rule stops that second being assembled out of unrelated bumps.

The reporter subscribes only while the app is foregrounded, ignores samples while a report is on screen, and mutes for cooldownMs when the flow closes — so the shake that dismissed it can't reopen it.

React Native < 0.79: Metro only honours the exports map when unstable_enablePackageExports is on, which became the default in 0.79. On older versions the adapter subpath resolves through a root re-export shim that ships with the package, so the import above still works. If your bundler resolves neither, use a custom provider as shown above.

WiFi / cellular context (optional)

The reporter auto-captures consent-free context. To add network type on native, inject it via collectContext (e.g. with expo-network):

import * as Network from 'expo-network';

config={{
  // …
  collectContext: async () => {
    const n = await Network.getNetworkStateAsync();
    return { Network: n.type, Online: String(n.isConnected) };
  },
}}

Redaction

The blur tool paints an opaque block rather than a real blur, because a blur can sometimes be inverted and a password or a customer's name deserves better than that.

It also fails closed. Annotations are flattened into the screenshot before it is submitted, and if that flatten fails the original capture — which still shows what was redacted — is not sent as a fallback. The editor stays open with an error instead, and Continue retries. Reports with no redaction still fall back to the unflattened capture, since nothing was hidden to lose.

Theming

The three presets are a starting point, not the whole menu — any colour works:

<BugReporterButton config={{ appName: 'My App', theme: 'noir' }} />                        // preset
<BugReporterButton config={{ appName: 'My App', theme: { accent: '#FF5C00' } }} />         // custom accent
<BugReporterButton config={{ appName: 'My App', theme: { preset: 'noir', accent: '#FF5C00' } }} />
<BugReporterButton config={{ appName: 'My App', fabColor: '#FF5C00' }} />                  // just the button

A custom accent is enough on its own: accentPress, ring and onAccent are derived from it, so the pressed state, selection tints and the button's bug glyph all follow — a light brand colour gets a dark icon rather than white-on-white. Set any of those tokens explicitly to override the derived value.

fabColor recolours the floating button and its halo only, leaving the rest of the flow on the theme accent. Both accept any hex or rgb() string; an unparseable value falls back to the accent instead of throwing.

Every token is overridable — see BugReporterTheme in @outcode/bug-reporter-core for the full list.

Device brand and model

Reports include the device's brand and model automatically, with no extra dependency:

Brand name: Samsung
Model: SM-S921B

On Android these come from Platform.constants (Build.BRAND, Build.MANUFACTURER, Build.MODEL). The brand is title-cased; the model is the raw SKU, kept verbatim because mapping SKUs to marketing names ("Galaxy S24") needs a lookup table that is stale on every device launch.

On iOS there is no machine identifier available to JavaScript, so you get Apple plus the interface idiom (iPhone / iPad). If you need the real model, supply it yourself — a host-provided value always wins:

import DeviceInfo from 'react-native-device-info';

<BugReporterButton
  config={{
    appName: 'My App',
    deviceInfo: { brand: DeviceInfo.getBrandSync(), model: DeviceInfo.getModelSync() },
  }}
/>

Props

BugReporterButton:

| Prop | Type | Notes | | --- | --- | --- | | config | BugReporterConfig | Required. Full reference in the root README. | | label? | string | Accessible label for the floating button (default 'Report a bug'). | | captureRef? | CaptureViewRef | Explicit capture target. Usually unnecessary — BugReporterCaptureRefProvider supplies it. | | accelerometerProvider? | AccelerometerProvider | Wire up shake-to-report. See Shake to report. |

Ref handle: open() and close() — see Reporting a bug while a modal or dropdown is open.

Configuration

Full BugReporterConfig reference lives in the root README.

License

MIT © OutCode Software