Capture exceptions from React, React Native, and Flutter apps — upload crash reports as searchable Markdown documents with breadcrumbs, device context, and offline queuing.
The Doctor SDKs turn your Lifestream Vault into a crash reporting backend. When an exception occurs in your app, the SDK captures the error, enriches it with breadcrumbs, device context, and session info, formats it as a Markdown document with YAML frontmatter, and uploads it to a vault. Every crash report becomes a first-class, searchable document — no separate error-tracking service required.
Two SDKs are available:
| Platform | Package | Version |
|---|---|---|
| React / React Native / Node.js | @lifestreamdynamics/doctor | 1.0.6 |
| Dart / Flutter | lifestream_doctor | 1.0.1 |
Both SDKs produce identical Markdown output and share the same HMAC request-signing protocol, so crash reports from all your platforms land in a single vault with consistent structure.
By the end of this guide you will have:
Prerequisites
Both SDKs are published to their respective package registries and ship with full type definitions. Choose the tab matching your platform.
# npm
npm install @lifestreamdynamics/doctor
# yarn
yarn add @lifestreamdynamics/doctor
# pnpm
pnpm add @lifestreamdynamics/doctorReact Native users install the same npm package. For persistent offline queuing and device context, also import the React Native adapter:
import { AsyncStorageBackend, installReactNativeHandlers, getReactNativeDeviceContext }
from '@lifestreamdynamics/doctor/react-native';
Source code & issues:
The Doctor SDK authenticates with your vault via an API key. For crash reporting, the key should be:
lsv_k_ (all Lifestream Vault API keys use this prefix)The full key is only shown once at creation time — copy and store it immediately. See the API Keys & Scoping guide for details on key rotation and best practices.
1. Go to Settings -> API Keys -> New Key
2. Name: "Crash Reports (Production)"
3. Scopes: select "write" only (uncheck "read")
4. Vault: select your crash reports vault
5. Click Create — copy the key immediatelyNever embed the API key in client-side JavaScript bundles served to end users — the key would be visible in browser dev tools. For web apps, proxy crash reports through your own backend. For React Native and Flutter, the key is compiled into the native binary, which provides reasonable protection but is not truly secret — consider enabling HMAC request signing (enableRequestSigning: true, on by default) for additional security.
Create a single LifestreamDoctor instance at app startup. The most important options are:
| Option | Default | Description |
|---|---|---|
apiUrl | (required) | Your vault's base URL |
vaultId | (required) | The crash reports vault ID |
apiKey | (required) | Write-only API key (lsv_k_ prefix) |
environment | 'production' | Tags every report (e.g. 'staging', 'preview') |
enabled | true | Set false to disable in development |
pathPrefix | 'crash-reports' | Document path prefix |
tags | [] | Custom tags added to every report |
beforeSend | — | Filter/transform reports before upload (return null to discard) |
maxBreadcrumbs | 50 | Breadcrumb buffer size |
enableRequestSigning | true | HMAC-SHA256 request signing |
debug | false | Log SDK activity to console |
import { LifestreamDoctor } from '@lifestreamdynamics/doctor';
const doctor = new LifestreamDoctor({
apiUrl: process.env.VAULT_URL!,
vaultId: process.env.VAULT_CRASH_REPORTS_ID!,
apiKey: process.env.VAULT_CRASH_API_KEY!,
environment: process.env.NODE_ENV ?? 'production',
enabled: process.env.NODE_ENV === 'production',
pathPrefix: 'crash-reports',
tags: ['frontend', 'v2.1.0'],
debug: false,
});
export default doctor;Use a dedicated crash reports vault to keep crash data separate from your content documents. Customize pathPrefix to organize by platform — for example, 'crashes/ios' or 'crashes/web' — if you want separate folders within the same vault. The SDK auto-organizes reports by date: crash-reports/2026-03-22/typeerror-a1b2c3d4.md.
The SDK requires explicit consent before sending any crash reports, ensuring GDPR and PIPEDA compliance. Three methods manage consent:
grantConsent() — Persists consent to the storage backend and enables reportingrevokeConsent() — Clears the consent flag and immediately purges any queued reportssetConsentPreVerified() — Synchronous, in-memory flag that eliminates the async storage check — use this at startup if your app has its own consent system and the user has already opted inConsent state is persisted via the StorageBackend interface. The default MemoryStorage loses state on restart, so mobile apps should use a persistent backend.
import doctor from './doctor';
// When user accepts your consent/privacy dialog:
async function onConsentAccepted() {
await doctor.grantConsent();
}
// When user opts out:
async function onConsentRevoked() {
await doctor.revokeConsent(); // also clears any queued reports
}
// If your app manages consent externally (e.g. a cookie consent system),
// call this synchronously at startup to avoid missing early crashes:
if (userHasAlreadyConsented()) {
doctor.setConsentPreVerified();
}The consent system is intentionally simple: call grantConsent() when the user opts in, revokeConsent() when they opt out. The SDK handles the rest — no reports are sent, queued, or retained without active consent. If your app already has a consent management system, use setConsentPreVerified() at startup after verifying consent externally.
captureException() is the core method. It builds a CrashReport, formats it as Markdown with YAML frontmatter, and uploads it to your vault. If the upload fails (offline, network error), the report is queued for later retry via flushQueue().
You can also use captureMessage() for non-exception events like warnings or informational notes.
Extras parameter options:
| Field | Type | Description |
|---|---|---|
severity | fatal | error | warning | info | Severity level (default: error) |
extra | Object / Map | Arbitrary key-value context (max 50KB) |
tags | string[] / List | Per-report tags (merged with global tags) |
componentStack | string | React component tree (auto-set by error boundary) |
import doctor from './doctor';
// Capture an exception with context
async function handleCheckout(orderId: string) {
try {
await processPayment(orderId);
} catch (error) {
await doctor.captureException(error as Error, {
severity: 'error',
extra: { orderId, route: '/checkout' },
tags: ['checkout', 'payments'],
});
// Show user-facing error UI...
}
}
// Capture a non-exception message
await doctor.captureMessage(
'Payment retry succeeded after 2 attempts',
'warning',
{ orderId: 'ord_123', attempts: 2 },
);Auto-generated tags: Every report automatically receives severity:<level>, env:<environment>, and the lowercased error name (e.g. typeerror, stateerror) as tags. These are merged with any global tags from initialization and per-capture tags, making all reports searchable by severity, environment, and error type in the vault.
Instead of wrapping every function in try/catch, set up automatic error capture at the app level. The approach differs by platform:
FlutterError.onError + PlatformDispatcher.instance.onErrorimport doctor from './doctor';
// Create the error boundary component
const DoctorErrorBoundary = doctor.createErrorBoundary();
// Wrap your app tree
function App() {
return (
<DoctorErrorBoundary>
<Router>
<AppRoutes />
</Router>
</DoctorErrorBoundary>
);
}
// The error boundary automatically:
// - Captures the error with severity 'fatal'
// - Includes the React component stack
// - Renders a fallback UI (or customize with a fallback prop)For React Native apps, installReactNativeHandlers() is the recommended one-call setup. It wires both the global JS error handler (ErrorUtils.setGlobalHandler) and the unhandled promise rejection tracker, and sets up device context collection automatically. The returned cleanup function unregisters the handlers on app unmount.
When a crash report fails to upload (network error, server unavailable), the SDK serializes it to the storage backend for later retry. Call flushQueue() to retry all pending reports — for example on app startup, on network reconnection, or periodically.
The queue holds up to 50 entries. Reports that fail 5 attempts are moved to dead letter status and discarded. The FlushResult object tells you how many reports were sent, failed, or dead-lettered.
import doctor from './doctor';
// Flush on app startup
const result = await doctor.flushQueue();
console.log(`Sent: ${result.sent}, Failed: ${result.failed}`);
// Flush on network reconnection (web)
window.addEventListener('online', async () => {
await doctor.flushQueue();
});
// For React Native, use @react-native-community/netinfo:
// NetInfo.addEventListener(state => {
// if (state.isConnected) doctor.flushQueue();
// });The default MemoryStorage backend loses queued reports when the process exits. For mobile apps, use AsyncStorageBackend (React Native) or implement the StorageBackend interface with shared_preferences (Flutter) to persist the queue across app restarts.
Doctor uploads crash reports to a predictable path structure (crash-reports/YYYY-MM-DD/...), which makes them perfect targets for document operation hooks. Hooks run server-side whenever a document is created, so you can automatically add organizational tags to every incoming crash report without any client-side changes.
Use the document_operation action type with operation: 'add_tag' to tag documents automatically. Combine this with a triggerFilter using pathPattern (glob syntax) to scope hooks to your crash reports folder.
See the Automate Your Vault guide for full hooks documentation.
import { LifestreamVaultClient } from '@lifestreamdynamics/vault-sdk';
const { client } = await LifestreamVaultClient.login(
'https://vault.lifestreamdynamics.com',
'you@example.com',
'your-password',
);
const vaultId = 'your-crash-vault-id';
// Hook 1: Tag all crash reports as "needs-triage"
await client.hooks.create(vaultId, {
name: 'Crash Report Triage',
triggerEvent: 'document.created',
triggerFilter: { pathPattern: 'crash-reports/**' },
actionType: 'document_operation',
actionConfig: { operation: 'add_tag', tag: 'needs-triage' },
});
// Hook 2: Tag all crash reports as "crash-report"
await client.hooks.create(vaultId, {
name: 'Crash Report Label',
triggerEvent: 'document.created',
triggerFilter: { pathPattern: 'crash-reports/**' },
actionType: 'document_operation',
actionConfig: { operation: 'add_tag', tag: 'crash-report' },
});Document operation hooks require a Pro or Business subscription. On the Free tier, crash reports are still uploaded and searchable, but server-side automatic tagging is not available. You can add tags at capture time using the SDK's tags option instead.
Because Doctor writes structured YAML frontmatter into every crash report, the vault's kanban board and calendar timeline views work out of the box with no additional configuration.
The kanban board groups documents by any frontmatter key. For a crash reports vault, the most useful groupings are:
| Group by | Columns created | Use case |
|---|---|---|
severity | fatal | error | warning | info | Triage board — prioritize by impact |
device | ios | android | web | Platform view — isolate platform-specific issues |
environment | production | staging | development | Environment view — separate real crashes from test noise |
appVersion | 2.0.0 | 2.1.0 | 2.2.0 | Version view — track regressions per release |
Drag a crash report between columns to update its frontmatter — for example, grouping by severity and moving a triaged report to a custom status column updates the document. Combined with the document operation hooks from the previous section, the kanban board becomes a lightweight crash triage workflow.
Every crash report includes a date field in its frontmatter, so reports appear on the activity heatmap as document creation events. This gives you:
Click any day to see all crash reports filed on that date. The timeline view shows the time, severity, and error name for each report.
After deploying v2.1.0 on Monday, you open the calendar view on Wednesday and notice the heatmap shows an unusually hot Tuesday. You click Tuesday's cell and see 47 crash reports — mostly TypeError with device: android.
You switch to the kanban board and group by severity. The fatal column has 12 reports, all tagged needs-triage by the document operation hook. You open one and find a null reference in the checkout flow — the stack trace points to CheckoutScreen.tsx:142 and the breadcrumbs show the user navigated from the cart page.
Within minutes, you know exactly what broke, when it broke, and which platform is affected — all from your vault's built-in views.
See the Manage Projects with Calendar guide for more on the activity heatmap and calendar features.
The kanban board and calendar timeline are available in the web UI for any vault — no special configuration needed. The frontmatter fields Doctor generates (severity, device, os, environment, appVersion) are standard YAML keys that the kanban grouping system reads automatically.
You now have crash reporting flowing into your vault with automatic tagging and visual triage tools. Here are some next steps to explore:
| Resource | Link |
|---|---|
| TypeScript SDK on npm | npmjs.com/package/@lifestreamdynamics/doctor |
| Dart SDK on pub.dev | pub.dev/packages/lifestream_doctor |
| TypeScript source & issues | github.com/lifestreamdynamics/lifestream-vault |
| Dart source & issues | github.com/lifestreamdynamics/lifestream-vault-doctor-dart |