MostlyGoodMetrics Flutter SDK

A lightweight Flutter SDK for tracking analytics events with MostlyGoodMetrics.

Documentation: docs.mostlygoodmetrics.com

Table of Contents

Requirements

  • Flutter 3.10+
  • Dart 3.0+

Platform Support

Platform Supported
iOS Yes
Android Yes
Web Yes
macOS Yes
Windows Yes
Linux Yes

Installation

Add the package to your pubspec.yaml:

dependencies:
  mostly_good_metrics_flutter: ^0.3.0

Then install dependencies:

flutter pub get

Or install directly via command line:

flutter pub add mostly_good_metrics_flutter

Quick Start

Initialize once at app startup (typically in main.dart):

import 'package:flutter/material.dart';
import 'package:mostly_good_metrics_flutter/mostly_good_metrics_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await MostlyGoodMetrics.configure(
    MGMConfiguration(apiKey: 'mgm_proj_your_api_key'),
  );

  runApp(const MyApp());
}

Then track events anywhere in your app:

MostlyGoodMetrics.track('button_clicked');

That's it! Events are automatically batched and sent.

Configuration Options

For more control, pass additional configuration:

await MostlyGoodMetrics.configure(
  MGMConfiguration(
    apiKey: 'mgm_proj_your_api_key',
    baseUrl: 'https://ingest.mostlygoodmetrics.com',
    environment: 'production',
    appVersion: '1.0.0', // Required for install/update tracking
    maxBatchSize: 100,
    flushInterval: 30,
    maxStoredEvents: 10000,
    enableDebugLogging: kDebugMode,
    trackAppLifecycleEvents: true,
    existingInstallation: false,
    contextProvider: () => {
      'current_screen': currentScreenName(),
    },
    optedOutByDefault: false,
    collectDeviceProperties: true,
  ),
);
Option Default Description
apiKey Required Your MostlyGoodMetrics API key
baseUrl https://ingest.mostlygoodmetrics.com API endpoint
environment "production" Environment name
appVersion - App version string (required for install/update tracking)
maxBatchSize 100 Events per batch (1-1000)
flushInterval 30 Auto-flush interval in seconds
maxStoredEvents 10000 Max cached events
enableDebugLogging false Enable debug output
trackAppLifecycleEvents true Auto-track lifecycle events
existingInstallation false Seed lifecycle state during a provider migration; prevents a false $app_installed for the current appVersion
contextProvider - Dynamic properties evaluated for every event; see Dynamic Context
experimentMode MGMExperimentMode.server How experiment variants are assigned (see Local Experiment Enrollment)
localExperiments - Inline experiment definitions for local mode (zero-network enrollment)
optedOutByDefault false Start opted out (for consent-first apps)
collectDeviceProperties true Collect device manufacturer, locale, and timezone

User Identification

By default, the SDK tracks events with an anonymous user ID. You can identify users to associate events with a specific user:

// Set user identity
await MostlyGoodMetrics.identify('user_123');

Once identified, all future events will be associated with this user ID. The user ID persists across app launches.

To reset the identity (e.g., on logout):

// Reset identity (falls back to the existing anonymous ID)
await MostlyGoodMetrics.resetIdentity();

// Full "forget me": also rotates the anonymous ID, deletes pending
// events, and clears super properties (see Privacy section)
await MostlyGoodMetrics.resetIdentity(clearAnonymousId: true);

Note: After calling resetIdentity(), the SDK starts a new session and subsequent events fall back to the anonymous ID.

Tracking Events

Track events anywhere in your app:

// Simple event
MostlyGoodMetrics.track('button_clicked');

// Event with properties
MostlyGoodMetrics.track('purchase_completed', properties: {
  'product_id': 'SKU123',
  'price': 29.99,
  'currency': 'USD',
});

Events are automatically queued and sent in batches for optimal network usage.

Event Naming

Event names must:

  • Start with a letter (or $ for system events)
  • Contain only alphanumeric characters, underscores, and spaces
  • Be 255 characters or less

Reserved $ prefix: Event names starting with $ are reserved for SDK system events (e.g., $app_opened, $app_installed). Do not use the $ prefix for your own events.

// Valid
MostlyGoodMetrics.track('button_clicked');
MostlyGoodMetrics.track('PurchaseCompleted');
MostlyGoodMetrics.track('step_1_completed');
MostlyGoodMetrics.track('user signed up');  // spaces allowed

// Invalid (will throw MGMError)
MostlyGoodMetrics.track('123_event');      // starts with number
MostlyGoodMetrics.track('event-name');     // contains hyphen
MostlyGoodMetrics.track('$custom_event');  // $ prefix is reserved

Properties

Events support various property types:

MostlyGoodMetrics.track('checkout', properties: {
  'string_prop': 'value',
  'int_prop': 42,
  'double_prop': 3.14,
  'bool_prop': true,
  'list_prop': ['a', 'b', 'c'],
  'nested': {
    'key': 'value',
  },
});

Limits:

  • String values: max 1000 characters
  • Nesting depth: max 3 levels
  • Total event payload: max 10KB

Dynamic Context

Use contextProvider for properties that can change during a session. It runs immediately before each event is recorded:

await MostlyGoodMetrics.configure(
  MGMConfiguration(
    apiKey: 'mgm_proj_your_api_key',
    contextProvider: () => {
      'screen': currentScreenName(),
      'plan': currentPlan(),
    },
  ),
);

Property precedence is deterministic: super properties < dynamic context < event properties < MGM system properties. MGM system properties such as $sdk cannot be overwritten.

Migrating an Existing Installation

When replacing another analytics SDK in an already-shipped app, derive existingInstallation from that provider's persisted installation marker for each device. MGM stores the current appVersion as lifecycle state and does not emit $app_installed for users with that marker:

await MostlyGoodMetrics.configure(
  MGMConfiguration(
    apiKey: 'mgm_proj_your_api_key',
    appVersion: '2.4.0',
    existingInstallation: legacyAnalytics.hasInstallationMarker,
  ),
);

Do not set existingInstallation: true for every user in a migration release: that would suppress genuine new installs. Once the legacy marker is no longer needed, remove this migration-specific configuration.

In DEBUG builds, MGM logs diagnostics for invalid event names and custom property keys beginning with $; those prefixes are reserved for MGM. SDK internal events do not generate these warnings.

Automatic Events

When trackAppLifecycleEvents is enabled (default), the SDK automatically tracks:

Event When Properties
$app_installed First launch after install -
$app_updated First launch after version change previous_version, current_version
$app_opened App became active -
$app_backgrounded App went to background -

Automatic Context

Every event automatically includes contextual information to provide rich analytics capabilities. You don't need to manually add these fields.

Identity & Session

Field Description Example Persistence
user_id Identified user ID (set via identify()) or anonymous ID user_123 or $anon_abc123def456 Persisted in local storage (survives app restarts)
session_id UUID generated per app launch a1b2c3d4-e5f6-7890-abcd-ef1234567890 Regenerated on each app launch

Device & Platform

Field Description Example Source
platform Platform identifier ios, android, web, macos, windows, linux Platform.operatingSystem
os_version Operating system version iOS 17.4, Android 14, macOS 14.3, Version 10.0 (Build 19045) Platform.operatingSystemVersion
device_manufacturer Device manufacturer Apple iOS/macOS only; null on other platforms
locale User's locale from device settings en_US, fr_FR Device locale settings
timezone User's timezone offset name EST, PST, UTC+5 System timezone settings

App & Environment

Field Description Example Source
app_version App version (if configured) 1.2.3 Configuration option (appVersion)
environment Environment name production, staging, development Configuration option (default: production)

Event Metadata

Field Description Example Purpose
client_event_id Unique UUID for each event 550e8400-e29b-41d4-a716-446655440000 Deduplication (prevents processing the same event twice)
timestamp ISO 8601 timestamp when event was tracked 2024-01-15T10:30:00.000Z Event ordering and time-based analysis

Note: All fields are automatically included with every event—no additional code required.

Automatic Behavior

The SDK automatically handles common tasks so you can focus on tracking what matters:

  • Anonymous user ID generation - UUID automatically generated and persisted for anonymous tracking
  • User ID persistence - Identity set via identify() persists across app launches; falls back to anonymous ID when reset
  • Event persistence - Events are saved to local storage and survive app restarts
  • Batch processing - Events are grouped for efficient network usage
  • Periodic flush - Events are sent every 30 seconds (configurable via flushInterval)
  • Background flush - Events are sent when the app goes to background
  • Retry on failure - Failed requests are retried; events are preserved until successfully sent
  • Session management - New session ID generated on each app launch
  • Deduplication - Events include unique IDs (client_event_id) to prevent duplicate processing

A/B Testing (Experiments)

By default, variants are assigned by the MostlyGoodMetrics server (MGMExperimentMode.server). Assignments are fetched in the background at configure (never blocking), cached per user in shared_preferences with no expiry, and refreshed at most about once per hour (stale-while-revalidate). For on-device assignment, see Local Experiment Enrollment.

Read a variant:

// Synchronous, never throws, never blocks.
// Returns the fallback (default null) when the experiment is unknown
// or assignments haven't loaded yet.
final variant = MostlyGoodMetrics.getVariant(
  'checkout-flow',
  fallback: 'control',
);

if (variant == 'treatment') {
  // Show treatment UI
}

Wait for assignments to load (optional):

// Completes when the initial load attempt finishes (success or failure),
// or when the timeout elapses - whichever comes first. Never hangs.
final loaded = await MostlyGoodMetrics.ready(
  timeout: Duration(seconds: 2),
);

Behavior:

  • Reading a variant sets the super property $experiment_{snake_case(name)} so the variant is attached to all subsequent events
  • Reading a variant tracks a $experiment_exposure event ($experiment_name, $variant) once per user/experiment/variant — the dedup is persisted and survives app restarts
  • After identify() with a new user ID, the SDK keeps serving the current variants and refetches assignments for the new user (linking the stored anonymous ID); the new assignments are swapped in atomically when the response arrives

Local Experiment Enrollment

With MGMExperimentMode.local, variants are assigned on device by deterministically hashing the experiment ID and the effective user ID (the identified user ID, or the anonymous ID before identify()). The user ID never leaves the device for enrollment — a privacy benefit over server-side assignment, and it works offline.

await MostlyGoodMetrics.configure(
  MGMConfiguration(
    apiKey: 'mgm_proj_your_api_key',
    experimentMode: MGMExperimentMode.local,
  ),
);

// Same API as server mode
final variant = MostlyGoodMetrics.getVariant(
  'button-color',
  fallback: 'control',
);

In this mode the SDK fetches experiment definitions (IDs, names, and variant lists — no user data is sent) from /v1/experiments/configs, caches them, and refreshes them in the background about once per hour.

Zero-network option: provide the experiment definitions inline and the SDK makes no experiment requests at all:

await MostlyGoodMetrics.configure(
  MGMConfiguration(
    apiKey: 'mgm_proj_your_api_key',
    experimentMode: MGMExperimentMode.local,
    localExperiments: [
      MGMExperimentConfig(
        id: '7b1e8a90-4c2d-4f6a-9e3b-2a1d5c8f0e71', // experiment UUID
        name: 'button-color',
        variants: ['control', 'treatment'],
      ),
    ],
  ),
);

Behavior:

  • Bucketing is deterministic: the variant is variants[bucket % variants.length], where bucket is the first 8 bytes of SHA-256("<experiment_uuid>:<user_id>") as an unsigned big-endian 64-bit integer — identical across MostlyGoodMetrics SDKs
  • Assignments are sticky: the first variant read for an experiment is persisted (keyed by experiment UUID) and reused, including after identify() — the SDK never re-buckets a device
  • Exposure tracking works exactly as in server mode: super property, $experiment_exposure event, and per-user/experiment/variant dedup
  • While opted out (see Privacy), local mode makes zero network requests — no config fetches — and records no exposures; bucketing from inline or previously cached definitions keeps working
  • A full "forget me" (resetIdentity(clearAnonymousId: true)) clears the sticky assignments so the new identity is re-bucketed fresh; a plain resetIdentity() keeps them

Cross-device caveat: because enrollment is keyed to the effective user ID at first read (often the per-device anonymous ID) and assignments are sticky per device, the same person may receive different variants on different devices. Server mode can link anonymous and identified assignments across devices; local mode cannot. Prefer server mode when consistent cross-device assignment matters more than keeping user IDs on device.

Manual Flush

Events are automatically flushed periodically and when the app backgrounds. You can also trigger a manual flush:

await MostlyGoodMetrics.flush();

To check pending events:

final count = await MostlyGoodMetrics.getPendingEventCount();
print('$count events pending');

To clear pending events:

await MostlyGoodMetrics.clearPendingEvents();

Session Management

The SDK automatically generates a new session ID when:

  • The SDK is configured
  • resetIdentity() is called
  • startNewSession() is called
// Start a new session manually
await MostlyGoodMetrics.startNewSession();

// Access current session ID
final sessionId = MostlyGoodMetrics.sessionId;

Privacy

The SDK is designed to collect the minimum needed for useful analytics — and to make it easy to collect less.

What's collected automatically:

  • A random anonymous ID (e.g., $anon_abc123def456) — generated by the SDK, not derived from the device
  • A per-launch session ID
  • Platform, OS version, and app version (functional fields used for filtering and compatibility)
  • Device manufacturer, locale, and timezone (can be disabled, see below)

What's never collected: advertising identifiers (IDFA/GAID), precise location, contacts, or any other personal data you don't explicitly pass to track() or identify(). identify() is entirely optional — the SDK works fully anonymously without it.

Opt-out / opt-in

Let users opt out of all tracking at runtime:

// Stop all tracking immediately. track(), identify(), and flush() become
// no-ops, and any queued (unsent) events are deleted. The choice is
// persisted and survives app restarts.
await MostlyGoodMetrics.optOut();

// Check the current state
final optedOut = MostlyGoodMetrics.isOptedOut;

// Resume tracking (also persisted)
await MostlyGoodMetrics.optIn();

For consent-first apps (e.g., GDPR consent flows), start opted out and only opt in after the user grants consent:

await MostlyGoodMetrics.configure(
  MGMConfiguration(
    apiKey: 'mgm_proj_your_api_key',
    optedOutByDefault: true,
  ),
);

// Later, once the user consents:
await MostlyGoodMetrics.optIn();

A persisted opt-in/opt-out choice always takes precedence over optedOutByDefault.

While opted out, local experiment enrollment makes no config fetches (zero network) and records no exposure events or dedup state — getVariant() keeps working from inline or cached definitions, and the first read after optIn() records the exposure.

Rotating the anonymous ID

Generate a fresh anonymous ID so future events can't be linked to earlier anonymous activity:

await MostlyGoodMetrics.resetAnonymousId();

Full "forget me"

For a complete local reset (e.g., a user deletes their account):

await MostlyGoodMetrics.resetIdentity(clearAnonymousId: true);

This clears the user ID, rotates the anonymous ID, deletes all pending (unsent) events, clears all super properties, clears sticky local experiment assignments (so the new identity is re-bucketed fresh, see Local Experiment Enrollment), and starts a new session — nothing tracked afterwards can be linked to the previous user.

Limiting device properties

To omit device manufacturer, locale, and timezone from all events:

await MostlyGoodMetrics.configure(
  MGMConfiguration(
    apiKey: 'mgm_proj_your_api_key',
    collectDeviceProperties: false,
  ),
);

Functional fields (platform, OS version, app version) are always sent.

Debug Logging

Enable debug logging to see SDK activity:

await MostlyGoodMetrics.configure(
  MGMConfiguration(
    apiKey: 'mgm_proj_your_api_key',
    enableDebugLogging: true,
  ),
);

Output example:

[MostlyGoodMetrics] Configuring MostlyGoodMetrics SDK
[MostlyGoodMetrics] Tracked event: button_clicked
[MostlyGoodMetrics] Flushing 5 events
[MostlyGoodMetrics] Successfully sent 5 events

Error Handling

The SDK throws MGMError for validation errors:

try {
  MostlyGoodMetrics.track('invalid-event-name');
} on MGMError catch (e) {
  print('Error type: ${e.type}');
  print('Message: ${e.message}');
}

Error types:

  • MGMErrorType.notConfigured - SDK not configured
  • MGMErrorType.invalidEventName - Invalid event name
  • MGMErrorType.invalidProperties - Invalid properties (too deeply nested)
  • MGMErrorType.networkError - Network failure
  • MGMErrorType.storageError - Storage failure
  • MGMErrorType.rateLimited - API rate limited

Framework Integration

MaterialApp

For a complete Flutter app setup with MostlyGoodMetrics:

import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:mostly_good_metrics_flutter/mostly_good_metrics_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await MostlyGoodMetrics.configure(
    MGMConfiguration(
      apiKey: 'mgm_proj_your_api_key',
      appVersion: '1.0.0',
      enableDebugLogging: kDebugMode,
    ),
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            MostlyGoodMetrics.track('button_clicked', properties: {
              'screen': 'home',
            });
          },
          child: const Text('Track Event'),
        ),
      ),
    );
  }
}

Running the Example

cd example
flutter pub get
flutter run

Testing

To run the tests:

flutter test

License

MIT

Libraries

mostly_good_metrics_flutter
Official Flutter SDK for MostlyGoodMetrics.