DOCS · SDK · REACT NATIVE

@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 logout

Tags & 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 Authorization headers
  • 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 mapsPOST /api/projects/:id/source-maps
  • Android ProGuard mapping — same endpoint, symbolication_type=proguard
  • iOS dSYMPOST /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: true and watch the console for [TillPulse] logs.
  • Check the DSN in the dashboard — settings > SDK Setup.
  • If on a corporate network, allow ingest.tillpulse.io through 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.