@tillpulse/react-native
Crash reporting, performance monitoring, and friction detection for React Native apps. Battery- and bandwidth-aware. Offline-first. PII-scrubbing.
Install
npm install @tillpulse/react-native @react-native-async-storage/async-storage
# or yarn add ...
cd ios && pod install@react-native-async-storage/async-storage is required — events captured offline are persisted there and flushed on reconnect.
Initialise
Call TillPulse.init as the first thing your app does:
import TillPulse from '@tillpulse/react-native'
await TillPulse.init({
// Required
dsn: 'https://<key>@ingest.tillpulse.io/<project-id>',
// Identifying this build
release: '1.4.0', // used for regression detection
environment: 'production', // 'production' | 'staging' | 'development'
dist: '42', // build number (optional)
// Feature flags — all default true
enableNetworkTracking: true,
enableFrictionDetection: true,
enablePreCrashWatcher: true,
enableANRDetection: true,
enablePerformanceTracking: true,
// Privacy
sendDefaultPii: false, // include IP, device IDs (default false)
maxBreadcrumbs: 100,
// Filter or drop events before send
beforeSend: (event) => {
if (event.exception?.value?.includes('PaymentCancelled')) return null
return event
},
// Network
maxQueueSize: 500,
flushInterval: 30000,
// Debugging
debug: false,
})Identify users
TillPulse never accepts raw PII as a user identifier. Hash on the client first.
TillPulse.setUser({
id: hashedUserId, // required, hashed
segment: 'pro',
subscription: 'paid',
})
TillPulse.clearUser() // on logoutTags & context
TillPulse.setTag('region', 'ng')
TillPulse.setTag('network_type', '3g')
TillPulse.setContext('device', {
model: 'Tecno Spark 10',
ram_mb: 4096,
})Tags are searchable and filterable in the dashboard. Context is shown on the issue detail page but not indexed.
Manual capture
try {
await processPayment()
} catch (err) {
TillPulse.captureException(err, {
tags: { flow: 'payment', step: 'charge' },
extra: { amount: 5000, currency: 'NGN' },
})
}
TillPulse.captureMessage('Validation failed', 'warning', {
extra: { reason: 'expired_card' },
})Breadcrumbs
Auto-captured: navigation, network requests, console warnings/errors, touch events. Add custom breadcrumbs for product-specific events:
TillPulse.addBreadcrumb({
category: 'payment',
message: 'Initiated M-Pesa STK push',
level: 'info',
data: { phone: '+254xxxxxxxx', amount: 5000 },
})React Navigation integration
import { NavigationContainer } from '@react-navigation/native'
<NavigationContainer
onReady={() => { routeNameRef.current = navigationRef.getCurrentRoute()?.name }}
onStateChange={() => {
const next = navigationRef.getCurrentRoute()?.name
TillPulse.addBreadcrumb({
category: 'navigation',
message: `${routeNameRef.current} → ${next}`,
level: 'info',
})
routeNameRef.current = next
}}
>
{/* ... */}
</NavigationContainer>Performance spans
const tx = TillPulse.startTransaction({ name: 'checkout', op: 'flow' })
const span = tx.startSpan({ op: 'http.client', description: 'POST /charge' })
try {
await charge()
span.finish('ok')
tx.finish('ok')
} catch (e) {
span.finish('internal_error')
tx.finish('internal_error')
throw e
}Or wrap a promise:
await TillPulse.withTransaction(
{ name: 'checkout', op: 'flow' },
async () => { /* ... */ }
)Friction detection
Auto-detected: rage taps (3+ on the same target within 1s), dead-zone taps (non-interactive area), ghost taps (rapid tap with no visible response). Surface on the issue's FrictionHeatmap overlay.
TillPulse.setFrictionDetection(false) // disable globally
TillPulse.setFrictionDetection(true)Native crash + jank
The SDK installs native uncaught-exception handlers (NSSetUncaughtExceptionHandleron iOS, Thread.setDefaultUncaughtExceptionHandler on Android) and a cold-start / frame-jank monitor (CADisplayLink / Choreographer). Captured automatically on init — no extra calls required.
Pre-crash watcher
A heartbeat process records system state on a short interval. If the app dies unexpectedly, the last 20 breadcrumbs, memory pressure, CPU usage, battery, and last screen are flushed on next launch as a synthetic PreviousSessionEndedUnexpectedly event.
Offline queue
await TillPulse.flush() // before backgrounding
const size = await TillPulse.getQueueSize()Bounded by event count and total payload size — oldest dropped first. Backoff is exponential up to 6 attempts; 5xx + Retry-After headers honoured.
PII scrubbing
The SDK scrubs the following before transmission, automatically:
- Email addresses
- Phone numbers (international formats)
- Mobile-money transaction references
- Payment card numbers (Luhn-checked)
- Bank account / national-ID patterns
- Bearer tokens in
Authorizationheaders - IPv4 addresses in payloads
Server-side, the ingest API runs the same scrubber a second time before any event reaches Postgres or ClickHouse — defence in depth.
Symbolication
Stack frames default to obfuscated names on minified JS, ProGuarded Android, and stripped iOS builds. Upload symbols on every release:
- JS source maps —
POST /api/projects/:id/source-maps - Android ProGuard mapping — same endpoint,
symbolication_type=proguard - iOS dSYM —
POST /api/projects/:id/dsyms(UUID auto-extracted)
See Source maps & symbolication for the full guide.
Expo
Compatible with Expo SDK 50+. Use expo-build-properties if you need to configure the native modules. The auto-installed config plugin handles iOS Pod and Android Gradle wiring for you.
TypeScript
import type {
TillPulseEvent,
TillPulseUser,
TillPulseOptions,
Breadcrumb,
SeverityLevel,
PerformanceSpan,
} from '@tillpulse/react-native'Troubleshooting
Events aren't appearing
- Set
debug: trueand watch the console for[TillPulse]logs. - Check the DSN in the dashboard — settings > SDK Setup.
- If on a corporate network, allow
ingest.tillpulse.iothrough the firewall. - Hit
TillPulse.flush()manually — the auto-flush interval defaults to 30s.
Stack frames are obfuscated
Upload symbols. See Source maps.
Rate-limited
Default ingest cap is 10,000 events / hour / DSN. Reduce noise via beforeSend or raise the cap on the project's settings page.