@mostly-good-metrics/capacitor
v0.5.1
Published
Capacitor SDK for MostlyGoodMetrics analytics
Maintainers
Readme
MostlyGoodMetrics Capacitor SDK
The official Capacitor SDK for MostlyGoodMetrics analytics.
Table of Contents
- Requirements
- Platform Support
- Installation
- Quick Start
- User Identification
- Privacy
- Apple privacy manifest
- Configuration Options
- Tracking Events
- Event Naming
- Properties
- Super Properties
- A/B Testing (Experiments)
- Local Experiment Enrollment
- Manual Flush
- Automatic Events
- Automatic Properties
- Automatic Context
- Automatic Behavior
- Debug Logging
- License
Requirements
- Capacitor 5.7.3+
- iOS 13+, Android 5.0+, or modern browser
Platform Support
The SDK provides comprehensive cross-platform support with platform-specific optimizations. Features and data availability vary by platform based on underlying OS capabilities.
Feature Support Matrix
| Feature | iOS | Android | Web |
|---------|-----|---------|-----|
| Lifecycle Tracking | ✅ Full support | ✅ Full support | ⚠️ Limited (initial load only) |
| Background Flush | ✅ Automatic on background | ✅ Automatic on background | ❌ Not applicable |
| Persistent Storage | ✅ Capacitor Preferences | ✅ Capacitor Preferences | ✅ localStorage (via Preferences) |
| Install Detection | ✅ $app_installed event | ✅ $app_installed event | ✅ $app_installed event |
| Update Detection | ✅ $app_updated event | ✅ $app_updated event | ✅ $app_updated event |
| Device Model | ✅ Hardware identifier | ✅ Build.MODEL | ❌ Not available |
| Device Manufacturer | ✅ Always "Apple" | ✅ Build.MANUFACTURER | ❌ Not available |
| Device Type Detection | ✅ Phone/Tablet (isPad) | ✅ Phone/Tablet | ⚠️ Desktop/Mobile (user agent) |
| OS Version | ✅ Semantic (e.g., "17.2") | ✅ API level (e.g., "14") | ⚠️ User agent parsing |
Context Properties by Platform
This table shows which automatic context fields and properties are available on each platform:
| Property/Field | iOS | Android | Web | Notes |
|----------------|-----|---------|-----|-------|
| Identity & Session |
| userId | ✅ | ✅ | ✅ | Set via identify(), persisted in Preferences |
| sessionId | ✅ | ✅ | ✅ | UUID generated per app launch |
| clientEventId | ✅ | ✅ | ✅ | UUID per event for deduplication |
| Platform & Device |
| platform | ✅ "ios" | ✅ "android" | ✅ "web" | From Capacitor.getPlatform() |
| osVersion | ✅ "17.2" | ✅ "14" | ✅ "macOS 14.3" | From @capacitor/device (or "unknown") |
| deviceModel | ✅ "iPhone15,2" | ✅ "Pixel 8" | ❌ | From @capacitor/device (mobile only) |
| deviceManufacturer | ✅ "Apple" | ✅ "Samsung" | ❌ | From @capacitor/device (mobile only) |
| locale | ✅ | ✅ | ✅ | From JavaScript Intl API |
| timezone | ✅ | ✅ | ✅ | From JavaScript Intl API |
| App & Environment |
| appVersion | ✅ | ✅ | ✅ | From configuration (if set) |
| environment | ✅ | ✅ | ✅ | From configuration (default: "production") |
| sdk | ✅ "capacitor" | ✅ "capacitor" | ✅ "capacitor" | Hardcoded SDK identifier |
| sdkVersion | ✅ | ✅ | ✅ | Package version |
| Automatic Properties |
| $device_type | ✅ "phone"/"tablet" | ✅ "phone"/"tablet" | ✅ "desktop"/"mobile" | iOS uses Platform.isPad; Web uses user agent |
| $device_model | ✅ | ✅ | ❌ | Only on mobile when @capacitor/device installed |
| $storage_type | ✅ "persistent"/"memory" | ✅ "persistent"/"memory" | ✅ "persistent"/"memory" | Indicates if Preferences is available |
Platform-Specific Implementation Details
iOS
Lifecycle Events:
- Uses
@capacitor/appplugin'sappStateChangelistener $app_opened: Fires when app returns to foreground (isActive: true)$app_backgrounded: Fires when app enters background (isActive: false)- Automatic flush triggered when app backgrounds
Device Information:
deviceModel: Hardware identifier (e.g., "iPhone15,2" for iPhone 14 Pro)deviceManufacturer: Always "Apple"osVersion: Semantic version (e.g., "17.2", "16.4.1")$device_type: Accurately detects "phone" vs "tablet" using native APIs
Storage:
- Events persisted via
@capacitor/preferences(maps to iOS UserDefaults) - User ID persisted across app launches
- Falls back to in-memory storage if Preferences unavailable
Android
Lifecycle Events:
- Uses
@capacitor/appplugin'sappStateChangelistener $app_opened: Fires when app returns to foreground (isActive: true)$app_backgrounded: Fires when app enters background (isActive: false)- Automatic flush triggered when app backgrounds
Device Information:
deviceModel: Build.MODEL (e.g., "Pixel 8", "SM-G998B")deviceManufacturer: Build.MANUFACTURER (e.g., "Google", "Samsung")osVersion: Android API level as string (e.g., "14" for Android 14, "31" for Android 12)$device_type: Detects "phone" vs "tablet" (can be enhanced with screen dimensions)
Storage:
- Events persisted via
@capacitor/preferences(maps to Android SharedPreferences) - User ID persisted across app launches
- Falls back to in-memory storage if Preferences unavailable
Web
Lifecycle Events:
- Limited support: Only
$app_openedon initial page load - No background/foreground events (browsers don't provide reliable lifecycle APIs for tabs)
- No automatic background flush (events flush on periodic interval only)
@capacitor/appplugin has no effect on Web platform
Device Information:
deviceModel: Not available (Web browsers don't expose device model)deviceManufacturer: Not availableosVersion: Extracted from user agent string (e.g., "macOS 14.3", "Windows 10")$device_type: Inferred from user agent ("desktop" or "mobile")
Storage:
- Events persisted via
localStorage(wrapped by@capacitor/preferences) - User ID persisted in localStorage
- Falls back to in-memory storage if localStorage is disabled/unavailable (e.g., private browsing mode)
Browser Considerations:
- Install/update detection works but is based on localStorage (cleared if user clears browsing data)
- Private browsing mode may disable localStorage, forcing memory-only storage
- No tab visibility tracking (consider implementing manually with Page Visibility API if needed)
Dependency Requirements
| Plugin | iOS | Android | Web | Purpose |
|--------|-----|---------|-----|---------|
| @capacitor/core | ✅ Required | ✅ Required | ✅ Required | Core Capacitor functionality |
| @capacitor/preferences | ⚠️ Recommended | ⚠️ Recommended | ⚠️ Recommended | Persistent event storage and user ID |
| @capacitor/app | ⚠️ Recommended | ⚠️ Recommended | ⚠️ Optional | Lifecycle tracking ($app_opened, etc.) |
| @capacitor/device | ⚠️ Recommended | ⚠️ Recommended | ⚠️ Recommended | Device info (model, manufacturer, OS version) |
Note: While only
@capacitor/coreis strictly required, we strongly recommend installing all plugins for full functionality. See Installation for details.
Installation
Install the SDK via npm:
npm install @mostly-good-metrics/capacitorOr with yarn:
yarn add @mostly-good-metrics/capacitorCapacitor Peer Dependencies
The SDK requires several Capacitor plugins to function properly:
npm install @capacitor/core @capacitor/app @capacitor/device @capacitor/preferencesThen sync your native projects:
npx cap syncDependency Details:
| Plugin | Version | Required? | Purpose | Behavior if Missing |
|--------|---------|-----------|---------|---------------------|
| @capacitor/core | 5.7.3+ | ✅ Yes | Core Capacitor functionality | SDK will not work |
| @capacitor/preferences | 5.0.7+ | ⚠️ Recommended | Persistent event and user storage | Events stored in memory only (lost on app restart) |
| @capacitor/app | 5.0.7+ | ⚠️ Recommended | App lifecycle tracking (iOS/Android) | No lifecycle events ($app_opened, etc.) |
| @capacitor/device | 5.0.7+ | ⚠️ Recommended | Device info (model, manufacturer, OS) | Missing device properties in events |
Note: While only
@capacitor/coreis strictly required, we strongly recommend installing all plugins for full functionality.
Quick Start
1. Initialize the SDK
Initialize once at app startup, typically in your main entry file:
import MostlyGoodMetrics from '@mostly-good-metrics/capacitor';
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
appVersion: '1.0.0', // Required for install/update tracking
});2. Track Events
// Simple event
MostlyGoodMetrics.track('button_clicked');
// Event with properties
MostlyGoodMetrics.track('purchase_completed', {
product_id: 'SKU123',
price: 29.99,
currency: 'USD',
});3. Identify Users (Optional)
// Set user identity
MostlyGoodMetrics.identify('user_123');That's it! Events are automatically batched and sent. See User Identification for more details.
User Identification
The SDK provides methods to identify users and associate events with specific user accounts.
Identifying Users
Use identify() to set the current user's identity:
// Basic identification with user ID
MostlyGoodMetrics.identify('user_123');
// Identification with profile data (email and name)
MostlyGoodMetrics.identify('user_123', {
email: '[email protected]',
name: 'Jane Doe',
});Parameters:
userId(required): Unique identifier for the user (string)profile(optional): Object containing:email: User's email address (string, optional)name: User's display name (string, optional)
User ID Persistence
User IDs are automatically persisted across app launches:
- Storage: Saved to Capacitor Preferences when
identify()is called - Restoration: Automatically restored on app restart (before any events are sent)
- Included in events: All events after identification include
userIdin the batch context
Anonymous ID persistence
Before you call identify(), events are attributed to an anonymous ID. The
wrapper persists this ID in Capacitor Preferences (native storage) and passes
it to the JS core, so it stays stable across app launches even when webview
cookies/localStorage are cleared. When you call identify(), the emitted
$identify event carries this anonymous ID as $anonymous_id, letting the
backend link the user's pre- and post-identification (anonymous → identified)
events. Rotating the ID via resetAnonymousId() or resetIdentity({ clearAnonymousId: true })
persists the new ID too.
Resetting Identity
Call resetIdentity() when a user logs out or you want to clear the current user:
MostlyGoodMetrics.resetIdentity();This clears the user ID from both memory and persistent storage. Subsequent events will not include a userId until identify() is called again.
Best Practices
- Call on login: Identify users immediately after successful authentication
- Reset on logout: Always call
resetIdentity()when users log out - Anonymous tracking: If you don't call
identify(), events are tracked anonymously (nouserIdin context) - User ID format: Use stable, unique identifiers (database IDs, UUIDs, etc.)—avoid using email addresses as IDs
Privacy
The SDK surfaces the privacy controls of the underlying JavaScript core, with opt-out state persisted in Capacitor Preferences (native storage), so the choice survives app restarts even when webview storage is cleared.
Opt-out / opt-in
MostlyGoodMetrics.optOut(); // Stop all tracking immediately
MostlyGoodMetrics.isOptedOut(); // => true
MostlyGoodMetrics.optIn(); // Resume trackingoptOut()immediately stops tracking:track(),identify(),flush()and automatic lifecycle events become no-ops, queued (unsent) events are purged, and the JS core makes no experiments network requests (including local-mode config fetches). In local modegetVariant()still buckets from inline/cached configs, but exposure events wait untiloptIn().- The choice is persisted in Capacitor Preferences and restored during
configure(); the initial$app_openedlifecycle event waits for that restore, so opted-out launches stay silent. - An explicit
optIn()is persisted too and overridesoptedOutByDefaulton later launches.
Consent-first apps
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
optedOutByDefault: true,
});
// Later, once the user consents:
MostlyGoodMetrics.optIn();Resetting the anonymous ID / forget me
// Rotate just the anonymous ID (returns the new ID)
const newId = MostlyGoodMetrics.resetAnonymousId();
// Standard logout: clear the user ID, keep the anonymous ID
MostlyGoodMetrics.resetIdentity();
// Full "forget me": also rotates the anonymous ID and purges queued events,
// super properties, identify debounce state and cached experiment variants
MostlyGoodMetrics.resetIdentity({ clearAnonymousId: true });The full "forget me" also clears the sticky local experiment assignments (local enrollment mode), so the new anonymous ID is re-bucketed - resetAnonymousId() clears them too.
Reduced data collection
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
collectDeviceProperties: false,
});When false, the wrapper omits $device_type and $device_model and the JS core omits locale/timezone context. Platform, OS version and app version are still sent.
Note: The JS core's
respectDoNotTrackandpersistenceoptions are web-only (browser Do Not Track / Global Privacy Control signals and cookie/localStorage persistence modes) and are not part of the Capacitor configuration. Opt-out state is persisted natively via Capacitor Preferences instead.
Apple privacy manifest
This SDK is pure JavaScript and has no native Apple code of its own, so it does not ship a PrivacyInfo.xcprivacy file. Its native functionality comes from Capacitor and the official Ionic plugins.
Capacitor core ships privacy manifests for its Capacitor and CapacitorCordova pods starting in 5.7.3. However, Ionic's @capacitor/app, @capacitor/device, and @capacitor/preferences plugins currently do not ship privacy manifests. Your app must declare the UserDefaults required-reason API with reason CA92.1 in its own app-level privacy manifest. If you use @capacitor/device below 7.0.0, also check its disk-space API usage against your app's required-reason declarations.
See the Mostly Good Metrics privacy guide for a copy-pasteable app-level manifest.
Configuration Options
For more control, pass a configuration object:
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
appVersion: '1.0.0',
environment: 'production',
maxBatchSize: 100,
flushInterval: 30,
maxStoredEvents: 10000,
enableDebugLogging: false,
trackAppLifecycleEvents: true,
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| appVersion | string | - | App version string (required for install/update tracking) |
| existingInstallation | boolean | false | On the first MGM launch, establish the version baseline without $app_installed |
| environment | string | "production" | Environment name |
| baseURL | string | https://ingest.mostlygoodmetrics.com | API endpoint |
| maxBatchSize | number | 100 | Events per batch (1-1000) |
| flushInterval | number | 30 | Auto-flush interval in seconds |
| maxStoredEvents | number | 10000 | Max cached events |
| enableDebugLogging | boolean | false | Enable console output |
| trackAppLifecycleEvents | boolean | true | Auto-track lifecycle events |
| optedOutByDefault | boolean | false | Start opted out until optIn() is called (consent-first apps) |
| collectDeviceProperties | boolean | true | Collect $device_type/$device_model (and locale context in the JS core) |
| contextProvider | () => EventProperties | - | Per-event dynamic properties; precedence is super properties, dynamic context, event properties, then MGM system properties |
| experimentMode | 'server' \| 'local' | 'server' | Server-assigned variants, or on-device bucketing (see Local Experiment Enrollment) |
| localExperiments | MGMExperimentConfig[] | - | Inline experiment configs for local mode (zero network) |
Tracking Events
Track custom events throughout your app using the track() method.
// Simple event
MostlyGoodMetrics.track('button_clicked');
// Event with properties
MostlyGoodMetrics.track('purchase_completed', {
product_id: 'SKU123',
price: 29.99,
currency: 'USD',
});Event Naming
Event names must:
- Start with a letter (or
$for system events) - Contain only alphanumeric characters and underscores
- Be 255 characters or less
// Valid
MostlyGoodMetrics.track('button_clicked');
MostlyGoodMetrics.track('PurchaseCompleted');
MostlyGoodMetrics.track('step_1_completed');
// Invalid (will be ignored)
MostlyGoodMetrics.track('123_event'); // starts with number
MostlyGoodMetrics.track('event-name'); // contains hyphen
MostlyGoodMetrics.track('event name'); // contains spaceNote: Event names starting with
$are reserved for system events. User events should not use this prefix.
Properties
Events support various property types:
MostlyGoodMetrics.track('checkout', {
string_prop: 'value',
int_prop: 42,
double_prop: 3.14,
bool_prop: true,
null_prop: null,
list_prop: ['a', 'b', 'c'],
nested: {
key: 'value',
},
});Limits:
- String values: truncated to 1000 characters
- Nesting depth: max 3 levels
- Total properties size: max 10KB
Dynamic context
Use contextProvider for values that must be evaluated on every event, such
as the active organization or screen. Its values override super properties;
explicit event properties and MGM system properties take precedence over it.
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
contextProvider: () => ({
organization_id: activeOrganizationId(),
current_screen: currentScreenName(),
}),
});With enableDebugLogging: true, the JavaScript core warns when custom event
properties use MGM-reserved $ keys.
Super Properties
Super properties are persistent properties that are automatically included with every event. Use them for context that applies across your entire session (e.g., user plan, A/B test group).
Setting Super Properties
// Set a single super property
MostlyGoodMetrics.setSuperProperty('plan', 'premium');
// Set multiple super properties at once
MostlyGoodMetrics.setSuperProperties({
plan: 'premium',
tier: 'gold',
ab_group: 'variant_a',
});Getting Super Properties
// Get all current super properties
const props = MostlyGoodMetrics.getSuperProperties();
console.log(props); // { plan: 'premium', tier: 'gold', ab_group: 'variant_a' }Removing Super Properties
// Remove a single super property
MostlyGoodMetrics.removeSuperProperty('plan');
// Clear all super properties
MostlyGoodMetrics.clearSuperProperties();Behavior
- Super properties are merged with event properties when tracking
- If an event property has the same key as a super property, the event property takes precedence
- Super properties persist in memory for the duration of the app session
- Super properties are not persisted to storage (they reset on app restart)
Use Cases
Super properties are ideal for:
- User subscription tier or plan
- A/B test variant assignment
- Feature flags or experimental features enabled
- User role or permissions level
- Any session-scoped context that applies to all events
// Example: Set user context on login
MostlyGoodMetrics.identify('user_123');
MostlyGoodMetrics.setSuperProperties({
plan: 'premium',
role: 'admin',
ab_test_variant: 'new_checkout',
});
// All subsequent events will include these properties
MostlyGoodMetrics.track('page_viewed', { page: 'dashboard' });
// Sends: { page: 'dashboard', plan: 'premium', role: 'admin', ab_test_variant: 'new_checkout' }
// Event properties override super properties
MostlyGoodMetrics.track('feature_used', { plan: 'trial' });
// Sends: { plan: 'trial', role: 'admin', ab_test_variant: 'new_checkout' }A/B Testing (Experiments)
// Wait for experiments to load (resolves immediately when inline
// localExperiments are configured or the cache is hydrated)
await MostlyGoodMetrics.ready();
// Get the assigned variant, with an optional fallback for when the
// experiment is unknown or experiments haven't loaded yet (default: null)
const variant = MostlyGoodMetrics.getVariant('checkout-flow', 'control');
if (variant === 'treatment') {
// show the new checkout
}By default (experimentMode: 'server') variants are assigned server-side so the same user always gets the same variant. On a getVariant() hit the variant is stored as a $experiment_{name} super property and a $experiment_exposure event is tracked once per (user, experiment, variant).
Requires a
@mostly-good-metrics/javascriptcore release with experiment support at runtime; on older coresgetVariant()logs a warning and returns the fallback.
Local Experiment Enrollment
If you prefer that no user identifier ever leaves the device for experiment enrollment, switch to local mode. Experiment configurations are loaded identity-free (GET /v1/experiments/configs, no user_id/anonymous_id on the wire) and variants are assigned on-device with deterministic hashing - identical across all MGM SDKs:
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
experimentMode: 'local',
});
// Or fully offline - no experiments network request at all:
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
experimentMode: 'local',
localExperiments: [
{
id: '7b1e8a90-4c2d-4f6a-9e3b-2a1d5c8f0e71', // experiment UUID from the dashboard
name: 'button-color',
variants: ['control', 'treatment'],
},
],
});
await MostlyGoodMetrics.ready();
const variant = MostlyGoodMetrics.getVariant('button-color', 'control');Details (provided by the underlying JavaScript SDK - see its README for the full algorithm):
- Algorithm:
variant = variants[bucket % variants.length]wherebucketis the first 8 bytes ofSHA-256(utf8("<experiment_uuid>:<user_id>"))as an unsigned big-endian 64-bit integer.user_idis the identified ID if set, otherwise the anonymous ID. - Sticky: the first resolved variant is persisted per experiment UUID (in the webview's localStorage) and reused across restarts;
identify()never re-buckets. Assignments are cleared byresetAnonymousId()and the forget-meresetIdentity({ clearAnonymousId: true })(see Privacy). - Exposure events unchanged:
$experiment_exposurewith the raw experiment name and the existing dedup. - Caveat - no cross-device consistency for anonymous users: there is no server-side alias resolution in local mode. A user who is anonymous on one device and identified on another may see different variants until identified everywhere.
Manual Flush
Events are automatically flushed periodically and when the app backgrounds. You can also trigger a manual flush:
MostlyGoodMetrics.flush();To check pending events:
const count = await MostlyGoodMetrics.getPendingEventCount();
console.log(`${count} events pending`);Automatic Events
When trackAppLifecycleEvents is enabled (default), the SDK automatically tracks:
| Event | When | Properties |
|-------|------|------------|
| $app_installed | First launch after install | $version |
| $app_updated | First launch after version change | $version, $previous_version |
| $app_opened | App came to foreground | - |
| $app_backgrounded | App went to background | - |
Note: Install and update detection require
appVersionto be configured.
Migrating from another analytics provider
If an existing app is adopting MGM, use your previous provider's persisted
installation marker to baseline its first MGM launch. This prevents existing
users from being counted as fresh installs while allowing genuinely new users
to emit $app_installed.
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
appVersion: AppInfo.version,
existingInstallation: legacyAnalytics.hasInstallationMarker(),
});Do not set existingInstallation: true for every user: remove the legacy
marker once the migration window has passed.
Automatic Properties
The SDK automatically includes these properties with every event:
| Property | Description | Example | Platform Availability |
|----------|-------------|---------|----------------------|
| $device_type | Device form factor | phone, tablet, desktop | All |
| $device_model | Device model name | iPhone 14, SM-G998B | iOS, Android (requires @capacitor/device) |
| $storage_type | Event persistence method | persistent, memory | All |
Automatic Context
Every event automatically includes the following context fields to provide rich analytics capabilities. These fields are collected automatically—you don't need to manually add any of them.
Identity & Session
| Field | Description | Example | Persistence |
|-------|-------------|---------|-------------|
| userId | Identified user ID (set via identify()) | user_123 | Persisted in Preferences (survives app restarts) |
| sessionId | UUID generated per app launch | abc123-def456-789012-ghijkl | Regenerated on each app launch |
| clientEventId | Unique UUID for each event (deduplication) | 550e8400-e29b-41d4-a716-446655440000 | Generated per event |
Device & Platform
| Field | Description | Example | Platform Availability | Source |
|-------|-------------|---------|----------------------|--------|
| platform | Platform identifier | ios, android, web | All | Capacitor.getPlatform() |
| osVersion | Device OS version | 17.2 (iOS)14 (Android API level)macOS 14.3 (Web) | All | @capacitor/device plugin(defaults to "unknown" if unavailable) |
| deviceModel | Device model name | iPhone15,2, SM-G998B, Pixel 8 | iOS, Android only | @capacitor/device plugin(not available on Web) |
| deviceManufacturer | Device manufacturer | Apple, Samsung, Google | iOS, Android only | @capacitor/device plugin(iOS always returns "Apple") |
| locale | User's locale/language setting | en-US, fr-FR, ja-JP | All | JavaScript Intl.DateTimeFormat().resolvedOptions().locale(from browser/device environment) |
| timezone | User's timezone | America/New_York, Europe/London, Asia/Tokyo | All | JavaScript Intl.DateTimeFormat().resolvedOptions().timeZone(from browser/device environment) |
App & Environment
| Field | Description | Example | Source |
|-------|-------------|---------|--------|
| appVersion | App version (if configured) | 1.2.0, 2.5.1 | Configuration option (recommended to set for install/update tracking) |
| environment | Environment name | production, staging, development | Configuration option (default: production) |
| sdk | SDK identifier | capacitor | Hardcoded by SDK |
| sdkVersion | SDK version | 0.2.0 | Package version (from package.json) |
Event Metadata
| Field | Description | Example | Purpose |
|-------|-------------|---------|---------|
| timestamp | ISO 8601 timestamp when event was tracked | 2024-01-15T10:30:00.000Z | Event ordering and time-based analysis |
Implementation Notes:
- All context fields are automatically collected during SDK initialization
- Device info (
deviceManufacturer,deviceModel,osVersion) is loaded asynchronously via@capacitor/deviceon startup - If
@capacitor/deviceis not installed:osVersiondefaults to"unknown"deviceModelanddeviceManufacturerare not included (iOS/Android only fields)- SDK continues to function normally with reduced context
localeandtimezoneare collected by the underlying JavaScript SDK from the browser/device environmentclientEventIdis unique per event and used for server-side deduplicationsessionIdis regenerated on each app launch; previous session context is not restored
Automatic Behavior
The SDK handles many tasks automatically to provide a seamless analytics experience. You don't need to manually manage any of these features—they work out of the box.
Identity Management
Anonymous Tracking:
- Events are tracked anonymously by default (no
userIdin context) untilidentify()is called - Each user automatically receives a unique session ID on app launch
User ID Persistence:
- User identity set via
identify()is saved to Capacitor Preferences - User ID is automatically restored on app restart (before any events are sent)
- Profile data (email, name) passed to
identify()are sent to the server but not persisted locally - If you need profile data on each launch, you must call
identify()with profile data again
Identity Reset:
- Calling
resetIdentity()clears the user ID from both memory and Preferences - Subsequent events will not include
userIduntilidentify()is called again - Useful for logout flows to ensure proper user separation
Session Management:
- New session ID (UUID) is generated on each app launch and persisted for the entire session
- Session ID is included in all events to group user activity within a session
- Previous session context is not restored—each app launch creates a fresh session with a new
sessionId - Session IDs are not persisted across app restarts (new UUID each launch)
Event Storage & Delivery
Event Persistence:
- Events are automatically saved to Capacitor Preferences (via
@capacitor/preferences) - Whole-queue serialization is deferred and coalesced so synchronous tracking bursts stay responsive
- Persisted events survive app restarts and crashes
- Events are only removed from storage after successful server acknowledgment
Fallback to Memory:
- If Preferences plugin is unavailable, events are stored in memory only
- Memory-only storage means events are lost on app restart
- Events tracked with memory storage include
$storage_type: "memory"property - SDK continues to function normally but without persistence
Batch Processing:
- Events are grouped into batches for efficient network usage
- Default: 100 events per batch (configurable via
maxBatchSize, range: 1-1000) - Batching reduces network overhead and battery usage
Automatic Flush Triggers:
- Periodic flush: Events are automatically sent every 30 seconds (configurable via
flushInterval) - Batch size flush: Events flush immediately when
maxBatchSizeis reached - Background flush: Events are automatically flushed when the app goes to background (iOS/Android only, requires
@capacitor/app) - Manual flush: You can trigger
flush()at any time
Network Reliability:
- Retry on failure: Failed network requests are retried with exponential backoff (2s, 4s, 8s, 16s, etc.)
- Rate limiting: Automatic exponential backoff when rate limited by the server (respects HTTP 429 responses)
- Event preservation: Events are preserved in storage until successfully sent (or storage limit is reached)
Storage Limits:
- Default: 10,000 max stored events (configurable via
maxStoredEvents) - When limit is reached, oldest events are dropped to make room for new events
- Prevents unbounded storage growth if device is offline for extended periods
Deduplication:
- Each event includes a unique
clientEventId(UUID) - Server uses
clientEventIdto prevent duplicate processing if events are accidentally sent multiple times - Ensures analytics accuracy even with network retries
Lifecycle Tracking
When trackAppLifecycleEvents is enabled (default: true) and @capacitor/app is installed, the SDK automatically tracks app lifecycle events.
Platform Behavior
| Platform | App Lifecycle Events | Automatic Background Flush | Implementation Details |
|----------|---------------------|---------------------------|------------------------|
| iOS | ✅ Full Support | ✅ Yes | Uses @capacitor/app appStateChange listener• $app_opened when app comes to foreground (isActive: true)• $app_backgrounded when app goes to background (isActive: false)• Background flush triggered on isActive: false state |
| Android | ✅ Full Support | ✅ Yes | Uses @capacitor/app appStateChange listener• $app_opened when app comes to foreground (isActive: true)• $app_backgrounded when app goes to background (isActive: false)• Background flush triggered on isActive: false state |
| Web | ⚠️ Limited | ❌ No | Limited to initial page load only• $app_opened only on initial page load• No background/foreground events (browser tabs don't provide reliable lifecycle APIs)• Relies on periodic flush interval (flushInterval) |
Deduplication:
- Lifecycle events that fire multiple times within 1 second are automatically deduplicated
- Prevents duplicate tracking when OS fires lifecycle events in rapid succession during state transitions
- Ensures clean, accurate lifecycle analytics
Missing Plugin Behavior:
If @capacitor/app is not installed:
- No lifecycle events (
$app_opened,$app_backgrounded) will be tracked - No automatic background flush (events flush only on periodic interval or manual
flush()) - SDK continues to function normally for manual event tracking
- On Web platform,
$app_openedstill fires once on initial page load
iOS & Android Lifecycle Details
Both platforms use the same @capacitor/app plugin which provides a unified appStateChange event:
App.addListener('appStateChange', ({ isActive }) => {
if (isActive) {
// App came to foreground
track('$app_opened');
} else {
// App went to background
track('$app_backgrounded');
flush(); // Ensure events are sent before app suspension
}
});When events fire:
- iOS:
isActive: falsewhen app enters background (user presses home, switches apps, or locks screen);isActive: truewhen app returns to foreground - Android:
isActive: falsewhen app enters background (user presses home, switches apps);isActive: truewhen app returns to foreground - Edge case handling: The 1-second deduplication window prevents duplicate events if the OS fires multiple rapid state changes
Web Lifecycle Details
Web browsers don't provide reliable page visibility or lifecycle APIs that match mobile app semantics. Therefore:
$app_openedfires only once on initial page load (when SDK is configured)- No subsequent lifecycle events fire when user switches tabs, minimizes browser, etc.
- Events are flushed on periodic interval only (no background flush trigger)
- Consider using the Page Visibility API manually if you need tab focus tracking
Install & Update Detection
When appVersion is configured and trackAppLifecycleEvents is enabled (both are defaults), the SDK automatically detects app installs and updates.
Install Detection
- Event:
$app_installed - When: First launch after install (determined by absence of stored app version in Preferences)
- Properties:
$version(current app version from configuration) - Requires:
appVersionconfiguration option
Update Detection
- Event:
$app_updated - When: First launch after version change (compares stored version with current
appVersionconfig) - Properties:
$version(new app version from configuration)$previous_version(old app version from Preferences)
- Requires:
appVersionconfiguration option
How It Works
- On first launch, no version is stored in Preferences →
$app_installedfires, version is saved - On subsequent launches, stored version is compared with
appVersionconfig:- If versions match → no event
- If versions differ →
$app_updatedfires, new version is saved
- If
appVersionis not configured, install/update tracking is completely disabled
Best Practice:
Always set appVersion in your configuration to enable install/update tracking:
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
appVersion: '1.0.0', // From your build system or package.json
});Context Collection
Automatic Context:
- Every event automatically includes platform, OS version, device info, locale, timezone, app version, environment, and more
- See Automatic Context for complete field reference
Device Info Collection:
- Device model, manufacturer, and detailed OS version are collected via
@capacitor/deviceat initialization time - Collection happens asynchronously to avoid blocking SDK initialization
- Gracefully degrades if plugin is unavailable (defaults to
"unknown"or omits fields)
Super Properties:
- Set persistent properties that are automatically included with every event
- See Super Properties for usage details
Dynamic Timestamps:
- Each event includes an ISO 8601 timestamp generated at the time
track()is called - Timestamp reflects when the event occurred, not when it was sent to the server
- Ensures accurate event ordering and time-based analysis
Failure handling
Native storage/device operations have a five-second deadline. Reads are coalesced; a timed-out native read uses memory for the rest of the process. Writes are coalesced to the latest value. A timed-out write cannot be cancelled, so that key also uses memory for the rest of the process rather than risking out-of-order durable writes. Failed identity/consent writes remain authoritative in memory. Unreadable or malformed native consent stays opted out until an explicit choice; a genuinely missing value uses the configured default for a new installation.
ready(timeoutMs) covers native initialization and experiment readiness together.
Before initialization, the SDK retains at most 10,000 calls and 1 MiB of owned
payload snapshots, dropping oldest calls on overflow. The event adapter caps its
combined cached and pending payloads at 1 MiB and drops new events that exceed its
remaining budget. Oversized, cyclic, unreadable, or excessively complex values are
discarded. Accepted data is copied so later app mutations cannot change queued
events. Persisted queues larger than 1 MiB are discarded before parsing; smaller
damaged queues recover valid entries. Count reads and pending clears are coalesced.
Call destroy() when tearing down the SDK. It releases readiness/flush waiters and
invalidates its event adapter, pending initialization, and lifecycle callbacks.
Late hydration cannot resurrect events cleared by a newer configuration. Repeated
privacy clears invalidate intervening queued stores. Logging and listener cleanup
contain bridge errors. flush() handles delivery errors internally and skips
initialization that has not completed within five seconds; resolution reports the
attempt finishing, not server acceptance.
Capacitor permits one outstanding lifecycle registration and waits for listener removal before registering another. Failed or indefinitely stalled removal disables further automatic lifecycle subscriptions for that process. Stale callbacks remain inert, and explicit tracking continues to work.
These guards cover SDK failures; they cannot prevent operating-system termination or crashes inside third-party native plugins.
Native Android host regression
The isolated native host fixture tests the built
Capacitor wrapper with the published JavaScript core installed by its lockfile.
CI runs real App, Device, and Preferences plugins in an Android WebView and checks
identity, consent, lifecycle capture, teardown, and host errors without analytics
traffic. Release verification must pass against that installed dependency graph.
MGM_JS_DIR remains an optional local override for an unpublished candidate core;
CI does not use it. Mocked regressions cover stalled and throwing native plugins.
Debug Logging
Enable debug logging to see detailed SDK activity in the console:
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
enableDebugLogging: true,
});When enabled, the SDK logs key operations to help with debugging and integration:
Example output:
[MostlyGoodMetrics] Configuring with options: {
apiKey: "mgm_proj_...",
appVersion: "1.0.0",
environment: "production",
...
}
[MostlyGoodMetrics] Setting up lifecycle tracking
[MostlyGoodMetrics] Device info loaded: {
model: "iPhone15,2",
manufacturer: "Apple",
osVersion: "17.2"
}
[MostlyGoodMetrics] Tracking lifecycle event: $app_opened
[MostlyGoodMetrics] Tracked event: button_clicked
[MostlyGoodMetrics] Flushing 5 events
[MostlyGoodMetrics] Successfully sent batch of 5 eventsWhat gets logged:
- SDK initialization and configuration
- Lifecycle tracking setup
- Device info collection results
- User identification calls
- Events tracked (name and properties)
- Flush operations (manual and automatic)
- Network requests and responses
- Errors and warnings
Best Practices:
- Enable debug logging during development to verify integration
- Disable in production to reduce console noise and improve performance
- Use environment-based configuration:
MostlyGoodMetrics.configure('mgm_proj_your_api_key', {
enableDebugLogging: process.env.NODE_ENV === 'development',
});License
MIT
