Flutter SDK
Add BugsPulse to your Flutter app to capture crashes, replay sessions, monitor network requests, and track custom events — with no video recording and no PII.
Installation
Add bugspulse and its peer dependency to your pubspec.yaml:
dependencies:
bugspulse: ^0.1.0
connectivity_plus: ^6.0.0 # required peer dependencyThen fetch packages:
flutter pub getInitialize
Call BugsPulse.init() as early as possible — before runApp(). The SDK is a no-op if it is called a second time, so it is safe to call in hot-restart scenarios.
import 'package:bugspulse/bugspulse.dart';
import 'package:flutter/widgets.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await BugsPulse.init(BugsPulseConfig(
apiKey: 'pr_your_key_here',
environment: 'production',
appVersion: '2.4.1',
appBuildNumber: '241',
));
runApp(const MyApp());
}Route tracking
Pass BugsPulse.navigatorObserver to your MaterialApp (or CupertinoApp). The observer emits a navigate event on every route push/pop so you can see the full screen path a user followed before a crash.
MaterialApp(
navigatorObservers: [BugsPulse.navigatorObserver],
home: const HomeScreen(),
);If you use GoRouter or Auto Route, register the observer at the router level:
// GoRouter example
final _router = GoRouter(
observers: [BugsPulse.navigatorObserver],
routes: [...],
);Network monitoring
Add a Dio interceptor to capture every HTTP request and response. The SDK automatically scrubs sensitive query parameters (anything matching authorization, password, token, secret, card, or cvv) before storing the URL.
import 'package:dio/dio.dart';
final dio = Dio();
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
options.extra['_bp_start'] = DateTime.now().millisecondsSinceEpoch;
handler.next(options);
},
onResponse: (response, handler) {
final start = response.requestOptions.extra['_bp_start'] as int? ?? 0;
BugsPulse.logNetworkRequest(
method: response.requestOptions.method,
url: response.requestOptions.uri.toString(),
statusCode: response.statusCode,
duration: DateTime.now().millisecondsSinceEpoch - start,
);
handler.next(response);
},
onError: (error, handler) {
final start = error.requestOptions.extra['_bp_start'] as int? ?? 0;
BugsPulse.logNetworkRequest(
method: error.requestOptions.method,
url: error.requestOptions.uri.toString(),
statusCode: error.response?.statusCode,
duration: DateTime.now().millisecondsSinceEpoch - start,
error: error.message,
);
handler.next(error);
},
),
);BugsPulse.logNetworkRequest() — the method accepts any HTTP library. The call is a no-op when the SDK is not initialized.Crash reporting
Automatic (zero config)
When captureCrashes: true (the default), the SDK hooks into FlutterError.onError and PlatformDispatcher.onError to capture all unhandled Flutter and platform errors. Fatal crashes set the session status to crashed in the dashboard.
Handled exceptions
Report caught errors that you want to track without re-throwing:
try {
await riskyOperation();
} catch (error, stackTrace) {
BugsPulse.captureException(error, stackTrace);
// continue normal execution
}Crash grouping
The server groups crashes by a SHA-256 hash of the first five stack frames. Each unique crash group appears once in the Crashes dashboard and its occurrence counter increments on every repeat — you won't see thousands of duplicate entries.
Custom event tracking
Track any in-app action with optional properties. Events are batched in memory and flushed every flushInterval (default 5 s):
// Simple event
BugsPulse.track('checkout_started');
// Event with properties
BugsPulse.track('item_added', {
'product_id': 'sku_123',
'quantity': 2,
'price_usd': 29.99,
});Custom events appear in the session replay timeline alongside navigation and network events.
Identifying users
Associate sessions with your own user identifier (an internal ID, not an email) so you can look up all sessions for a specific user:
// After sign-in
BugsPulse.setUser(currentUser.id); // e.g. 'usr_7f2a3b'App lifecycle integration
Call BugsPulse.endSession() when the app moves to the background. Without this, sessions stay in the active state and their duration is never recorded in analytics. Attach WidgetsBindingObserver in your root widget:
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
BugsPulse.endSession();
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [BugsPulse.navigatorObserver],
home: const HomeScreen(),
);
}
}Privacy & redaction
The SDK never captures keyboard input, clipboard content, or images. For network monitoring, URLs are scrubbed of known sensitive query parameters automatically. To redact additional patterns, set redactedFields:
BugsPulseConfig(
apiKey: 'pr_your_key',
redactedFields: const ['user_token', 'promo_code', 'ssn'],
)Any query parameter whose key contains one of those strings (case-insensitive) is replaced with [redacted] before the URL is sent to the server.
Session sampling
To reduce session volume on high-traffic apps, set sessionSamplingRate to a value between 0.0 and 1.0. For example, 0.1 captures 10% of sessions:
BugsPulseConfig(
apiKey: 'pr_your_key',
sessionSamplingRate: 0.1, // capture 10 % of sessions
)When a session is sampled out, all SDK methods become no-ops for that app launch. The navigator observer still returns a valid (but inert) object so you never need to null-check it.
Configuration reference
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | String | required | Project API key from the dashboard. |
| apiUrl | String | https://api.bugspulse.com | Override the ingest endpoint. Only needed for local development. |
| environment | String | 'production' | Tag sessions by environment (production, staging, …). |
| appVersion | String? | null | App version string (e.g. '2.4.1'). Shown on crash groups. |
| appBuildNumber | String | '0' | Build/bundle number for filtering crashes by build. |
| captureCrashes | bool | true | Install FlutterError and PlatformDispatcher handlers. |
| captureNetworkRequests | bool | true | Reserved for future auto-capture support. |
| captureTouches | bool | true | Include touch coordinates in event payloads. |
| sessionSamplingRate | double | 1.0 | Fraction of sessions to capture (0.0–1.0). |
| flushInterval | Duration | 5 seconds | How often the event queue is sent to the server. |
| maxOfflineQueueSize | int | 1000 | Maximum events held in memory when offline. |
| redactedFields | List<String> | [] | Additional query-parameter keys to redact from URLs. |
API reference
| Method | Description |
|---|---|
| BugsPulse.init(config) | Initialize the SDK. Must be called before runApp(). Idempotent. |
| BugsPulse.track(name, [props]) | Emit a named custom event with optional string/number properties. |
| BugsPulse.setUser(userId) | Attach an opaque user identifier to the current session. |
| BugsPulse.captureException(error, stackTrace) | Report a handled exception without re-throwing. |
| BugsPulse.logNetworkRequest({…}) | Manually record an HTTP request (use with non-Dio clients). |
| BugsPulse.endSession() | Flush the queue, send session end, and reset all state. |
| BugsPulse.navigatorObserver | NavigatorObserver to pass to MaterialApp / GoRouter. |
Troubleshooting
Sessions appear but crashes are missing
Make sure WidgetsFlutterBinding.ensureInitialized() is called before BugsPulse.init(). The crash handlers require the binding to be active.
Session duration shows 0 or is missing
Call BugsPulse.endSession() when the app is paused (see App lifecycle integration above). Sessions without an end event stay in the active state and their duration is not calculated.
Network requests not appearing
The Dio interceptor must be added before any requests are made. If you create a Dio singleton, add the interceptor in its initializer. Also confirm BugsPulse.init() has resolved before the first request fires.
401 errors on ingest
Your API key is tied to a specific project. Make sure you copied the key from the correct project in the dashboard. Keys start with pr_.