Skip to content

Flutter SDK

Typed event tracking, screen and lifecycle auto-tracking, and identity for Flutter apps with pug_flutter.

Updated View as Markdown

pug_flutter captures events from Flutter apps on mobile, desktop and the web. It authenticates with your public key (pub_...), which is write-scoped (events + identify) and safe to ship in the app, and follows the same model as every Pug SDK: initialize once, track events, identify users, reset on sign-out.

Install

flutter pub add pug_flutter

View pug_flutter on pub.dev

flutter pub add pug_flutter adds it to pubspec.yaml and fetches it; to pin the version yourself, add it under dependencies instead:

pubspec.yamlyaml
dependencies:
  pug_flutter: ^0.0.5
Requirement Minimum
Dart SDK >=3.7.0 <4.0.0
Flutter >=3.29.0

Import it wherever you call the SDK:

import 'package:pug_flutter/pug_flutter.dart';

Smoke-test by adding a minimal Pug.init in main() (see Initialize) and running the app - with auto-tracking on, an app_open event appears in Live within a few seconds. (page_view events need the PugRouteObserver - see Auto-tracking.)

Working with a coding agent? Paste this instead - it points the agent at the markdown copy of this page, so it works from the reference below rather than from memory. More on the approach in Install with AI.

Prompt
Add Pug product analytics to this Flutter app.

Read these first - they are the current source of truth, and more recent than your training data:
- https://docs.pug.sh/sdks/flutter/index.md - SDK reference
- https://docs.pug.sh/reference/well-known-events/index.md - the well-known event catalog

Then read this codebase before you write anything, and work out what the app actually does: its screens and navigation graph, its main models, and the handful of things a user opens the app to complete. The events worth tracking are the moments the app exists to produce - an account created, an order placed, a workout finished - not every tap. If you cannot state the core action in one sentence, keep reading.

Then:
1. Add pug_flutter to pubspec.yaml.
2. Await Pug.init() in main(), after WidgetsFlutterBinding.ensureInitialized() and before runApp().
3. Keep the project ID and public key (pub_...) out of source - pass them with --dart-define and read them with String.fromEnvironment.
4. Register PugRouteObserver on the app's navigatorObservers, otherwise screen views never fire.
5. Add tracking for the three or four actions you identified - the ones someone would look at in a weekly review.
6. If this app has authentication, call Pug.identify() after sign-in and Pug.reset() on sign-out.

Name events from the catalog before you invent one. Flutter gets a generated named-argument method for all 127 well-known events - Pug.track.purchase(productId: ..., amount: ...) - so the compiler checks the properties for you, and the dashboards those events drive stay empty for a custom name. Reach for Pug.track.<event> first, search the catalog for every action you are about to track, and pass schema-external properties through extras. Use Pug.track(kind, props: {...}) with a custom snake_case name only when nothing in the catalog fits, and when you do, tell me which event you considered and why it did not.

Flutter consent has two states and starts granted, which is not the same as the Web SDK - check the consent section before you add a banner. Do not use any option that is not in the SDK reference above. When you are done, tell me in one sentence what you concluded this app does, then list the files you changed, the event names you chose, and the dart-defines I need to pass.

Initialize

Await Pug.init once in main(), before runApp(), with your project ID and public key - every other call needs it first. Find your project ID in the dashboard URL (/p/<projectId>), and your public key on your project Overview or under Settings > API Keys.

import 'package:pug_flutter/pug_flutter.dart';

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

  await Pug.init(
    'YOUR_PROJECT_ID',
    const PugOptions(
      apiKey: 'pub_your_public_key',
    ),
  );

  runApp(const MyApp());
}

Pug.init returns Future<void> and throws ArgumentError if projectId or apiKey is empty. A second init while running is silently ignored (a warning is logged via PugLogger); call Pug.destroy() first to reinitialize.

Option Type Default Description
apiKey String required Public key (pub_...), sent as x-api-key.
endpoint String https://api.pugs.dev API base URL: set it only when self-hosting.
batch BatchConfig maxSize 10, maxWaitMs 5000, maxQueueSize 1000 Queue size and flush timing; persisted to SharedPreferences.
session SessionConfig idleTimeout 30 min, maxDuration 24 h Session timeouts as Durations.
autoTrack bool true Emit app_open and app_close on lifecycle changes. Page views and install events have their own switches.
autoPageViews bool true Emit a navigation event on route changes: screen_view on mobile, page_view on web (needs PugRouteObserver).
autoInstallEvents bool true Emit app_install and app_update at launch. See Auto-tracking.
trackingConsent TrackingConsentConfig const TrackingConsentConfig() Consent gate: two states, granted by default, unlike the Web SDK. See Tracking consent.
dryRun bool false Log via PugLogger without sending.

Advanced overrides (logger, storage, transport, autoPropertyProvider) let you inject test doubles or custom collection.

Track events

Events are the foundation of every analytic in Pug. The first argument is the event name (the kind): custom names work without a schema, while well-known events add typed properties and richer dashboards. Names starting with pug. are reserved.

// Custom event - any name
Pug.track('button_clicked', props: {'label': 'Sign up', 'screen': 'pricing'});

// Typed well-known event - compile-time checked
Pug.track.purchase(productId: 'sku-1', amount: 99.50, currency: 'USD');

Pug.track(...) never throws - calls are wrapped in try/catch and queued in the background. The generated Pug.track.* namespace has a typed method per well-known event; pass schema-external properties via extras: {...} (on a key collision, the named argument wins). For runtime-determined names, call Pug.track(kind, props: {...}) directly.

Property values accept String (truncated to 1024 UTF-8 bytes), bool, int, finite double/num (non-finite dropped), and DateTime (UTC epoch ms). Iterable/Map are JSON-encoded to a string; null is dropped - omit the key instead.

TrackOptions (on Pug.track and every typed method):

Option Type Default Description
immediate bool false Bypass the queue and send now (falls back to the queue on transient failure).
timestampMillis int? wall clock Override the event time as epoch milliseconds.
Pug.track.purchase(
  productId: 'sku-789', amount: 99.0, currency: 'USD',
  options: const TrackOptions(immediate: true),
);

Identify and sessions

Identity turns anonymous events into actionable profiles. The pattern: events accumulate against an anonymous ID, you Pug.identify() on sign-in to merge that history into a known profile, and Pug.reset() on sign-out. Use a stable, unique external ID - your database user ID, not an email (which can change). Only the first identify after an anonymous session merges the prior activity; the same external ID always resolves to the same profile, and traits are merged (never removed) server-side.

// On sign-in
await Pug.identify('user-123', traits: {'email': 'user@example.com', 'plan': 'pro'});

// On sign-out
Pug.reset();

// New session ID, identity kept
Pug.rotate();

Pug.identify(externalId, {traits}) returns Future<void> and never throws - invalid input and transport failures are logged and the future completes normally; a call before Pug.init() is ignored with a warning. reset() is synchronous - it generates a new device ID, a new anonymous ID, and a new session, and removes the stored externalId (the profile stays in the dashboard). Drain the queue with await Pug.flush(); fully tear down with Pug.destroy().

Function Clears identity New device ID New session ID
rotate() No No Yes
reset() Yes Yes Yes

Sessions

Sessions group events within a single visit. The SDK creates a session ID on the first event, extends it on each event within the idle window, and starts a new one once the idle or max-duration timeout passes - identity is preserved across the boundary.

First eventsession ID created
Events within timeoutsame session ID
Idle past timeoutsession expires
Next eventnew session ID (identity kept)

The session is persisted to SharedPreferences and restored on cold start (re-checking the timeouts). Configure the timeouts at init with session (idleTimeout/maxDuration). For the anonymous->identified model, see Core concepts.

Auto-tracking

With auto-tracking on (the default), the SDK captures app lifecycle and installs for you, and navigation once you wire a PugRouteObserver - no manual Pug.track() calls. Every auto-tracked event also carries the standard auto-properties, and the server attaches enrichment properties to every event.

Navigation is captured when you wire a PugRouteObserver into your MaterialApp:

MaterialApp(
  navigatorObservers: [PugRouteObserver()],
  // ... rest of your config
)

On each route change the SDK emits a navigation event: screen_view (with screenName set to the route name) on iOS/Android, and page_view on web; desktop targets emit neither. The navigation event itself carries no url props - instead every event picks up the current and previous route as the $url and $referrer auto-properties, so route context is present even when autoPageViews is off. Route names come from route.settings.name, falling back to the route’s runtime type.

App lifecycle is observed automatically (no wiring): app_open when the app enters the foreground, and app_close when it is backgrounded (hidden, paused or detached) - sent with immediate: true so it arrives before the OS kills the process. A transient inactive state is not a close, since the app is still visible: an incoming call, the app switcher, a control-centre swipe.

Installs and updates (autoInstallEvents, on by default) are checked once at launch: app_install on the first launch the SDK sees on a device, and app_update, carrying previousVersion, on the first launch after the app version changes. The check compares the $appVersion auto-property with the version stored at the last launch, so a custom PugAutoPropertyProvider that supplies no $appVersion turns it off. An app that already used Pug doesn’t report its existing users as installs, but Pug.destroy() clears the stored version along with the rest of Pug’s state, so a destroy-and-re-init cycle does look like a fresh install.

pug_flutter gates capture with two states (TrackingConsent.granted and TrackingConsent.denied) and starts granted. There is no cookieless middle state and no isConsentPending(): the Web SDK’s three-state model is a browser concern, where the identifier is a cookie and the banner is a page element.

await Pug.init(
  'YOUR_PROJECT_ID',
  const PugOptions(
    apiKey: 'pub_YOUR_PUBLIC_KEY',
    trackingConsent: TrackingConsentConfig(
      defaultConsent: TrackingConsent.denied,
      persist: true,
    ),
  ),
);

Pug.optInTracking();
Pug.optOutTracking();

Pug.isTrackingEnabled();
Pug.getTrackingConsent();

Note the field is defaultConsent, not the Web SDK’s initial.

Field Type Default Description
defaultConsent TrackingConsent granted Starting state, used when nothing is persisted.
persist bool false Write the choice to __pug_<projectId>_consent__ and restore it on the next Pug.init(). Otherwise the choice lasts for the process and resets to defaultConsent on restart.

While denied, Pug.track(...), the typed Pug.track.* methods, Pug.identify(...) and the automatic lifecycle, page-view and install events are all dropped, resuming once consent is granted.

Consent is independent of dryRun, which suppresses delivery without changing consent. Consent activity (denied drops, persistence failures) is reported only through the configured PugLogger, and the default NoopPugLogger is silent, so pass DebugPrintPugLogger() while you’re wiring a prompt up.

Redacting PII

pug_flutter has no device-side redaction - no beforeSend, redactUrlParams or element-text capture - so keep PII out of the properties you pass to Pug.track(...) yourself.

Upgrading

v0.0.5

Campaign capture and push device registration leave the public API until the backend accepts them. The code stays in the package, to be switched back on in a later release, but code that used either no longer compiles:

Removed Fix
PugOptions.autoCaptureCampaigns and linkProvider Delete them. No $utm* or click-id auto-properties are attached any more
PugPush, PushProvider, PushSubscription, PushSubscribeOptions Delete the registration code - the devices endpoint isn’t live, so it could not succeed
PugLinkProvider, AppLinksPugLinkProvider, campaignPropertiesFromUri Delete them
PugTransport.subscribeDevice Delete it from a custom transport, which no longer has to implement it

The notification events are unaffected. Pug.track.notificationClicked(...), notificationReceived and notificationDismissed still work alongside your own push integration - send them with TrackOptions(immediate: true), since those callbacks often fire while the app is backgrounded and the OS can suspend it before the next flush.

  • app_install and app_update are tracked for you, behind the new autoInstallEvents option (default on). An app that already used Pug reports no installs on upgrade, and its first app_update comes with the release after the one that ships 0.0.5. If you already track either event yourself, set autoInstallEvents: false, or each is counted twice.
  • app_close no longer fires on inactive. An incoming call, the app switcher or a control-centre swipe used to log an app_close/app_open pair; now only hidden, paused and detached count as leaving, so expect fewer of both.

v0.0.4

Nothing here breaks compilation - the changes are all in the auto-properties each event carries.

  • $deviceType is new: tv, mobile or desktop. Android TV is detected from the device’s system features rather than the OS, which reports plain android there, so $platform stays android and matches the other SDKs. Omitted rather than guessed on web and on any target it can’t classify.
  • $deviceModel no longer ships placeholders. device_info_plus returns literal "Unknown device"/"Unknown Model" strings when its marketing-name lookup misses; those now fall back to the device identifier (iPhone, MacBookPro18,3). macOS was affected on any Mac absent from the plugin’s table.
  • $osVersion is omitted when the device-info payload carries no version, rather than sent as an empty string that overwrote the fallback.
  • $deviceManufacturer is no longer sent on Windows, which exposes no manufacturer - the value being sent was the host name.
  • $platform now derives from Platform.operatingSystem rather than defaultTargetPlatform. No value changes on any supported platform.
Navigation

Type to search...

↑↓ navigate↵ selectEsc close