lifestream_doctor 1.0.3
lifestream_doctor: ^1.0.3 copied to clipboard
Crash reporting SDK for Lifestream Vault — captures exceptions and uploads reports as searchable Markdown documents.
lifestream_doctor #
Crash reporting SDK for Lifestream Vault — captures exceptions and uploads them as searchable, taggable Markdown documents via the Vault API.
Getting started? Follow the Crash Reporting with Doctor SDKs user guide for a step-by-step walkthrough covering setup, consent, Flutter error hooks, and crash triage with the kanban board and calendar timeline.
Dart port of @lifestreamdynamics/doctor — produces format-compatible reports and uses the same HMAC-SHA256 signing protocol.
Table of Contents #
- Installation
- Quick Start
- Platform Compatibility
- API Reference
- DoctorOptions
- Flutter Integration
- Document Format
- Consent Management
- beforeSend Filter
- Offline Queue
- Custom Context
- Cross-Platform Signing Compatibility
- License
Installation #
Add to your pubspec.yaml:
dependencies:
lifestream_doctor: ^1.0.0
Or install from git:
dependencies:
lifestream_doctor:
git:
url: https://github.com/lifestreamdynamics/lifestream-vault-doctor-dart.git
Then run:
dart pub get
Quick Start #
import 'package:lifestream_doctor/lifestream_doctor.dart';
final doctor = LifestreamDoctor(
apiUrl: 'https://vault.example.com',
vaultId: 'your-vault-id',
apiKey: 'lsv_k_your_api_key',
environment: 'production',
);
// Crash reports are only uploaded after the user grants consent.
await doctor.grantConsent();
// Capture an exception manually.
try {
await riskyOperation();
} catch (e, stack) {
await doctor.captureException(e, stackTrace: stack, severity: Severity.error);
}
Each captured exception becomes a Markdown document inside your vault, searchable by error name, severity, tag, date, or any text in the stack trace.
Platform Compatibility #
This is a pure Dart package with no Flutter dependency. It works in:
- Flutter apps (iOS, Android, macOS, Linux, Windows)
- Dart CLI tools and scripts
- Dart server applications
The package depends only on package:http, package:crypto, and package:uuid — all pure Dart.
The built-in device context adapter (getDartIoDeviceContext) uses dart:io and is available everywhere except Dart web. For web targets, implement a custom DeviceContextProvider.
API Reference #
LifestreamDoctor #
import 'package:lifestream_doctor/lifestream_doctor.dart';
final doctor = LifestreamDoctor(
apiUrl: 'https://vault.example.com',
vaultId: 'your-vault-id',
apiKey: 'lsv_k_your_api_key',
);
The main SDK class. Manages consent state, breadcrumb history, the offline queue, and report upload. A new session ID is generated on construction and included in every report produced by this instance.
Consent Methods #
grantConsent()
Future<void> grantConsent()
Marks consent as granted in the configured storage backend and enables report uploads.
revokeConsent()
Future<void> revokeConsent()
Revokes consent and clears the pending offline queue. Subsequent calls to captureException and captureMessage silently no-op until consent is re-granted.
isConsentGranted()
Future<bool> isConsentGranted()
Returns true if consent is currently active.
setConsentPreVerified()
void setConsentPreVerified()
Sets an in-memory flag that bypasses the async storage read in captureException. This eliminates the race window where an exception thrown immediately after grantConsent() could be dropped because the storage write has not yet completed.
await doctor.grantConsent();
doctor.setConsentPreVerified();
// Exceptions captured immediately after this point are guaranteed to be processed
captureException #
Future<void> captureException(
Object error, {
StackTrace? stackTrace,
Severity severity = Severity.error,
Map<String, Object?>? extra,
String? componentStack,
List<String>? tags,
})
Builds a crash report from the error and current breadcrumb buffer, runs it through beforeSend (if configured), and uploads it to the vault. If the upload fails, the report is placed in the offline queue for later retry via flushQueue().
Duplicate errors (same error type and message) are suppressed within the rateLimitWindowMs window to prevent report storms.
try {
await placeOrder();
} catch (e, stack) {
await doctor.captureException(
e,
stackTrace: stack,
severity: Severity.fatal,
tags: ['checkout', 'payment'],
extra: {'orderId': 'ord_123', 'userId': 'usr_456'},
);
}
captureMessage #
Future<void> captureMessage(
String message, {
Severity severity = Severity.info,
Map<String, Object?>? extra,
})
Captures a plain message (not an exception) as a crash report. Useful for logging degraded states or manual checkpoints.
await doctor.captureMessage(
'Payment gateway returned unexpected status code 202',
severity: Severity.warning,
extra: {'gatewayResponse': rawBody},
);
addBreadcrumb #
void addBreadcrumb(Breadcrumb crumb)
Adds an event to the breadcrumb buffer. The buffer holds the most recent maxBreadcrumbs entries (default 50); older entries are evicted automatically. Timestamps are auto-set by the buffer if missing.
doctor.addBreadcrumb(Breadcrumb(
timestamp: DateTime.now().toUtc().toIso8601String(),
type: 'navigation',
message: 'Navigated to /checkout',
));
doctor.addBreadcrumb(Breadcrumb(
timestamp: DateTime.now().toUtc().toIso8601String(),
type: 'http',
message: 'POST /api/v1/orders',
data: {'statusCode': 500, 'durationMs': 342},
));
setDeviceContextProvider #
void setDeviceContextProvider(DeviceContextProvider provider)
Registers a function that returns device and runtime context. Called at capture time (not construction), so it always reflects current state. Use the built-in adapter or provide your own:
// Built-in dart:io adapter
doctor.setDeviceContextProvider(getDartIoDeviceContext);
// Custom provider with app-specific context
doctor.setDeviceContextProvider(() => DeviceContext(
platform: Platform.operatingSystem,
osVersion: Platform.operatingSystemVersion,
appVersion: '2.1.0',
timezone: DateTime.now().timeZoneName,
locale: Platform.localeName,
extras: {'memoryMB': ProcessInfo.currentRss ~/ 1000000},
));
flushQueue #
Future<FlushResult> flushQueue()
Attempts to upload all reports in the offline queue. Returns a summary:
final result = await doctor.flushQueue();
// result.sent, result.failed, result.deadLettered
Call flushQueue() when the device comes back online or on app resume.
DoctorOptions #
| Parameter | Type | Default | Description |
|---|---|---|---|
apiUrl |
String |
required | Base URL of your Vault instance |
vaultId |
String |
required | UUID of the target vault |
apiKey |
String |
required | API key with write scope (lsv_k_ prefix) |
environment |
String |
'production' |
Environment tag in every report |
enabled |
bool |
true |
Master switch. When false, all capture calls no-op |
maxBreadcrumbs |
int |
50 |
Maximum breadcrumb buffer size |
rateLimitWindowMs |
int |
60000 |
Suppression window (ms) for duplicate errors |
pathPrefix |
String |
'crash-reports' |
Document path prefix |
tags |
List<String>? |
[] |
Tags attached to every report |
beforeSend |
CrashReport? Function(CrashReport)? |
null |
Filter/transform reports before upload |
storage |
StorageBackend? |
MemoryStorage |
Persistence backend for queue and consent |
enableRequestSigning |
bool |
true |
Sign uploads with HMAC-SHA256 |
signRequest |
CustomSignRequest? |
null |
Custom signing function |
debug |
bool |
false |
Enable debug logging via dart:developer |
httpClient |
http.Client? |
null |
Custom HTTP client (useful for testing) |
Disabled instances #
When enabled: false, credential fields are not validated:
final doctor = LifestreamDoctor(
apiUrl: '',
vaultId: '',
apiKey: '',
enabled: false, // All capture calls return immediately
);
Flutter Integration #
This package is pure Dart by design. Flutter-specific integration requires a few lines of glue code in your app.
Error Hooks #
Install global error handlers in your main.dart:
import 'package:flutter/foundation.dart';
import 'dart:ui';
void installFlutterErrorHandlers(LifestreamDoctor doctor) {
FlutterError.onError = (details) {
doctor.captureException(
details.exception,
stackTrace: details.stack,
severity: Severity.fatal,
extra: {'library': details.library},
);
};
PlatformDispatcher.instance.onError = (error, stack) {
doctor.captureException(
error,
stackTrace: stack,
severity: Severity.fatal,
);
return true;
};
}
Hive Storage Backend #
Persist the offline queue and consent state across app restarts:
import 'package:hive/hive.dart';
class HiveStorageBackend implements StorageBackend {
final Box<String> _box;
HiveStorageBackend(this._box);
@override
Future<String?> getItem(String key) async => _box.get(key);
@override
Future<void> setItem(String key, String value) async =>
_box.put(key, value);
@override
Future<void> removeItem(String key) async => _box.delete(key);
}
Riverpod Provider #
import 'package:flutter_riverpod/flutter_riverpod.dart';
final doctorProvider = Provider<LifestreamDoctor>((ref) {
final doctor = LifestreamDoctor(
apiUrl: Environment.apiBaseUrl,
vaultId: 'your-vault-id',
apiKey: 'lsv_k_your_api_key',
storage: HiveStorageBackend(Hive.box<String>('doctor')),
);
doctor.setDeviceContextProvider(getDartIoDeviceContext);
return doctor;
});
AppInitializer Integration #
Future<void> initializeApp() async {
// ... other init steps ...
final doctor = LifestreamDoctor(
apiUrl: Environment.apiBaseUrl,
vaultId: 'your-vault-id',
apiKey: 'lsv_k_your_api_key',
storage: HiveStorageBackend(await Hive.openBox<String>('doctor')),
);
doctor.setDeviceContextProvider(getDartIoDeviceContext);
// Grant consent (call after user agrees in your UI)
await doctor.grantConsent();
doctor.setConsentPreVerified();
// Install Flutter error handlers
installFlutterErrorHandlers(doctor);
// Flush any reports queued from previous sessions
await doctor.flushQueue();
}
Flush on Connectivity Change #
import 'package:connectivity_plus/connectivity_plus.dart';
Connectivity().onConnectivityChanged.listen((result) {
if (result != ConnectivityResult.none) {
doctor.flushQueue();
}
});
Document Format #
Each crash report is stored as a Markdown document with YAML frontmatter. The path follows this pattern:
{pathPrefix}/{YYYY-MM-DD}/{errorname-lowercase}-{first8charsOfId}.md
For example: crash-reports/2026-03-13/stateerror-a3f2c1b0.md
The document format is identical to the TypeScript SDK, ensuring cross-platform compatibility within the same vault.
Example Document #
---
title: "[ERROR] StateError: Bad state: No element"
tags:
- severity:error
- env:production
- stateerror
date: 2026-03-13T14:23:01.482Z
severity: error
device: ios
os: Version 17.4 (Build 21E219)
appVersion: 2.1.0
sessionId: f47ac10b-58cc-4372-a567-0e02b2c3d479
environment: production
---
## Stack Trace
\```
Bad state: No element
#0 List.first (dart:core/list.dart:101:5)
#1 CheckoutScreen._getSelectedItem (checkout_screen.dart:142:18)
#2 CheckoutScreen._handlePlaceOrder (checkout_screen.dart:87:22)
\```
## Breadcrumbs
| Time | Type | Message |
|------|------|---------|
| 2026-03-13T14:22:58.100Z | navigation | Navigated to /checkout |
| 2026-03-13T14:23:00.340Z | http | POST /api/v1/orders |
| 2026-03-13T14:23:01.100Z | user | Tapped "Place Order" button |
## Device Context
- **platform**: ios
- **osVersion**: Version 17.4 (Build 21E219)
- **locale**: en-CA
## Additional Context
\```json
{
"orderId": "ord_missing",
"cartItems": 3
}
\```
Consent Management #
Crash reporting is gated on explicit user consent. captureException and captureMessage are silent no-ops until grantConsent() is called. This design satisfies GDPR Article 7 and PIPEDA Principle 3.
Consent state is persisted in the configured StorageBackend so it survives app restarts.
// Show your consent UI, then:
Future<void> onUserAcceptsReporting() async {
await doctor.grantConsent();
}
Future<void> onUserDeclinesReporting() async {
await doctor.revokeConsent(); // Queue cleared, captures suppressed
}
// Check on startup to restore UI state
final hasConsent = await doctor.isConsentGranted();
if (!hasConsent) {
showConsentBanner();
}
beforeSend Filter #
Register a beforeSend callback to inspect, transform, or discard reports before upload.
Redacting PII #
final doctor = LifestreamDoctor(
// ...
beforeSend: (report) {
if (report.extra?['userEmail'] != null) {
return report.copyWith(
extra: {...report.extra!, 'userEmail': '[redacted]'},
);
}
return report;
},
);
Discarding Reports #
final doctor = LifestreamDoctor(
// ...
beforeSend: (report) {
// Don't report known benign errors
if (report.errorMessage.contains('ResizeObserver')) return null;
return report;
},
);
beforeSend is called synchronously. Avoid expensive work inside it.
Offline Queue #
When a report upload fails, the report is placed in the offline queue rather than being dropped.
Queue Behaviour #
- Maximum queue size: 50 entries. Oldest evicted when full.
- Maximum retry attempts: 5. After 5 failures, the entry is dead-lettered and removed.
- The queue is NOT flushed automatically. Call
flushQueue()to trigger.
Custom StorageBackend #
Implement StorageBackend to adapt to any storage mechanism:
class SharedPrefsStorageBackend implements StorageBackend {
final SharedPreferences _prefs;
SharedPrefsStorageBackend(this._prefs);
@override
Future<String?> getItem(String key) async => _prefs.getString(key);
@override
Future<void> setItem(String key, String value) async =>
_prefs.setString(key, value);
@override
Future<void> removeItem(String key) async => _prefs.remove(key);
}
Custom Context #
Add arbitrary structured data to individual reports:
await doctor.captureException(error, stackTrace: stack, extra: {
'userId': currentUser.id,
'planTier': subscription.tier,
'requestId': response.headers['x-request-id'],
});
Extra context payloads over 50 KB are replaced with an error marker. Circular references are handled gracefully.
Add custom tags to every report via the constructor:
final doctor = LifestreamDoctor(
// ...
tags: ['flutter', 'mobile', 'version:2.1.0'],
);
Cross-Platform Signing Compatibility #
This Dart SDK uses the same HMAC-SHA256 signing protocol as the TypeScript @lifestreamdynamics/doctor package. Signatures produced by either SDK are verified by the same server-side logic. The canonical payload format is:
METHOD\nPATH\nTIMESTAMP\nNONCE\nBODY_SHA256
The Dart implementation uses package:crypto (synchronous), while the TypeScript version uses Web Crypto API (async). The output is identical for the same inputs.
License #
MIT — see LICENSE for details.