---
url: https://simpledatalogs.com/docs/sdks/flutter.md
description: >-
  The simpledatalogs packages for Flutter apps and Dart servers; install,
  configure, track, screen views, errors, the opt-out, the crash-safe outbox and
  caps.
---

# 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_flutter
```

Dart servers and scripts:

```sh
dart pub add simpledatalogs
```

::: info Early 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.git
```

For 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_view` on push, replace and pop; pass `screenNameExtractor` to name them your own way.

  ```dart
  MaterialApp(
    navigatorObservers: [SimpleDataLogsNavigatorObserver()],
    home: const HomePage(),
  )
  ```

* **By hand:** `SimpleDataLogs.trackScreen('Settings')` and `SimpleDataLogs.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](/guide/web-analytics) can tell active sessions from passive ones:

* `app_state` is `active` while the app is on screen, else `background`.
* `engaged_ms` is the time on screen since the previous event.
* Mark background work yourself with `SimpleDataLogs.track('sync_finished', {'app_state': 'background'})`; your value wins.
* `reportsActivity: false` sends 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_id` is 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 `*.unreadable` and 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](/quickstarts/flutter): the shortest path.
* [Core concepts](/guide/concepts#deployments): one deployment per build, such as `android` and `ios`.
* [Privacy for senders](/reference/privacy): device ids, not emails.
