Expo SDK — API reference
All public exports from @reprojs/expo.
<ReproProvider>
The provider normalizes config, starts all collectors, manages the offline queue, and renders the wizard modal when triggered.
import { ReproProvider } from "@reprojs/expo"
<ReproProvider config={config}>
<App />
</ReproProvider>config: ReproConfigInput
| Field | Type | Default | Description |
|---|---|---|---|
projectKey | string | — | Your project's public key (rp_pk_…). Empty string = silent disable. |
intakeUrl | string | — | https://your-dashboard.example.com/api/intake. Empty = silent disable. |
reporter | { userId?: string, email?: string, name?: string } | null | Pre-fill the reporter identity. useRepro().identify(...) overrides at runtime. |
collectors.console | boolean | true | Patch console.* and buffer the last 200 entries. |
collectors.network.enabled | boolean | true | Patch globalThis.fetch and buffer the last 100 calls. |
collectors.network.captureBodies | boolean | false | Reserved for v1.1; currently ignored. |
collectors.breadcrumbs | boolean | true | Enable the useRepro().log(event, data) buffer. |
collectors.systemInfo | boolean | true | Attach device + env info at submit time. |
queue.maxReports | number | 5 | Max queued reports before oldest is evicted. |
queue.maxBytes | number | 10_485_760 (10 MB) | Byte cap across all queued attachments. |
queue.backoffMs | number[] | [1000, 5000, 30000, 120000] | Retry delays per attempt. |
redact.headerDenylist | string[] | ["authorization", "cookie", "x-api-key"] | Case-insensitive header names that get [redacted] in the network entry. |
redact.bodyRedactKeys | string[] | ["password", "token", "secret"] | Reserved for v1.1 body capture. |
theme.accent | string | "#6366f1" | Reserved; v1 UI uses the flame accent internally. |
theme.mode | "auto" | "light" | "dark" | "auto" | Reserved for v1.1. |
metadata | Record<string, string | number | boolean> | {} | Attached to every report. |
<ReproLauncher>
Opt-in floating bug-report button. Tap opens the wizard; drag relocates to any of the four corners.
<ReproLauncher
position="bottom-right"
offset={{ bottom: 24, right: 24 }}
icon={<CustomBugIcon />}
hideWhen={() => someAtom.value}
draggable
/>| Prop | Type | Default | Description |
|---|---|---|---|
position | "bottom-right" | "bottom-left" | "top-right" | "top-left" | "bottom-right" | Initial corner. Overridden by the persisted choice if the user has dragged. |
offset | { top?, bottom?, left?, right? } | 24 each | Distance from the edge. |
icon | React.ReactNode | 🐞 | Replace the default emoji. |
hideWhen | () => boolean | — | Called on every render; return true to hide (e.g. on specific routes). |
draggable | boolean | true | When false, pinned to position and ignores the persisted choice. |
The launcher returns null when the provider is in silent-disable mode, so it's always safe to render unconditionally.
useRepro()
React hook returning a ReproHandle for the nearest provider.
interface ReproHandle {
disabled: boolean
open: (opts?: { initialTitle?: string; initialDescription?: string }) => void
close: () => void
identify: (reporter: ReporterIdentity | null) => void
log: (event: string, data?: Record<string, string | number | boolean | null>) => void
setMetadata: (patch: Record<string, string | number | boolean>) => void
queue: {
pending: number
lastError: string | null
flush: () => Promise<void>
}
}disabled: boolean
true when the provider rendered but config was empty (silent disable). All other methods are no-ops in that state. Use for branching UI:
const repro = useRepro()
if (!repro.disabled) {
// feature-gate a "Report a bug" menu item
}open(opts?) / close()
Open / dismiss the wizard programmatically. initialTitle / initialDescription pre-fill the form step.
identify(reporter)
Set or clear the reporter's identity. null clears.
repro.identify({ userId: user.id, email: user.email, name: user.name })log(event, data?)
Emit a breadcrumb. The last 50 breadcrumbs ship with the next submitted report.
repro.log("checkout.failed", { cartId: "abc", total: 42.50 })setMetadata(patch)
Shallow-merge host-supplied metadata that ships with every report.
repro.setMetadata({ appVersion: "1.2.3", buildChannel: "staging" })queue
Read-only view of the offline queue + a manual flush trigger.
if (repro.queue.pending > 0) {
await repro.queue.flush()
}Repro singleton
Non-React callsites (analytics wrappers, error handlers, class components) can use the module-level singleton.
import { Repro } from "@reprojs/expo"
Repro.open()
Repro.log("purchase.clicked", { sku: "pro-annual" })
Repro.identify({ userId: "u_123" })
Repro.close()
await Repro.flush()It proxies to the currently-mounted provider. When no provider is mounted (or the provider is silently disabled), every call is a no-op — safe to sprinkle anywhere.
Type exports
import type { ReproConfigInput, ReproHandle } from "@reprojs/expo"ReproConfigInput matches the config shape accepted by <ReproProvider>. ReproHandle matches what useRepro() returns. ReporterIdentity is re-exported from @reprojs/shared.
Imperative config plugin (advanced)
The @reprojs/expo config plugin is loaded via app.json's plugins array. It's a no-op in v1 but participates in the Expo build chain so future releases can add Info.plist / AndroidManifest patches without requiring consumers to rewire their config.
If you want to manually compose multiple plugins without relying on the plugins array:
// app.config.ts
import withRepro from "@reprojs/expo/app.plugin.js"
export default ({ config }) => withRepro(config)