sn: process.env.CRASH_REPORTING_DSN,
environment: process.env.NODE_ENV,
// Async transport prevents ANR during crash flush
transport: 'async-batch',
// Local queue ensures offline resilience
enableOffline: true,
maxQueueSize: 50,
flushTimeout: 30000, // 30s batch window
// Context sampling reduces payload size & PII exposure
contextSampling: {
device: true,
app: true,
network: true,
user: false, // Disabled by default; enable with explicit consent
},
});
};
### Step 2: Implement Context Enrichment & PII Scrubbing
Raw crash data is useless without session context. Enrich crashes with deterministic, non-sensitive metadata. Implement regex-based scrubbing before payload serialization.
```typescript
import { Scrubber } from '@codcompass/pii-utils';
const PII_PATTERNS = [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
/\b\d{3}-\d{2}-\d{4}\b/g, // SSN pattern
/(?:password|secret|token|api_key)\s*[:=]\s*\S+/gi,
];
export const enrichCrashContext = (context: Record<string, unknown>) => {
const sanitized = Scrubber.sanitize(context, PII_PATTERNS);
CrashReporter.setContext('session', {
id: context.sessionId,
duration: context.sessionDuration,
screen: context.currentRoute,
networkType: context.networkState,
});
CrashReporter.setContext('device', {
model: context.deviceModel,
os: context.osVersion,
memoryUsage: `${context.memoryUsageMB}MB`,
storageFree: `${context.storageFreeGB}GB`,
});
return sanitized;
};
Step 3: Integrate Error Boundaries & Non-Fatal Routing
Fatal crashes terminate the process. Non-fatal errors degrade UX silently. Route them separately to prioritize engineering effort.
import React, { ErrorInfo } from 'react';
interface CrashBoundaryProps {
children: React.ReactNode;
fallback: React.ComponentType<{ error: Error }>;
}
export const CrashBoundary: React.FC<CrashBoundaryProps> = ({ children, fallback: Fallback }) => {
const [hasError, setHasError] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);
React.useEffect(() => {
const unsubscribe = CrashReporter.onError((err: Error) => {
// Non-fatal JS errors route to analytics pipeline
CrashReporter.captureException(err, { level: 'warning', tags: { source: 'js-runtime' } });
});
return unsubscribe;
}, []);
React.useEffect(() => {
if (hasError && error) {
// Fatal boundary crashes route to crash pipeline
CrashReporter.captureException(error, { level: 'fatal', tags: { source: 'react-boundary' } });
}
}, [hasError, error]);
if (hasError) {
return <Fallback error={error!} />;
}
return (
<ErrorBoundary
onError={(err: Error, info: ErrorInfo) => {
setError(err);
setHasError(true);
}}
>
{children}
</ErrorBoundary>
);
};
Step 4: Automate Symbolication in CI/CD
Raw native crashes are memory addresses. Symbol maps (dSYM for iOS, ProGuard/R8 mapping for Android) must be uploaded during build time.
# .github/workflows/crash-symbolication.yml
name: Upload Crash Symbol Maps
on:
push:
tags: ['v*']
jobs:
upload-symbols:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Install dependencies
run: npm ci
- name: Build iOS & Generate dSYM
run: |
cd ios && xcodebuild -workspace App.xcworkspace -scheme App -configuration Release -derivedDataPath build
- name: Build Android & Generate ProGuard Map
run: |
cd android && ./gradlew assembleRelease
- name: Upload to Crash Reporter
env:
CRASH_AUTH_TOKEN: ${{ secrets.CRASH_AUTH_TOKEN }}
run: |
npx @codcompass/crash-cli upload-ios --path ios/build/Build/Products/Release-iphoneos
npx @codcompass/crash-cli upload-android --path android/app/build/outputs/mapping/release
Architecture Decisions & Rationale
- Local Queue over Immediate Upload: Mobile networks are unstable. A local SQLite/AsyncStorage queue with exponential backoff guarantees delivery without blocking the UI thread or draining battery.
- Context Sampling over Full Collection: Sending full user objects, request payloads, or device identifiers violates data minimization. Sampling deterministic metadata (OS version, memory state, route) provides debugging value without compliance risk.
- Separate Fatal/Non-Fatal Routing: Fatal crashes require immediate engineering attention. Non-fatal errors (e.g., failed API calls, UI glitches) belong in analytics pipelines. Mixing them dilutes prioritization.
- CI-Driven Symbolication: Symbol maps change with every build. Uploading them during CI ensures crashes are resolved before developers see them, eliminating manual mapping steps.
Pitfall Guide
1. Synchronous Crash Flushing
Mistake: Calling crashReporter.flush() on the main thread before app termination.
Impact: Triggers ANR/cold start penalties. iOS watchdog kills the process; Android triggers Application Not Responding.
Best Practice: Use async batch transport. Rely on OS-level crash handlers (SIGSEGV/NSException) to flush queued payloads during process termination.
2. Ignoring Native Bridge Crashes
Mistake: Only capturing JavaScript-layer errors in React Native/Flutter apps.
Impact: Native module crashes (camera, Bluetooth, navigation) appear as unhandled process terminations with zero stack context.
Best Practice: Bridge native crash handlers to the JS layer. Use NativeModules event emitters or platform channels to forward NSException/Throwable objects before process death.
3. Over-Collecting PII in Breadcrumbs
Mistake: Logging full navigation history, API responses, or user inputs as breadcrumbs.
Impact: GDPR/CCPA violations. Audit failures. Unnecessary storage costs.
Best Practice: Implement scrubbing regex at the SDK boundary. Log only route paths, HTTP status codes, and action types. Never log response bodies or form data.
4. Missing Symbol Maps in Release Builds
Mistake: Building release APKs/IPAs without uploading dSYM/ProGuard mappings to the crash reporter.
Impact: 100% of native crashes show as 0x1a2b3c4d addresses. Debugging becomes impossible without manual symbolication.
Best Practice: Automate symbol upload in CI. Verify mapping integrity with a test crash on a staging build before production rollout.
5. No Network Retry or Backoff Strategy
Mistake: Assuming crashes upload immediately. Dropping payloads on 4xx/5xx responses.
Impact: Silent data loss during carrier handoffs, airplane mode, or API outages.
Best Practice: Implement exponential backoff with jitter. Queue payloads locally. Retry on network state changes (NetInfo/ConnectivityManager). Cap retries at 7 days to prevent storage bloat.
6. Treating All Errors as Critical
Mistake: Routing every exception to the crash dashboard.
Impact: Alert fatigue. Engineers ignore critical crashes because they're buried under non-fatal noise.
Best Practice: Tag errors by severity. Fatal crashes route to PagerDuty/Slack critical channels. Non-fatal errors route to analytics dashboards with weekly digest reports.
7. Skipping Crash Path Testing
Mistake: Assuming the SDK works because it initializes without errors.
Impact: Unverified integrations. Crashes in production go unreported because the transport layer was misconfigured.
Best Practice: Force a test crash in staging. Verify symbolication. Validate context enrichment. Confirm queue persistence across app restarts.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small indie app (<10k DAU) | Default SDK with async transport | Low overhead, fast setup, sufficient for early validation | Minimal |
| Enterprise cross-platform (RN/Flutter) | Enriched client pipeline + CI symbolication | Handles bridge crashes, enforces compliance, scales with team size | Medium (CI compute + SDK tier) |
| High-compliance fintech/healthcare | Server-side symbolication + strict PII scrubbing + audit logging | Zero PII in transit, regulatory alignment, tamper-proof crash pipeline | High (dedicated infrastructure + compliance review) |
| Offline-heavy utility (IoT/field apps) | Local queue + deferred batch upload + storage capping | Survives extended disconnections, prevents storage bloat, preserves battery | Low-Medium (storage optimization required) |
Configuration Template
// crash-reporting.config.ts
import { CrashReporterConfig } from '@codcompass/crash-sdk';
export const crashConfig: CrashReporterConfig = {
dsn: process.env.CRASH_DSN,
environment: process.env.NODE_ENV || 'development',
transport: 'async-batch',
enableOffline: true,
maxQueueSize: 50,
flushTimeout: 30000,
retryPolicy: {
maxRetries: 5,
backoffBase: 2000,
jitter: true,
networkAware: true,
},
contextSampling: {
device: true,
app: true,
network: true,
user: false,
custom: ['route', 'action', 'api_status'],
},
scrubbing: {
enabled: true,
patterns: [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
/(?:password|token|secret|api_key)\s*[:=]\s*\S+/gi,
],
maxBreadcrumbLength: 256,
},
routing: {
fatal: { destination: 'crash-pipeline', alert: true },
nonFatal: { destination: 'analytics-pipeline', alert: false },
},
};
Quick Start Guide
- Install the SDK: Run
npm install @codcompass/crash-sdk and add your DSN to .env.
- Initialize in entry point: Import and call
initCrashReporting() before mounting your root component.
- Configure CI symbol upload: Add the provided GitHub Actions workflow or equivalent GitLab/CircleCI job to your repository.
- Force-test in staging: Trigger a test crash, verify symbolication in the dashboard, and confirm context enrichment.
- Monitor queue health: Track
upload_success_rate and queue_depth metrics. Adjust flushTimeout and maxQueueSize based on your user's network patterns.