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 

### 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 user@example.com 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('user@example.com')).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: 'a@b.com' };
 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 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.

### Consent and Compliance

- 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

 Metric Target Measurement 
 Collector overhead < 1 ms/frame idle react-native-performance / Systrace 
 RAM buffer < 2 MB Flipper React DevTools memory profiler 
 Battery impact < 5% over 24 h Android Battery Historian / iOS Energy Log 
 Network payload ≤ 256 KB/batch maxBatchSizeBytes config 

 Profiling steps:

- npx react-native-performance → record 30 s trace.

- Open in Perfetto/Chrome DevTools → filter Monitoring module.

- Verify no frame drops > 16 ms during flush.

## Failure Modes, Troubleshooting, and Rollback

<div class="my-stack-md overflow-x-auto">
<table class="w-full min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Symptom</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Cause</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Detection</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Mitigation</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Rollback Step</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Frame drops &gt; 16 ms</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">UI freeze detector too frequent</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Systrace shows <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">setInterval</code> / rAF spikes</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Increase <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">uiFreezeIntervalMs</code> to 200 ms; disable via remote config</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">disableCollector(&#39;uiFreeze&#39;)</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Buffer OOM</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Offline &gt; 24 h, <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">maxBuffer</code> too high</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">AsyncStorage size &gt; 5 MB</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Lower <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">maxBuffer</code> to 200; persist only last 50 KB</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Clear <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">monitor:buffer</code> in AsyncStorage</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">PII in logs</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Redaction pattern missed</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Log audit finds email/token</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Add pattern to <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">piiPatterns</code>; redeploy config</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Hotfix config via remote config</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Alert fatigue</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Thresholds too tight</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">&gt; 10 alerts/day per rule</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Multiply threshold by 3× p99 baseline</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Adjust PromQL rule threshold</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Native crash invisible</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">JS-only pilot</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">previous_unclean_exit</code> spikes, no <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">js.exception</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Integrate <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">react-native-exception-handler</code> or vendor SDK</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Correlate via <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">session_id</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Time skew</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Device clock drift</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">ts</code> vs server receipt delta &gt; 5 min</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Use <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">performance.now()</code> for durations; server corrects</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">N/A (server-side)</td></tr></tbody>
</table>
</div>

### 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 :

- Alert fires: ReactNativePurchaseFailureSpike (failure rate 18% Stripe) + ReactNativeAPILatencyRegression (catalog p95 2.1 s).

- Dashboard triage: Purchase funnel shows authentication_required 60% of failures; API latency heatmap shows /v1/catalog regression on Android 10 devices.

- 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.

- 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.