E-NO
React Native monitoring 12 Min Read

React Native Monitoring Pilot: Instrumentation, Alerts, and Operations

calendar_today Published: 2026-08-09
update Last Updated: 2026-08-12
analytics SEO Efficiency: 100%
Technical guide illustration for React Native Monitoring Pilot: Instrumentation, Alerts, and Operations.

A practical, low-risk path to observable React Native apps. This pilot captures JavaScript exceptions, startup timing, UI freezes, network performance, and purchase outcomes. It defines alert rules and dashboards tied to user experience, and verifies everything locally before broad rollout. The modular TypeScript module, server-side alerting, and operational routines keep noise low and build confidence.

Prerequisites, Versions, and Assumptions

  • React Native ≥ 0.73 (Hermes default), Node ≥ 18, TypeScript ≥ 5.0, @react-native-async-storage/async-storage ≥ 1.21
  • Platforms: iOS 13+ / Android API 24+; Debug and Release builds; Expo SDK 50+ (managed and dev client)
  • Single JS bundle, single process, no CodePush in pilot (CodePush requires session reset on bundle swap)
  • All collectors run on the JS thread; no native bridge in pilot. Native crash correlation is covered in the Extension Path.

Architecture Overview

App → Monitoring Module (Collectors → Redactor → Buffer → Flusher) → Transport → Ingestion API

Data flow: Session start → event enqueue → periodic/background flush → server aggregation → alerts/dashboards. Threading stays on the JS thread; native crash correlation is added later. The module exposes a small, typed API so product code stays clean.

Configuration and Feature Flags

Remote config (monitoring.config.json) controls everything at runtime:

{
  "endpoint": "https://example.com/monitoring",
  "flushIntervalMs": 5000,
  "maxBuffer": 500,
  "maxBatchSizeBytes": 262144,
  "uiFreezeThresholdMs": 500,
  "uiFreezeIntervalMs": 100,
  "piiPatterns": [
    "\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}\\b",
    "\\b\\d{12,19}\\b",
    "Bearer\\s+[A-Za-z0-9\\-._~+/]+=*",
    "eyJ[A-Za-z0-9_-]*\\.[A-Za-z0-9_-]*\\.[A-Za-z0-9_-]*",
    "\\b(-?\\d+(\\.\\d+)?),\\s*(-?\\d+(\\.\\d+)?)\\b"
  ],
  "piiAllowlist": ["user_id", "device_id"],
  "enabledCollectors": ["jsExceptions", "startup", "uiFreeze", "network", "purchase"],
  "samplingRate": 1.0,
  "enableCompression": true,
  "hmacKeyId": "prod-key-1",
  "monitoringEnabled": true
}

Local override: store partial config in AsyncStorage key monitor:config_override for development.

Instrumentation API (TypeScript)

Core Types

export interface MonitoringConfig {
  endpoint: string;
  flushIntervalMs: number;
  maxBuffer: number;
  maxBatchSizeBytes: number;
  uiFreezeThresholdMs: number;
  uiFreezeIntervalMs: number;
  piiPatterns: string[];
  piiAllowlist: string[];
  enabledCollectors: CollectorName[];
  samplingRate: number;
  enableCompression: boolean;
  hmacKeyId: string;
  monitoringEnabled: boolean;
}

export type CollectorName = 'jsExceptions' | 'startup' | 'uiFreeze' | 'network' | 'purchase';

export interface Event {
  ts: number;
  session_id: string;
  type: 'event' | 'metric' | 'log';
  level: 'info' | 'warn' | 'error';
  name: string;
  fields: Record<string, unknown>;
  app: AppContext;
  rn: RNContext;
  device: DeviceContext;
  event_id: string;
}

Context objects (AppContext, RNContext, DeviceContext) carry version, bundle ID, Hermes flag, platform, OS version, and model. Every event includes a client-generated UUID event_id for idempotent ingestion.

Public Methods

export const Monitoring = {
  init: (config: Partial<MonitoringConfig>) => Promise<void>;
  markAppReady: (screen: string) => void;
  trackPurchaseAttempt: (fields: PurchaseEventFields) => void;
  trackPurchaseSuccess: (fields: PurchaseEventFields) => void;
  trackPurchaseFailure: (fields: PurchaseEventFields) => void;
  recordEvent: (name: string, fields: Record<string, unknown>, level?: Event['level'], type?: Event['type']) => void;
  setEnabled: (flag: boolean) => void;
  disableCollector: (name: CollectorName) => void;
  flush: () => Promise<void>;
  closeSession: () => Promise<void>;
  getDebugState: () => DebugState; // for MonitoringDebugPanel
};

PurchaseEventFields uses minor units (cents) and ISO 4217 currency. Failure reasons map to actionable buckets: authentication_required, insufficient_funds, network_error, cancelled, invalid_payment_method, unknown.

Collectors Deep Dive

JavaScript Exceptions

  • Mechanism: ErrorUtils.setGlobalHandler plus a console.error proxy.
  • Hermes note: Global handler works in Hermes 0.73+; minified stacks preserve frame structure but lose variable names. Upload source maps to the ingestion pipeline for symbolication.
  • Fatal vs non-fatal: isFatal flag from ErrorUtils; console.error captures non-fatal React warnings.

Startup Timing

  • tAppStart: Captured at module load via performance.now() (monotonic). If Performance.mark is available, mark app_start.
  • markAppReady(screen): Emits app.startup with phase: 'first_screen_ready', startup_ms, and screen.
  • Cold vs warm: Detected via session.previous_unclean_exit flag (true if prior session did not close cleanly).

UI Freeze Detection

  • Algorithm: requestAnimationFrame / setTimeout hybrid loop measuring frame budget.
  • Config: uiFreezeThresholdMs (default 500), uiFreezeIntervalMs (default 100).
  • Per-frame budget: 16.67 ms at 60 fps; lag = now - last - intervalMs.

Network Monitoring

  • Fetch wrapper: Clones Request for body reading if needed; captures method, sanitized URL, status, duration, request/response byte counts.
  • Redaction: Strips Authorization, Cookie, X-Api-Key headers; scrubs query params matching token|key|secret|sig.
  • AbortSignal: Propagates abort; records net.request_aborted with duration.

Purchase Flow

  • Provider enum: stripe | gplay | iap | other.
  • Amount in minor units (cents). Currency: ISO 4217.
  • Failure taxonomy: authentication_required, insufficient_funds, network_error, cancelled, invalid_payment_method, unknown.

Transport and Reliability

Buffer

  • Structure: Ring buffer persisted to AsyncStorage (monitor:buffer) plus in-memory head/tail.
  • Persistence: On enqueue, serialize buffer to AsyncStorage (debounced 500 ms). On init, hydrate from storage.
  • Limits: maxBuffer events (default 500) or maxBatchSizeBytes (default 256 KB), whichever is reached first.

Flush

  • Schedule: Interval flushIntervalMs (default 5 s) plus AppState background trigger.
  • Backoff: Exponential base 1 s, cap 60 s, jitter ±25%.
  • Batch: ≤ 50 events or 256 KB; gzip Content-Encoding: gzip when enableCompression=true.
  • Idempotency: Client-generated event_id (UUID v4 via crypto.randomUUID() with polyfill for RN).

Kill Switch

Remote config monitoring_enabled=false → immediate flush(), stop all collectors, clear buffer, disable network calls.

Alerting and Dashboards (Operational)

PromQL Alert Rules

# High JS exception rate
- alert: ReactNativeHighJSExceptionRate
  expr: |
    sum(rate(rn_js_exception_total[5m])) by (app_version)
    /
    sum(rate(rn_session_start_total[5m])) by (app_version)
    > 0.01
  for: 10m
  labels:
    severity: critical
    team: mobile-oncall
  annotations:
    runbook_url: "https://example.com/runbooks/react-native-js-errors"
    summary: "JS exception rate > 1% for {{ $labels.app_version }}"

# UI freeze surge
- alert: ReactNativeUIFreezeSurge
  expr: |
    sum(rate(rn_ui_freeze_total{lag_ms>700}[5m])) by (device_model)
    > 50
  for: 5m
  labels:
    severity: warning
    team: mobile-oncall
  annotations:
    runbook_url: "https://example.com/runbooks/react-native-ui-freezes"
    summary: "UI freeze surge on {{ $labels.device_model }}"

# API latency regression
- alert: ReactNativeAPILatencyRegression
  expr: |
    histogram_quantile(0.95, sum(rate(rn_net_request_duration_seconds_bucket{url=~"api\\.example\\.com.*"}[15m])) by (le, endpoint))
    > 1
  for: 15m
  labels:
    severity: warning
    team: backend-oncall,mobile-oncall
  annotations:
    runbook_url: "https://example.com/runbooks/api-latency"
    summary: "API p95 > 1s for {{ $labels.endpoint }}"

# Purchase failure spike
- alert: ReactNativePurchaseFailureSpike
  expr: |
    sum(rate(rn_purchase_failure_total[15m])) by (provider)
    /
    sum(rate(rn_purchase_attempt_total[15m])) by (provider)
    > 0.1
  for: 15m
  labels:
    severity: critical
    team: payments-oncall
  annotations:
    runbook_url: "https://example.com/runbooks/purchase-failures"
    summary: "Purchase failure rate > 10% for {{ $labels.provider }}"

Grafana Dashboard Panels

  • JS Exception Rate by Version (timeseries)
  • Previous Unclean Exits by Device (barchart)
  • Startup p50/p95 (timeseries)
  • UI Freezes Heatmap: Device × Lag (heatmap)
  • API Latency by Endpoint (timeseries)
  • Purchase Funnel: Attempt / Success / Failure by Provider and Reason (barchart)

Verification and Testing

Local Event Inspection

  • JS exception: Press test button → throw new Error('test') → verify js.exception with stack, isFatal=false.
  • Console error: console.error('test error') → verify js.console_error.
  • Startup: Cold launch → verify app.startup with startup_ms in device-expected range.
  • UI freeze: Debug button blocks JS thread 1 s (const t=Date.now(); while(Date.now()-t<1000){}) → verify ui.freeze with lag_ms ~ 1000.
  • Network: Fetch healthy/fail endpoints → verify net.request with sanitized URL, status, duration.
  • Purchase: Trigger attempt/success/failure → verify purchase.attempt, purchase.success, purchase.failure with provider, amount_cents, reason.
  • No server? Temporarily set flush() to console.log(JSON.stringify({events: payload})) and validate shapes/scrubbing.

Aggregation Sanity Checks

  • Session attribution: Every event includes session_id. previous_unclean_exit appears once at session start when prior session not closed.
  • Rate denominators: Emit explicit session.start event for reliable denominator. If absent, approximate by distinct session_id per day.
  • PII scrubbing: Inject [email protected] in field → confirm [redacted_email].

Platform and Release Checks

  • Android Release: Minified stacks preserved in js.exception; native crashes not captured (unclean-exit heuristic helps).
  • iOS Release: Background transition (AppState change) triggers flush.
  • Expo managed: Monitoring.init() in entry; console proxy works in dev/prod. With expo-dev-client, native modules available for future native crash capture.

Unit Tests (monitoring.test.ts)

import { Monitoring, redactPII, createBuffer } from './monitoring';

describe('PII redaction', () => {
  test('redacts email', () => expect(redactPII('[email protected]')).toBe('[redacted_email]'));
  test('redacts credit card (Luhn-valid)', () => expect(redactPII('4242 4242 4242 4242')).toBe('[redacted_numeric]'));
  test('redacts JWT', () => expect(redactPII('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c')).toContain('[redacted_jwt]'));
  test('preserves allowlisted keys', () => {
    const obj = { user_id: '123', email: '[email protected]' };
    expect(redactPII(obj)).toEqual({ user_id: '123', email: '[redacted_email]' });
  });
});

describe('Buffer eviction', () => {
  test('drops oldest when maxBuffer exceeded', () => {
    const buf = createBuffer({ maxBuffer: 3 });
    buf.push({id:1}); buf.push({id:2}); buf.push({id:3}); buf.push({id:4});
    expect(buf.toArray().map(e=>e.id)).toEqual([2,3,4]);
  });
});

describe('Flush retry/backoff', () => {
  test('exponential backoff with jitter', async () => {
    const flush = jest.fn().mockRejectedValueOnce(new Error('net')).mockResolvedValueOnce(undefined);
    const { flushWithBackoff } = await import('./monitoring');
    await flushWithBackoff(flush, { baseMs: 100, capMs: 1000, jitter: 0 });
    expect(flush).toHaveBeenCalledTimes(2);
  });
});

describe('Session unclean detection', () => {
  test('emits previous_unclean_exit when prior session not closed', async () => {
    await AsyncStorage.setItem('monitor:last_session', JSON.stringify({ id: 'old', startedAt: 1, closed: false }));
    const events: any[] = [];
    Monitoring.recordEvent = (name, fields) => events.push({name, fields});
    await Monitoring.init({});
    expect(events.find(e=>e.name==='session.previous_unclean_exit')).toBeTruthy();
  });
});

describe('Collector enable/disable', () => {
  test('disableCollector stops UI freeze detector', () => {
    Monitoring.disableCollector('uiFreeze');
    // verify no ui.freeze events emitted during simulated lag
  });
});

E2E (Detox) Spec (monitoring.e2e.ts)

describe('Monitoring E2E', () => {
  beforeAll(async () => { await device.launchApp({newInstance: true, delete: true}); });

  test('cold start emits app.startup', async () => {
    const events = await getMonitoringEvents();
    expect(events.find(e => e.name === 'app.startup' && e.fields.phase === 'first_screen_ready')).toBeTruthy();
  });

  test('background triggers flush', async () => {
    await device.sendToHome();
    await waitForFlush();
    const events = await getMonitoringEvents();
    expect(events.find(e => e.name === 'session.background_flush')).toBeTruthy();
  });

  test('unclean exit detected on next launch', async () => {
    await device.terminateApp();
    await device.launchApp({newInstance: true});
    const events = await getMonitoringEvents();
    expect(events.find(e => e.name === 'session.previous_unclean_exit')).toBeTruthy();
  });
});

CI gate: npm run test:monitoring in pipeline (runs Jest + Detox on emulator/simulator).

Debug Panel (MonitoringDebugPanel.tsx)

A React Native component that shows live state and lets QA trigger test events. All buttons have accessibilityLabel; panel works with TalkBack/VoiceOver.

import React from 'react';
import { View, Text, Button, ScrollView, StyleSheet } from 'react-native';
import { Monitoring } from './monitoring';

export const MonitoringDebugPanel = () => {
  const [state, setState] = React.useState(Monitoring.getDebugState());
  React.useEffect(() => {
    const id = setInterval(() => setState(Monitoring.getDebugState()), 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <ScrollView style={styles.container} accessibilityLabel="Monitoring Debug Panel">
      <Text style={styles.title}>Monitoring Debug</Text>
      <Text>Enabled: {String(state.enabled)}</Text>
      <Text>Buffer: {state.bufferLength}/{state.maxBuffer}</Text>
      <Text>Session: {state.sessionId}</Text>
      <Button title="Throw Test Error" onPress={() => { throw new Error('test'); }} accessibilityLabel="Throw test error" />
      <Button title="Console Error" onPress={() => console.error('test error')} accessibilityLabel="Log console error" />
      <Button title="Simulate UI Freeze" onPress={() => { const t=Date.now(); while(Date.now()-t<1000){} }} accessibilityLabel="Simulate UI freeze" />
      <Button title="Fetch Healthy" onPress={() => fetch('https://httpbin.org/get')} accessibilityLabel="Fetch healthy endpoint" />
      <Button title="Fetch Fail" onPress={() => fetch('https://httpbin.org/status/500')} accessibilityLabel="Fetch failing endpoint" />
      <Button title="Purchase Attempt" onPress={() => Monitoring.trackPurchaseAttempt({provider:'stripe', amount_cents: 1000})} accessibilityLabel="Simulate purchase attempt" />
      <Button title="Flush Now" onPress={() => Monitoring.flush()} accessibilityLabel="Flush buffer now" />
      <Button title="Toggle Enabled" onPress={() => Monitoring.setEnabled(!state.enabled)} accessibilityLabel="Toggle monitoring enabled" />
    </ScrollView>
  );
};
const styles = StyleSheet.create({ container: { padding: 16 }, title: { fontSize: 18, fontWeight: '600', marginBottom: 12 } });

Security and Privacy

PII Categories Handled

  • Email: Regex plus allowlist keys (user_id, device_id).
  • Credit card: Luhn validation optional (enable via piiPatterns).
  • JWT: Three-segment base64url pattern.
  • Bearer tokens: Authorization: Bearer <token> header scrubbing.
  • GPS coordinates: lat,lon pattern in strings.
  • IP addresses: Server-side scrubbing (not client).

Data Retention

  • Client: SESSION_TTL_MS (24 h) → discard stale events on init.
  • Server: 30 days raw events, 13 months aggregated metrics.
  • Map scrub_pii flag to consent state; GDPR/CCPA delete endpoint reference in runbook.
  • Transport security: Pin endpoint via remote config; validate TLS; sign payloads with HMAC-SHA256 (hmacKeyId in config) to reject tampering.
  • Secrets: Never log headers/query params in net.request; scrub before enqueue.

Performance and Overhead Budget

MetricTargetMeasurement
Collector overhead< 1 ms/frame idlereact-native-performance / Systrace
RAM buffer< 2 MBFlipper React DevTools memory profiler
Battery impact< 5% over 24 hAndroid Battery Historian / iOS Energy Log
Network payload≤ 256 KB/batchmaxBatchSizeBytes config

Profiling steps:

  1. npx react-native-performance → record 30 s trace.
  2. Open in Perfetto/Chrome DevTools → filter Monitoring module.
  3. Verify no frame drops > 16 ms during flush.

Failure Modes, Troubleshooting, and Rollback

SymptomCauseDetectionMitigationRollback Step
Frame drops > 16 msUI freeze detector too frequentSystrace shows setInterval / rAF spikesIncrease uiFreezeIntervalMs to 200 ms; disable via remote configdisableCollector('uiFreeze')
Buffer OOMOffline > 24 h, maxBuffer too highAsyncStorage size > 5 MBLower maxBuffer to 200; persist only last 50 KBClear monitor:buffer in AsyncStorage
PII in logsRedaction pattern missedLog audit finds email/tokenAdd pattern to piiPatterns; redeploy configHotfix config via remote config
Alert fatigueThresholds too tight> 10 alerts/day per ruleMultiply threshold by 3× p99 baselineAdjust PromQL rule threshold
Native crash invisibleJS-only pilotprevious_unclean_exit spikes, no js.exceptionIntegrate react-native-exception-handler or vendor SDKCorrelate via session_id
Time skewDevice clock driftts vs server receipt delta > 5 minUse performance.now() for durations; server correctsN/A (server-side)

Native Crash Gap

Add react-native-exception-handler (or vendor SDK) and emit native.crash with same session_id. Correlate in dashboard.

Time Skew

Durations use performance.now() (monotonic). Server applies server_received_ts - client_ts correction for absolute timestamps.

Realistic Technical Scenario: Black Friday Purchase Spike

Conditions: 5× traffic, Stripe 3DS challenges, Android low-end UI freezes, API p95 degradation.

Walkthrough:

  1. Alert fires: ReactNativePurchaseFailureSpike (failure rate 18% Stripe) + ReactNativeAPILatencyRegression (catalog p95 2.1 s).
  2. Dashboard triage: Purchase funnel shows authentication_required 60% of failures; API latency heatmap shows /v1/catalog regression on Android 10 devices.
  3. Mitigation:
  • Feature flag disable_heavy_animation → reduces UI freezes on low-end chipsets.
  • Remote config: increase flushIntervalMs to 10 s, enable enableCompression to reduce network contention.
  • Backend: add cache layer for catalog; Stripe: verify 3DS flow client-side.
  1. Post-incident: Collect 7 days pilot data → set new thresholds at 3× p99 baseline; update runbook with 3DS troubleshooting steps.

Operations Checklists

Daily

  • Review Stability dashboard: JS exception rate trends by latest app_version.
  • Check Performance: API p95 for primary endpoints; watch regressions vs yesterday.
  • Triage Alerts: Acknowledge, add context, link to runbooks.

Weekly

  • Tune thresholds: Reduce noise, add dimensions (device model, OS version).
  • Backlog review: Investigate top 3 exception signatures; add guardrails/fixes.
  • Data health: Sample event payloads → verify fields, scrubbing, session attribution.

Per Release

  • Compare beta vs prior production: Exception rate, startup p50/p95, API p95.
  • Spot-check low-end Android devices for UI freeze outliers.
  • Validate purchase flow outcomes by provider; ensure failure reasons map to actionable buckets.

Extension Path

  • Native crash capture: Integrate react-native-exception-handler or vendor SDK; tie to session_id.
  • Navigation/screen metrics: Record navigation.transition with duration_ms, from, to.
  • Foreground/background: Track app.foreground_duration_s, session.count by release channel.
  • Sampling: 1% verbose logs (full stack, Redux actions) via samplingRate config.

Conclusion

This pilot delivers a practical, low-risk path to monitor a React Native app: capture JavaScript exceptions, startup timing, UI freezes, network performance, and purchase outcomes; define alert rules and dashboards aligned to user experience; and verify everything locally before broad rollout. Start with the pilot, validate signals and scrubbing, then grow coverage and tighten thresholds steadily. The modular TypeScript module, server-side alerting, and operational routines keep noise low, build confidence, and make your React Native monitoring program effective and maintainable.

Related Research

Article Quality Score

Reader usefulness 100%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL