TILLPULSE · SDK · FLUTTER

tillpulse

PREVIEW

Flutter plugin for Android, iOS, macOS, Windows and Linux: error and native crash capture, navigation and network breadcrumbs, performance transactions, and friction signals, delivered through an offline queue. The API is static — every call is TillPulse.something(…).

01Setup

Install

bash
flutter pub add tillpulse

Or add tillpulse: ^0.1.0 under dependencies in pubspec.yaml. Requires Dart 3.6 and Flutter 3.27 or newer.

02Init

Initialise

dart
import 'package:flutter/widgets.dart';
import 'package:tillpulse/tillpulse.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await TillPulse.init(TillPulseOptions(
    dsn: 'https://<key>@ingest.tilldev.dev/<project-id>',
    release: '1.4.0',
    environment: 'production',
    beforeSend: (event) async {
      if (event.exception?.value.contains('PaymentCancelled') ?? false) return null;
      return event;
    },
  ));

  TillPulse.runApp(const MyApp());
}

init hooks FlutterError.onError, PlatformDispatcher.instance.onError and the isolate error listener. TillPulse.runApp adds a guarded zone for async errors that escape the framework. If your app already runs its own zone, pass the handler instead:

dart
runZonedGuarded(() => runApp(const MyApp()), TillPulse.captureZonedError);
03Options

Options

OptionDefaultEffect
dsnrequiredProject DSN from the dashboard.
releaserequiredApp version; matches uploaded symbols to events.
environment'production'Filterable in the dashboard.
dist—Build number within a release.
beforeSend—Rewrite an event, or return null to drop it.
initialTags{}Tags on every event.
enableNetworkTrackingtrueFalse makes the HTTP client and Dio interceptor pass-throughs.
networkUrlDenylist[]URL substrings never recorded. The DSN host is always excluded.
enableFrictionDetectiontrueRage-tap and dead-zone detection.
rageTapThreshold3Taps in one spot that count as a rage tap.
deadZoneResponseMs300A tap with no visible response inside this window is a dead zone.
enablePreCrashWatchertrueSnapshots device state when memory, thermal and battery signals converge.
enableSessionTrackingtrueSession start/end markers for crash-free rates.
maxDailyDataKbunlimitedDaily cap on queued data. Past it only errors, pre-crash snapshots and session markers are kept.
disableDefaultPiiScrubbingfalseTurns off on-device PII scrubbing. Only if you scrub in beforeSend.
enableThreatDetectionfalseRoot/jailbreak and integrity checks.
enableInstalledAppsScanfalseAndroid: lists apps holding the overlay permission. Needs user consent.
flushIntervalMs30000How often the queue drains.
debugfalseLogs SDK activity to the console.
04Context

Users, tags and extras

dart
TillPulse.setUser(id: hashedUserId, segment: 'pro');
TillPulse.setTag('region', 'eu-west');
TillPulse.setTags({'flavor': 'play-store', 'ab': 'checkout-v2'});
TillPulse.setExtra('feature_flags', {'newCheckout': true});

// On sign-out:
TillPulse.clearContext();
TillPulse.resetSession();

Send a stable hashed id, not an email or phone number.

05Capture

Manual capture

dart
try {
  await processPayment();
} catch (e, st) {
  TillPulse.captureError(e, stackTrace: st);
}

TillPulse.captureMessage('Payment validation failed', severity: Severity.medium);

Severities: critical, high (the default for errors), medium, low, info (the default for messages).

08Network

Network breadcrumbs

dart
// package:http
final client = TillPulseHttpClient(denylist: ['/health']);
final res = await client.get(Uri.parse('https://api.example.com/cart'));

// dio
dio.interceptors.add(TillPulseDioInterceptor());

Each request records method, status and duration. Query strings are stripped before the URL is stored.

09Friction

Friction signals

dart
GestureDetector(
  onTapUp: (d) {
    recordTap(d.globalPosition.dx, d.globalPosition.dy, targetLabel: 'BuyButton');
    onPressed();
    recordResponse(); // something visible happened
  },
  child: buyButton,
)

// A form left part-way: name, fields completed, total fields.
recordFormAbandon('checkout_address', 3, 8);
10Performance

Performance transactions

dart
await TillPulse.withTransaction(
  name: 'checkout',
  op: 'flow',
  body: (tx) async {
    final span = tx.startSpan(op: 'http', description: 'POST /charge');
    await charge();
    span.finish();
  },
);

// Manual control:
final tx = TillPulse.startTransaction(name: 'image-upload', sampleRate: 0.2);
try {
  await upload();
  tx.finish();
} catch (_) {
  tx.finish(status: 'error');
  rethrow;
}

withTransaction finishes with error when the body throws. sampleRate (0–1) keeps that share of transactions. App start and frame metrics are recorded automatically.

11Native

Native crash handlers

A native crash is written to disk on the crashing run and sent as a normal event on the next launch.

PlatformMechanism
AndroidThread.setDefaultUncaughtExceptionHandler
iOSNSSetUncaughtExceptionHandler
macOSNSSetUncaughtExceptionHandler + signal handlers (SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE)
WindowsSetUnhandledExceptionFilter, with a minidump
LinuxSignal handlers; link the runner with -rdynamic for frame names

A session that ends without a clean shutdown is reported on the next launch as an unclean termination: an OS kill, out-of-memory or force-quit.

12Delivery

Offline queue and delivery

Events are scrubbed, then encrypted and persisted in shared_preferences (up to 500 items or 5 MB, oldest low-priority first, for up to 7 days) and survive restarts. Errors are sent before anything else. Failed sends back off exponentially for up to 6 attempts and honour Retry-After; 5 consecutive failures pause delivery for 1–10 minutes. Before an event is stored, any value held in your TillSecrets vault is replaced with [REDACTED:vault-secret]; see Leaked values. Your workspace's data policy also masks personal data again, by default, plus any patterns you add.

The stored queue is sealed with AES-256-GCM. On Android the data key is wrapped by an Android Keystore key that can't be exported (in StrongBox when the device has one); on iOS and macOS the key is a keychain item that never syncs or leaves the device; on Windows the queue is protected with DPAPI for the signed-in user. Encryption runs off the UI thread. An edited or swapped queue fails authentication and is discarded instead of sent, and a queue written by an older version is read once and rewritten encrypted. Linux has no system key store the plugin can rely on, so there, and anywhere the key store can't be reached, nothing is written to disk: events stay in memory until they are sent.

dart
await TillPulse.flush(); // before a known-disruptive operation
await TillPulse.close(); // tests only
13Platform

Platform setup

Android minSdk 21; iOS 12.0.

If you use R8 / ProGuard, keep the plugin and line numbers:

android/app/proguard-rules.pro
-keep class io.tillpulse.** { *; }
-keepattributes SourceFile,LineNumberTable

Upload mapping.txt and dSYMs for each release; see Source maps. The device and connectivity plugins the SDK depends on may need entries in AndroidManifest.xml and Info.plist; see each plugin's pub.dev page.

14Support

Troubleshooting

  • Events not arriving → set debug: true and watch the console.
  • Obfuscated Android frames → upload mapping.txt for that release.
  • Raw addresses in iOS or macOS native frames → upload the dSYMs of that exact build, e.g. tilldev pulse dsym upload <id> --release <v> --file build/ios/archive/Runner.xcarchive (see dSYMs). Crashes that arrived before the upload stay as reported.
  • Errors thrown in compute() isolates are captured by the isolate error listener.