npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mostly-good-metrics/capacitor

v0.5.1

Published

Capacitor SDK for MostlyGoodMetrics analytics

Readme

MostlyGoodMetrics Capacitor SDK

The official Capacitor SDK for MostlyGoodMetrics analytics.

Table of Contents

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/app plugin's appStateChange listener
  • $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/app plugin's appStateChange listener
  • $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_opened on 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/app plugin has no effect on Web platform

Device Information:

  • deviceModel: Not available (Web browsers don't expose device model)
  • deviceManufacturer: Not available
  • osVersion: 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/core is 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/capacitor

Or with yarn:

yarn add @mostly-good-metrics/capacitor

Capacitor Peer Dependencies

The SDK requires several Capacitor plugins to function properly:

npm install @capacitor/core @capacitor/app @capacitor/device @capacitor/preferences

Then sync your native projects:

npx cap sync

Dependency 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/core is 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 userId in 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 (no userId in 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 tracking
  • optOut() 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 mode getVariant() still buckets from inline/cached configs, but exposure events wait until optIn().
  • The choice is persisted in Capacitor Preferences and restored during configure(); the initial $app_opened lifecycle event waits for that restore, so opted-out launches stay silent.
  • An explicit optIn() is persisted too and overrides optedOutByDefault on 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 respectDoNotTrack and persistence options 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 space

Note: 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/javascript core release with experiment support at runtime; on older cores getVariant() 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] where bucket is the first 8 bytes of SHA-256(utf8("<experiment_uuid>:<user_id>")) as an unsigned big-endian 64-bit integer. user_id is 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 by resetAnonymousId() and the forget-me resetIdentity({ clearAnonymousId: true }) (see Privacy).
  • Exposure events unchanged: $experiment_exposure with 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 appVersion to 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/device on startup
  • If @capacitor/device is not installed:
    • osVersion defaults to "unknown"
    • deviceModel and deviceManufacturer are not included (iOS/Android only fields)
    • SDK continues to function normally with reduced context
  • locale and timezone are collected by the underlying JavaScript SDK from the browser/device environment
  • clientEventId is unique per event and used for server-side deduplication
  • sessionId is 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 userId in context) until identify() 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 userId until identify() 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 maxBatchSize is 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 clientEventId to 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_opened still 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: false when app enters background (user presses home, switches apps, or locks screen); isActive: true when app returns to foreground
  • Android: isActive: false when app enters background (user presses home, switches apps); isActive: true when 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_opened fires 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: appVersion configuration option

Update Detection

  • Event: $app_updated
  • When: First launch after version change (compares stored version with current appVersion config)
  • Properties:
    • $version (new app version from configuration)
    • $previous_version (old app version from Preferences)
  • Requires: appVersion configuration option

How It Works

  1. On first launch, no version is stored in Preferences → $app_installed fires, version is saved
  2. On subsequent launches, stored version is compared with appVersion config:
    • If versions match → no event
    • If versions differ → $app_updated fires, new version is saved
  3. If appVersion is 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/device at 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 events

What 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