MostlyGoodMetrics Flutter SDK
A lightweight Flutter SDK for tracking analytics events with MostlyGoodMetrics.
Documentation: docs.mostlygoodmetrics.com
Table of Contents
- Requirements
- Platform Support
- Installation
- Quick Start
- Configuration Options
- User Identification
- Tracking Events
- Event Naming
- Properties
- Dynamic Context
- Migrating an Existing Installation
- Automatic Events
- Automatic Context
- Automatic Behavior
- A/B Testing (Experiments)
- Local Experiment Enrollment
- Manual Flush
- Session Management
- Privacy
- Debug Logging
- Error Handling
- Framework Integration
- Running the Example
- Testing
- License
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_exposureevent ($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], wherebucketis the first 8 bytes ofSHA-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_exposureevent, 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 plainresetIdentity()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 calledstartNewSession()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 configuredMGMErrorType.invalidEventName- Invalid event nameMGMErrorType.invalidProperties- Invalid properties (too deeply nested)MGMErrorType.networkError- Network failureMGMErrorType.storageError- Storage failureMGMErrorType.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.