Appearance
Flutter and Dart
Two packages track events from Flutter apps, Dart servers and scripts. Both keep a crash-safe outbox, so an event is never lost to a crash or a killed app.
simpledatalogs: the pure Dart core. Runs on the Dart VM, Flutter (mobile, desktop, web) and servers. One dependency:http.simpledatalogs_flutter: optional Flutter wiring. A disk outbox in the app support folder, flushes when the app is hidden, sessions, error capture and screen views.- Dart 3.5+, Flutter 3.24+.
Install
Flutter apps:
sh
flutter pub add simpledatalogs_flutterDart servers and scripts:
sh
dart pub add simpledatalogsEarly access
The packages aren't on pub.dev yet. Until they are, add them from GitHub in pubspec.yaml:
yaml
dependencies:
simpledatalogs_flutter:
git:
url: https://github.com/XKD-Labs/simpledatalogs-dart.git
path: flutter
# The Flutter package asks for the core from pub.dev; point it at GitHub too.
dependency_overrides:
simpledatalogs:
git:
url: https://github.com/XKD-Labs/simpledatalogs-dart.gitFor a Dart server, list only simpledatalogs with the same git URL under dependencies.
Quick start
dart
import 'package:flutter/material.dart';
import 'package:simpledatalogs_flutter/simpledatalogs_flutter.dart';
Future<void> main() async {
// The opt-out first, then identity and default properties, then the key.
SimpleDataLogs.setEnabled(userAllowsUsageStatistics);
SimpleDataLogs.setDeviceId(anonymousDeviceId);
SimpleDataLogs.setDefaultProperties({'app_version': '1.1.0'});
await SimpleDataLogsFlutter.configure(ingestKey: 'sdl_pub_…');
runApp(const MyApp());
}Then, anywhere. It returns at once and never throws:
dart
SimpleDataLogs.track('level_complete', {'level': 3, 'duration_s': 42});Use a public key (sdl_pub_…) in apps. The Flutter package logs a warning if it sees a secret key.
Autocapture
One argument sends launches, sessions and errors without any track call. It is off until you pass it.
dart
await SimpleDataLogsFlutter.configure(
ingestKey: 'sdl_pub_…',
autocapture: AutocaptureOptions.all,
);| Option | What you get |
|---|---|
appLaunches | app_launched once per configure, with is_first_launch |
sessions | session_start, a session_id on every event, and session_end (duration_s) once the app has been away 30 minutes; a relaunch within 30 minutes resumes the session |
errors | Uncaught Flutter and platform errors as app_error (source = flutter or platform), after your own handlers |
AutocaptureOptions.all is all three; pick with AutocaptureOptions(sessions: true, errors: true).
Screen views: add the observer to your app. Named page routes send
screen_viewon push, replace and pop; passscreenNameExtractorto name them your own way.dartMaterialApp( navigatorObservers: [SimpleDataLogsNavigatorObserver()], home: const HomePage(), )By hand:
SimpleDataLogs.trackScreen('Settings')andSimpleDataLogs.trackError(error, stackTrace).The opt-out applies: with
setEnabled(false), nothing is captured.
Active and background
With SimpleDataLogsFlutter.configure, every event tracked now says whether someone was using the app, so the Sessions tab can tell active sessions from passive ones:
app_stateisactivewhile the app is on screen, elsebackground.engaged_msis the time on screen since the previous event.- Mark background work yourself with
SimpleDataLogs.track('sync_finished', {'app_state': 'background'}); your value wins. reportsActivity: falsesends neither. Plain Dart never does.
API
| Call | What it does |
|---|---|
SimpleDataLogsFlutter.configure(ingestKey:, endpoint:, autocapture:, httpClient:) | Flutter: starts the SDK with a disk outbox and lifecycle flushes. Later calls are ignored |
SimpleDataLogs.configure(ingestKey:, endpoint:, autocapture:, storage:, batchInterval:, httpClient:) | Pure Dart: starts the SDK once. storage defaults to memory; batchInterval to 10 s in release and AOT builds, 5 s otherwise (debug, profile, dart run); httpClient swaps the HTTP client (proxies, tests) |
track(eventName, [properties]) | Records one event. event_id and ts are stamped now, when the event happens |
trackScreen(screenName, [properties]) | Records screen_view with a screen prop |
trackError(error, [stackTrace, properties]) | Records app_error (exception_type, message, stack), at most 5 per message and 50 per run |
setDeviceId(id) | Sets, or clears with null, the device_id on every later event |
setDefaultProperties(map) | Replaces the properties merged into every event; the event's own properties win |
setEnabled(bool) | The opt-out. false ignores new events and deletes every unsent event; true collects again |
flush() | Sends everything now; completes when that attempt ends. Undelivered events stay in the outbox |
close() | Stops timers so a script can exit |
Property values are strings, integers, finite doubles or booleans. Anything else (null, NaN, lists, maps) is dropped with a log line.
For several keys in one process, create SimpleDataLogsClient instances; they take the same arguments.
Dart servers and scripts
Pass a disk outbox and a shorter batch interval:
dart
import 'dart:io';
import 'package:simpledatalogs/io.dart';
import 'package:simpledatalogs/simpledatalogs.dart';
Future<void> main() async {
SimpleDataLogs.configure(
ingestKey: Platform.environment['SIMPLEDATALOGS_KEY']!,
storage: FileOutboxStorage(Directory('/var/lib/my-app/simpledatalogs')),
batchInterval: const Duration(seconds: 5),
);
SimpleDataLogs.track('report_generated', {'rows': 120});
// Scripts send before exiting; anything undelivered stays on disk for the next run.
await SimpleDataLogs.flush();
await SimpleDataLogs.close();
}Servers may use a secret key (sdl_sec_…). In pure Dart, sessions follow client.appBecameActive() and client.appWentToBackground(), which the Flutter package calls for you.
The opt-out
Call setEnabled before configure. A user who opted out then never sends a byte, and whatever an earlier run left on disk is deleted instead of sent.
The SDK doesn't persist the choice. Your app's setting is the source of truth, so pass it on every launch.
Privacy
- No identifiers are collected for you.
device_idis only what you set: an anonymous id you keep, never an email or advertising id. - Events are kept in the app's support folder (Flutter) or the folder you choose (Dart), never in shared storage.
- The opt-out deletes everything not yet sent.
How it behaves
Batching
Events go out in batches of 50, or 10 seconds after the first event of a partial batch (5 seconds in debug and profile builds). Every request stays under 256 KB, and bodies of 1 KB or more are gzipped (not on the web).
A crash-safe outbox
- Each event is appended to a journal as it is tracked.
- Batches are sealed into files and deleted only after a
202. - The next run resends anything left over, including a journal cut short by a crash.
- A batch that fails to read three times in a row is renamed
*.unreadableand skipped. - Flutter web keeps the outbox in memory.
Errors and offline
| Answer | What the SDK does |
|---|---|
202 | Deletes the batch file |
401 | Pauses sending with one error line; nothing is lost. Re-probes after 1 minute, then every 10 minutes, and on the next run |
413 | Splits the batch in half and resends. A single event still too large is dropped, with a log line |
415 | Sets the batch aside as *.unsupported |
| Anything else, a timeout, offline | Backs off with full jitter from 1 second to 5 minutes, honoring Retry-After |
Caps
- The outbox keeps at most 10,000 events, 10 MB or 500 batch files. Past that, the oldest batches are dropped, with a log line.
- If more than 5,000 events wait to be written, new ones are skipped, with one log line.
Nothing throws into your app.
Troubleshooting
The SDK logs through dart:developer under the name simpledatalogs. Set SdkLog.handler to route its lines into your own logger.
For local development, point it at a local ingest:
dart
await SimpleDataLogsFlutter.configure(ingestKey: 'sdl_pub_…', endpoint: Uri.parse('http://localhost:4001'));Next steps
- Flutter quickstart: the shortest path.
- Core concepts: one deployment per build, such as
androidandios. - Privacy for senders: device ids, not emails.