dashi 1.0.1 copy "dashi: ^1.0.1" to clipboard
dashi: ^1.0.1 copied to clipboard

Pure Dart client for Umami Analytics — pageviews, custom events, identify, and a pluggable offline queue. Web included.

dashi #

pub package CI License: MIT

A pure-Dart client for Umami Analytics. Send pageviews, custom events and identify to your self-hosted Umami instance — from a CLI, a server, or any Dart app. Web included.

Building a Flutter app? Use dashi_flutter: it wraps this package and adds automatic screen tracking, device context and background flushing. Its README repeats everything below, so you only need that one.

dashi (出汁) is the stock at the base of umami — the thing that feeds the flavor.


Install #

# pubspec.yaml
dependencies:
  dashi: ^1.0.0
import 'package:dashi/dashi.dart';

The 30-second version #

final umami = UmamiClient(
  endpoint: Uri.parse('https://umami.example.com'), // your Umami server
  websiteId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', // from the Umami dashboard
  hostname: 'com.example.app',                       // identifies this app
  contextProvider: () => const UmamiContext(
    userAgent: 'MyApp/1.0 (com.example.app; Android 14)',
  ),
);

await umami.pageView(url: '/home');                  // a screen was opened
await umami.event('signup', url: '/onboarding');     // something happened
await umami.identify('user-42');                     // who the user is

That's the whole API. The rest of this README explains each line.


Step 1 — Create the client #

You create one UmamiClient and keep it around (a top-level variable, a singleton, your DI container — your call).

final umami = UmamiClient(
  // Where your Umami instance lives (no trailing /api needed).
  endpoint: Uri.parse('https://umami.example.com'),

  // The website you're tracking. Copy it from:
  // Umami dashboard → Settings → Websites → (your site) → "Website ID".
  websiteId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',

  // A label for this app/site. Umami uses it to resolve relative URLs.
  // e.g. your bundle id, or a domain like "myapp.com".
  hostname: 'com.example.app',

  // dashi never reads the device itself — YOU describe the context.
  // (More on this in "The context" below.)
  contextProvider: () => const UmamiContext(
    language: 'en-US',
    screen: '1080x2400',
    userAgent: 'MyApp/1.0 (com.example.app; Android 14)',
  ),
);

Good to know: every tracking method is fire-and-forget. It never throws and never blocks your UI — if the network is down, the event is queued (see Offline queue). You can await the calls, but you don't have to.

Step 2 — Track a page view (a screen change) #

A pageview = "the user opened a screen/route". Call pageView yourself every time a screen becomes visible:

await umami.pageView(url: '/home');

// With an optional title and the screen they came from (the "referrer"):
await umami.pageView(
  url: '/product/42',
  title: 'Cool sneakers',
  referrer: '/home',
);
  • url — the path of the screen (e.g. /home, /settings, /product/42).
  • title — optional, a human-readable name shown in Umami.
  • referrer — optional, the previous screen. Useful to see navigation flows.

On Flutter you usually don't write these by hand — dashi_flutter's DashiNavigatorObserver calls pageView for you on every navigation. This package is the manual, framework-agnostic layer.

Step 3 — Track a custom event #

A custom event = "something happened that isn't a screen change" (a click, a purchase, a signup…). It's a pageView with a name:

// Simplest form — just a name and where it happened.
await umami.event('subscribe_clicked', url: '/pricing');

// Attach structured data (anything JSON-serializable):
await umami.event(
  'purchase',
  url: '/checkout',
  data: {
    'plan': 'pro',
    'amount': 49.0,
    'currency': 'EUR',
  },
);
  • name — the event name (kept ≤ 50 chars by Umami).
  • url — the screen where it happened.
  • data — optional key/value details, browsable in Umami.

Step 4 — Identify a user #

By default Umami tracks anonymous sessions. Once you know who the user is (typically right after login), call identify with a stable id you control:

// Call this once, after the user logs in.
await umami.identify('user-42');

// You can pass extra session properties:
await umami.identify('user-42', data: {'plan': 'pro', 'role': 'admin'});

After this call, every pageview and event you send carries that id, so Umami attributes them to the user. No Riverpod, no provider, no magic — just call the method from wherever you handle login:

Future<void> onLoginSuccess(User user) async {
  await umami.identify(user.id);
}

To "log out" for analytics, create a fresh client (or just stop sending the id by not calling identify again — note Umami v3 does not merge the pre-login anonymous session with the identified one).


The context #

contextProvider is a function dashi calls on every track to snapshot the environment. It returns a UmamiContext:

const UmamiContext({
  String? language,  // BCP-47, e.g. 'en-US', 'fr-FR'
  String? screen,    // '<width>x<height>', e.g. '1080x2400'
  String? userAgent, // see below — this one matters
});

It's a function (not a value) so it can return fresh data each time (locale or orientation can change while the app runs).

The User-Agent rule (read this — it bites everyone) #

Umami silently drops requests from bots. A missing or bot-like User-Agent gets your events thrown away while still returning HTTP 200 — they just never show up in your stats.

  • On io (mobile, desktop, server): provide a descriptive, plausible UA, e.g. 'MyApp/1.0 (com.example.app; Android 14)'. Required.
  • On web: set userAgent: null. The browser sends its own UA (you're not even allowed to set it — it's a forbidden header), and that one passes.

dashi only attaches the UA header on io; on web it leaves it to the browser.

Offline queue #

If a send fails (no network, server down…), dashi keeps the event in a queue and retries later — automatically on the next successful send, or when you call flush(). Replayed events keep their original timestamp, so your stats stay accurate.

final umami = UmamiClient(
  endpoint: Uri.parse('https://umami.example.com'),
  websiteId: '...',
  hostname: 'com.example.app',
  contextProvider: () => const UmamiContext(userAgent: 'MyApp/1.0 (...)'),

  // Defaults shown — all optional:
  queue: InMemoryQueueStore(maxSize: 200), // keep up to 200 pending events
  eventTtl: const Duration(hours: 48),     // drop events older than this
  flushChunkSize: 25,                      // send them in batches of 25
);

// Force a flush now (e.g. before the app closes):
await umami.flush();

Three storage modes:

You want… Use
Default in-memory retry (lost on app restart) InMemoryQueueStore() (default)
No retry at all — drop failed events const NoopQueueStore()
Persist across restarts (file, DB, prefs…) your own UmamiQueueStore

Transports #

How dashi actually talks HTTP. The default is LuckyTransport (built on Lucky Dart / dio). If you'd rather not pull in dio, use the package:http one:

final umami = UmamiClient(
  endpoint: Uri.parse('https://umami.example.com'),
  websiteId: '...',
  hostname: 'com.example.app',
  contextProvider: () => const UmamiContext(userAgent: 'MyApp/1.0 (...)'),
  transport: HttpTransport(), // dio-free alternative
);

Debug mode (don't send anything) #

Pass enabled: false to run the whole pipeline (build the payload, log it) without any network call — handy in tests or local dev. Add a logger to see what would be sent:

final umami = UmamiClient(
  endpoint: Uri.parse('https://umami.example.com'),
  websiteId: '...',
  hostname: 'com.example.app',
  contextProvider: () => const UmamiContext(userAgent: 'MyApp/1.0 (...)'),
  enabled: false,
  logger: (record) => print('[umami] ${record.level.name}: ${record.message}'),
);

The logger also surfaces real errors (failed sends, dropped events) — since the API never throws, this callback is how you observe what's happening.

Cleaning up #

When you're done (app shutting down), dispose() does a final best-effort flush and releases resources:

await umami.dispose();

Requirements #

A self-hosted Umami instance, version 3.1.0 or newer.

0
likes
160
points
108
downloads

Documentation

API reference

Publisher

verified publisherowlnext.fr

Weekly Downloads

Pure Dart client for Umami Analytics — pageviews, custom events, identify, and a pluggable offline queue. Web included.

Repository (GitHub)
View/report issues

Topics

#analytics #umami #tracking #telemetry

License

MIT (license)

Dependencies

dio, http, lucky_dart

More

Packages that depend on dashi