@mostly-good-metrics/javascript
v0.13.1
Published
JavaScript/TypeScript SDK for MostlyGoodMetrics - a lightweight analytics library for web applications
Maintainers
Readme
MostlyGoodMetrics JavaScript SDK
A lightweight JavaScript/TypeScript SDK for tracking analytics events with MostlyGoodMetrics.
Table of Contents
- Requirements
- Installation
- Quick Start
- User Identification
- Privacy
- Configuration Options
- Migrating an Existing Site
- Automatic Events
- Automatic Properties
- Automatic Context
- Event Naming
- Properties
- Manual Flush
- A/B Testing (Experiments)
- Local Experiment Enrollment
- Automatic Behavior
- Debug Logging
- Framework Integration
- Custom Storage
- License
Requirements
- Node.js 18+ (for build tools)
- Modern browser with ES2020 support, or Node.js runtime
Installation
npm install @mostly-good-metrics/javascriptOr with yarn:
yarn add @mostly-good-metrics/javascriptQuick Start
1. Initialize the SDK
Initialize once at app startup:
import { MostlyGoodMetrics } from '@mostly-good-metrics/javascript';
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
});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
// Set user identity (optional - anonymous ID is auto-generated)
MostlyGoodMetrics.identify('user_123');
// Reset identity (e.g., on logout)
MostlyGoodMetrics.resetIdentity();That's it! Events are automatically batched and sent.
User Identification
The SDK automatically generates and persists an anonymous user ID (UUID) for each user. This anonymous ID is stored in both cookies and localStorage, persisting across sessions.
// Before identify(): user_id = auto-generated UUID (e.g., "550e8400-e29b-41d4-a716-446655440000")
MostlyGoodMetrics.identify('user_123');
// After identify(): user_id = "user_123"
MostlyGoodMetrics.resetIdentity();
// After reset: user_id = new auto-generated UUIDWhen identify() sends a $identify event (i.e. when you pass profile data such as email or name), the event's properties include $anonymous_id set to the anonymous ID the user had before identify() was called. Its user_id is the newly-identified ID. This lets the backend link a user's earlier anonymous events to their identified ID. $anonymous_id is omitted when there is no distinct anonymous ID (e.g. the anonymous ID already equals the identified ID).
Cross-Subdomain Tracking
To track users across subdomains (e.g., app.example.com and blog.example.com), configure a shared cookie domain:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
cookieDomain: '.example.com', // Share ID across all *.example.com subdomains
});Privacy Mode
For privacy-focused implementations or GDPR compliance, you can disable cookies entirely:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
disableCookies: true, // Uses localStorage only, no cookies
});When disableCookies: true:
- Anonymous ID stored in localStorage only
- No cookies are set
- Cross-subdomain tracking is disabled
See the Privacy section for the full set of privacy controls (opt-out, Do Not Track, memory-only persistence, forget-me, and more).
Privacy
The SDK ships with a full set of privacy controls: user-level opt-out, browser privacy signals (Do Not Track / Global Privacy Control), anonymous ID rotation and a full "forget me", reduced data collection, and configurable persistence including a memory-only mode.
Opt-out / opt-in
MostlyGoodMetrics.optOut(); // Stop all tracking immediately
MostlyGoodMetrics.isOptedOut(); // => true
MostlyGoodMetrics.optIn(); // Resume trackingoptOut()immediately stops tracking:track(),identify()andflush()become no-ops, and no network requests (including experiment and local-experiment-config fetches) are made. In local enrollment mode,getVariant()still buckets from inline or cached configs, but no$experiment_exposureevents (or their dedup state) are recorded untiloptIn().- Any queued (unsent) events are purged on opt-out, so nothing tracked before the call is ever sent.
- The choice is persisted (localStorage + cookie, consistent with the anonymous ID) and survives page reloads. In
persistence: 'memory'mode nothing is written, so the choice only lasts for the page session. - An explicit
optIn()is also persisted and overridesoptedOutByDefaultand Do Not Track defaults on later visits.
Consent-first sites (optedOutByDefault)
For sites that must not track before consent, start opted out and opt the user in once they consent:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
optedOutByDefault: true,
persistence: 'memory', // optional: also avoid writing any IDs before consent
});
// Later, when the user consents:
MostlyGoodMetrics.optIn();A previously persisted optIn()/optOut() choice takes precedence over optedOutByDefault.
Tip:
optedOutByDefaultalone still persists an anonymous ID to localStorage/cookies. Combine it withpersistence: 'memory'if nothing may be written before consent, then calloptIn()after consent (note: with memory persistence the anonymous ID resets on every page load).
Do Not Track / Global Privacy Control (respectDoNotTrack)
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
respectDoNotTrack: true,
});When enabled, the SDK treats navigator.doNotTrack === '1' or navigator.globalPrivacyControl === true as opted out from initialization. Off by default. An explicit persisted optIn() takes precedence over the browser signal.
Resetting the anonymous ID / forget me
// Rotate just the anonymous ID (returns the new ID)
MostlyGoodMetrics.resetAnonymousId();
// Standard logout: clear the user ID, keep the anonymous ID
MostlyGoodMetrics.resetIdentity();
// Full "forget me": also rotates the anonymous ID and purges local state
MostlyGoodMetrics.resetIdentity({ clearAnonymousId: true });resetIdentity({ clearAnonymousId: true }) clears everything tied to the previous identity:
- User ID and identify debounce state
- Anonymous ID (rotated to a fresh one)
- Queued (unsent) events
- Super properties (including
$experiment_*assignments) - Cached experiment variants and exposure dedup flags
- Sticky local experiment assignments (local enrollment mode), so the new anonymous ID is re-bucketed -
resetAnonymousId()clears these too
Reduced data collection (collectDeviceProperties)
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
collectDeviceProperties: false,
});When false, the SDK omits $device_type and $device_model from event properties and locale/timezone from events and batch context. platform, os_version and app_version are still sent. Default: true.
Persistence modes (persistence)
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
persistence: 'memory', // 'localStorage+cookie' (default) | 'localStorage' | 'memory'
});| Mode | Anonymous ID | Event queue | Cookies | Survives reload |
|------|--------------|-------------|---------|-----------------|
| 'localStorage+cookie' (default) | localStorage + cookie | localStorage | Yes (enables cross-subdomain tracking) | Yes |
| 'localStorage' | localStorage only | localStorage | No | Yes |
| 'memory' | In-memory only | In-memory only | No | No |
In 'memory' mode nothing is written to localStorage or cookies: the anonymous ID, event queue, super properties, opt-out flag and experiment cache all live in memory and are lost on reload. disableCookies: true keeps working as an alias for persistence: 'localStorage'; an explicit persistence option wins over disableCookies.
What the SDK collects automatically
With default settings, every event includes:
- Identity:
user_id(identified ID or generated anonymous ID),session_id(per page load) - Device (disable with
collectDeviceProperties: false):$device_type,$device_model,locale,timezone - App/environment:
platform,os_version,app_version(if configured),environment,$sdk - Metadata:
client_event_id,timestamp
See Automatic Properties and Automatic Context for the full reference. The SDK never reads or transmits anything beyond what is listed there plus the properties you pass explicitly.
Configuration Options
For more control, pass additional configuration:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
baseURL: 'https://ingest.mostlygoodmetrics.com',
environment: 'production',
appVersion: '1.0.0',
maxBatchSize: 100,
flushInterval: 30,
maxStoredEvents: 10000,
enableDebugLogging: process.env.NODE_ENV === 'development',
trackAppLifecycleEvents: true,
trackPageViews: true,
cookieDomain: '.yourdomain.com',
disableCookies: false,
});| Option | Default | Description |
|--------|---------|-------------|
| apiKey | - | Required. Your 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 console output |
| trackAppLifecycleEvents | false | Auto-track lifecycle events ($app_opened, etc.) |
| trackPageViews | false | Capture page views, SPA navigation, and active page engagement for Web Analytics |
| sessionTimeoutMinutes | 30 | Start a new web session after this many inactive minutes |
| existingInstallation | false | Establish lifecycle state without a migration-time $app_installed |
| contextProvider | - | Dynamic properties evaluated when each event is captured |
| bundleId | auto-detected | Custom bundle identifier |
| cookieDomain | - | Cookie domain for cross-subdomain tracking (e.g., .example.com) |
| disableCookies | false | Disable cookies entirely (alias for persistence: 'localStorage') |
| persistence | 'localStorage+cookie' | Where SDK state lives: 'localStorage+cookie', 'localStorage', or 'memory' (see Privacy) |
| optedOutByDefault | false | Start opted out until optIn() is called (consent-first sites) |
| respectDoNotTrack | false | Honor navigator.doNotTrack / Global Privacy Control as opt-out |
| collectDeviceProperties | true | Collect $device_type, $device_model, locale, timezone |
| anonymousId | auto-generated | Override anonymous ID (for wrapper SDKs) |
| storage | auto-detected | Custom storage adapter (see Custom Storage) |
| networkClient | fetch-based | Custom network client |
| experimentStorage | auto-detected | Custom key-value storage for the experiments cache (see A/B Testing) |
| experimentMode | 'server' | 'server' (server-assigned variants) or 'local' (on-device bucketing, see Local Experiment Enrollment) |
| localExperiments | - | Inline experiment configs for local mode (zero network) |
Migrating an Existing Site
When adding MGM to an already-deployed site, set existingInstallation from a
legacy marker that proves this browser/device existed before MGM. MGM records
the current appVersion as its lifecycle baseline without treating those users
as fresh installs:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
appVersion: '2.0.0',
trackAppLifecycleEvents: true,
existingInstallation: legacyAnalytics.hasInstallationMarker(),
});Future version changes still emit $app_updated. Do not set this to true for
every visitor: doing so would suppress $app_installed for genuine new users.
Automatic Events
For website analytics, opt in once during configuration:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
trackPageViews: true,
});Page views and visible-page engagement are stored in the SDK's durable browser
queue before delivery. When a page is hidden or closed, MGM also starts a
byte-bounded fetch keepalive request. Browsers treat teardown delivery as
best-effort; anything they do not send remains queued for the next visit.
This captures an initial page_view, subsequent pushState, replaceState,
and back/forward navigation, plus $page_engagement when the page becomes
hidden. Page views include url, hostname, pathname, title, referrer,
referring_domain, and any utm_source, utm_medium, utm_campaign,
utm_term, or utm_content values. A session continues across reloads and
navigation until 30 minutes of inactivity (configurable with
sessionTimeoutMinutes).
When trackAppLifecycleEvents is enabled, the SDK automatically tracks:
| Event | When | Properties |
|-------|------|------------|
| $app_installed | First visit (localStorage) | $version |
| $app_updated | Version change detected | $version, $previous_version |
| $app_opened | Page load / tab visible | - |
| $app_backgrounded | Tab hidden / page unload | - |
Note: Install and update detection require
appVersionto be configured.
Automatic Properties
The SDK automatically includes these properties with every event:
| Property | Description | Example | Source |
|----------|-------------|---------|--------|
| $device_type | Device form factor | desktop, phone, tablet | Detected from user agent and screen size |
| $device_model | Browser name and major version | Chrome 120.0 | Parsed from user agent |
| $browser | Browser name | Chrome, Safari, Firefox | Parsed from user agent |
| $browser_version | Full browser version | 120.0.6099.130 | Parsed from user agent |
| $os | Operating system name | Mac OS X, Windows, Linux, iOS, Android | Parsed from user agent |
| $os_version | Operating system version | 10.15.7 | Parsed from user agent |
| $screen_width | Screen width in pixels | 1920 | window.screen.width |
| $screen_height | Screen height in pixels | 1080 | window.screen.height |
| $viewport_width | Viewport width in pixels | 1440 | window.innerWidth |
| $viewport_height | Viewport height in pixels | 900 | window.innerHeight |
| $user_agent | Full user agent string | Mozilla/5.0... | navigator.userAgent |
Note: Properties with the
$prefix are reserved for system use. Do not use the$prefix for your own custom properties.
Automatic Context
The SDK automatically includes these fields with every event to provide rich context:
Identity & Session
| Field | Description | Example | Persistence |
|-------|-------------|---------|-------------|
| user_id | Identified user ID (set via identify()) or auto-generated anonymous UUID | user_123 | Persisted in cookies + localStorage (survives page reloads) |
| session_id | UUID generated per page load | abc123-def456 | Regenerated on each page load |
Device & Platform
| Field | Description | Example | Source |
|-------|-------------|---------|--------|
| platform | Platform identifier | web | Hardcoded |
| locale | User's locale | en-US | Intl.DateTimeFormat().resolvedOptions().locale |
| timezone | User's timezone | America/New_York | Intl.DateTimeFormat().resolvedOptions().timeZone |
App & Environment
| Field | Description | Example | Source |
|-------|-------------|---------|--------|
| app_version | App version (if configured) | 1.2.0 | Configuration option |
| 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 |
Event Naming
Event names must:
- Start with a letter (or
$for system events) - Contain only alphanumeric characters and underscores
- Be 255 characters or less
Note: The
$prefix is reserved for system events (like$app_opened,$app_installed). Do not use$for custom event names.
// 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
MostlyGoodMetrics.track('$custom_event'); // $ prefix is reservedProperties
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',
},
});Dynamic global properties
Use contextProvider for values that can change while a page is open. The
provider runs for every event and its result is never persisted:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
contextProvider: () => ({
organization_id: currentOrganization.id,
subscription_tier: currentUser.plan,
}),
});The provider runs synchronously on the JavaScript caller. Return ordinary JSON-like values; asynchronous providers are unsupported. MGM contains provider exceptions and observes rejected callback promises so they do not become unhandled rejections. Unreadable property getters are omitted, and property traversal is bounded to prevent oversized or recursive arrays from exhausting capture work. Properties are copied when captured, including super properties, so later mutation does not change queued events.
Collision precedence is: super properties < dynamic context < event properties
< MGM system properties. $-prefixed keys are MGM-reserved; with
enableDebugLogging: true, MGM warns when custom event/context properties use
those keys.
Limits:
- String values: truncated to 1000 characters
- Nesting depth: max 3 levels
- Total properties size: max 10KB
Manual Flush
Events are automatically flushed periodically and when the page is hidden. You can also trigger a manual flush:
await MostlyGoodMetrics.flush();Automatic timer and page-exit flushes contain storage/network failures and keep
unsent events queued for retry. Explicitly awaited flush(),
clearPendingEvents(), and getPendingEventCount() retain their promise failure
behavior for custom adapters; handle those promises in your application. SDK
error handlers may return a promise; MGM observes its rejection, but does not
wait for asynchronous error handling before continuing delivery.
Built-in event stores also cap queued event JSON at approximately 1 MiB. The SDK
evicts the oldest events when either this byte ceiling or maxStoredEvents is
reached. An individual event larger than the ceiling is dropped. Oversized
persisted queues are ignored before parsing so recovery cannot exhaust the host
heap. Truncated property strings are copied so they cannot retain a much larger
source string. Stored super properties, experiment caches and supported streamed
experiment responses are also checked before parsing. Older fetch adapters without
readable body streams own the allocation needed to produce their text/JSON
responses. Custom event storage adapters own their own memory and persistence
limits.
To check pending events:
const count = await MostlyGoodMetrics.getPendingEventCount();
console.log(`${count} events pending`);A/B Testing (Experiments)
Variants are assigned server-side (GET /v1/experiments) so the same user always gets the same variant for the same experiment.
// Wait for experiments to load (resolves immediately if cached).
// Resolves after 5 seconds at the latest so a hanging network never
// blocks startup - pass a custom timeout with ready(timeoutMs).
// ready() never rejects; if it times out, getVariant() returns fallbacks
// until the in-flight fetch completes (a late response is still applied).
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
}Caching (stale-while-revalidate, no TTL)
Assigned variants are cached locally per user and never expire:
- Cached variants are served synchronously as soon as they are hydrated;
ready()resolves without waiting for the network. - On a cold cache,
ready(timeoutMs = 5000)resolves when the first experiments load settles or the timeout elapses, whichever comes first (it never rejects). The underlying request is aborted after 60 seconds so it always settles; a response arriving after aready()timeout is still applied atomically. - A background refetch keeps assignments up to date, throttled to at most once per hour (the last-fetch timestamp is persisted).
- On
identify()with a changed user ID, the SDK keeps serving the current in-memory variants, refetches immediately (passinganonymous_idalongsideuser_idso the server can alias pre-identify assignments), and atomically swaps in the response. Variants are never cleared to null mid-session.
Exposure tracking
On the first getVariant() hit per (user, experiment, variant), the SDK automatically tracks a $experiment_exposure event with properties $experiment_name (the raw experiment name) and $variant. Deduplication flags are persisted, so exposures are not re-tracked across page reloads or app restarts.
The variant is also stored as a super property ($experiment_{name}: variant, snake_cased) so it is attached to all subsequent events.
Custom experiment storage (React Native)
The experiments cache and exposure flags are persisted through a small pluggable key-value interface (IExperimentStorage). By default the SDK uses localStorage (or in-memory storage where unavailable). React Native apps should inject AsyncStorage:
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MostlyGoodMetrics, IExperimentStorage } from '@mostly-good-metrics/javascript';
const experimentStorage: IExperimentStorage = {
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
};
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
experimentStorage,
});Async adapters are fully supported - cache hydration completes before ready() resolves.
Local Experiment Enrollment
By default, experiment variants are assigned by the server (experimentMode: 'server'), which sends the user ID with the experiments request. If you prefer that no user identifier ever leaves the device for experiment enrollment, switch to local mode:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
experimentMode: 'local',
});
await MostlyGoodMetrics.ready();
const variant = MostlyGoodMetrics.getVariant('button-color', 'control');In local mode the SDK fetches only the experiment configurations (GET /v1/experiments/configs - a list of { id, name, variants } with no user-specific data; the request carries no user_id or anonymous_id), and assigns the variant on-device.
Inline mode (zero network)
Provide the configurations inline and the SDK makes no experiments network request at all:
MostlyGoodMetrics.configure({
apiKey: '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'],
},
],
});The bucketing algorithm
Local assignment is deterministic and identical across all MGM SDKs (Swift, Android, Flutter, JavaScript):
bucket = first 8 bytes of SHA-256(utf8("<experiment_uuid>:<user_id>"))
interpreted as an unsigned big-endian 64-bit integer
variant = variants[bucket % variants.length]user_id is the identified user ID if identify() has been called, otherwise the anonymous ID. Because the bucket value exceeds Number.MAX_SAFE_INTEGER, the SDK computes it with BigInt. SHA-256 is computed by a small vendored synchronous implementation (no dependencies; works in browsers, Node.js, React Native and Capacitor webviews, where WebCrypto's async crypto.subtle is not universally available). The implementation is locked down by golden-vector tests shared across all SDKs.
Stickiness
The first variant resolved for an experiment is persisted (keyed by experiment UUID) and reused on subsequent getVariant() calls and across restarts. identify() never re-buckets: a user who was assigned control anonymously keeps control after logging in, even though their effective user ID changed. A persisted variant is only discarded if it no longer exists in the experiment's variants list, or when the identity is reset via resetAnonymousId() or the forget-me resetIdentity({ clearAnonymousId: true }) (see Privacy) - a fresh identity is re-bucketed.
Exposure tracking
Unchanged from server mode: 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), with the raw experiment name.
Caveats
- No cross-device consistency for anonymous users: bucketing keys on the local effective user ID, and there is no server-side alias resolution. A user who is anonymous on one device and identified on another may see different variants until they are identified everywhere (identified users get consistent variants on every device, for experiments first evaluated after login).
- Config changes: reordering or removing variants changes assignments for new users (existing sticky assignments are kept while the variant still exists).
Automatic Behavior
The SDK handles many tasks automatically to provide a seamless analytics experience:
Identity Management
- Generates anonymous user ID: Creates a persistent UUID on first visit, stored in cookies + localStorage
- Persists identified user ID: Stores user ID (from
identify()) in cookies + localStorage, automatically restored on page reload - Generates session IDs: Creates a new UUID on each page load to track user sessions
Event Storage & Delivery
- Persists events: Stores events in localStorage (with in-memory fallback if unavailable)
- Batches events: Groups events together for efficient network usage (default: 100 events per batch)
- Flushes on interval: Automatically sends events every 30 seconds (configurable via
flushInterval) - Flushes on visibility change: Sends pending events when tab is hidden or page unloads
- Compresses payloads: Large batches (>1KB) are automatically gzip compressed
- Retries on failure: Preserves events on network errors and retries with exponential backoff
- Handles rate limiting: Automatically backs off when server rate limits are hit
- Adds deduplication IDs: Includes unique
client_event_idwith each event to prevent duplicate processing - Offline support: Events queue locally when offline and send when connectivity returns
reset() and destroy() stop SDK timers and abort SDK-owned requests. Page-view
capture remains best effort when browser metadata or hooks are unavailable;
SDK capture failures do not interrupt native navigation. MGM restores only its
own history wrappers and preserves router hooks installed afterward. Repeated
identity refreshes cancel the previous experiment request, and late responses
cannot replace assignments for a newer identity.
Lifecycle Tracking
When trackAppLifecycleEvents is enabled, the SDK automatically:
- Detects install: Tracks
$app_installedevent on first visit (requiresappVersionconfig) - Detects updates: Tracks
$app_updatedevent when app version changes (requiresappVersionconfig) - Tracks page visibility: Fires
$app_openedwhen page becomes visible - Tracks page hidden: Fires
$app_backgroundedwhen page is hidden or unloads
Platform Integration
- Detects device type: Automatically identifies desktop, phone, or tablet from user agent and screen size
- Captures browser info: Includes browser name and version with every event
- Captures OS info: Includes operating system name and version with every event
- Captures screen dimensions: Includes screen and viewport size for UI analytics
- Captures locale: Includes user's language/region setting
- Captures timezone: Includes user's timezone for accurate time-based analysis
Debug Logging
Enable debug logging to see SDK activity:
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
enableDebugLogging: true,
});Output example:
[MostlyGoodMetrics] [INFO] MostlyGoodMetrics initialized with environment: production
[MostlyGoodMetrics] [DEBUG] Tracking event: button_clicked
[MostlyGoodMetrics] [DEBUG] Starting flush
[MostlyGoodMetrics] [DEBUG] Successfully sent 5 eventsFramework Integration
The SDK supports both JavaScript and TypeScript projects. All type definitions are included automatically.
React
// src/analytics.ts
import { MostlyGoodMetrics } from '@mostly-good-metrics/javascript';
export function initAnalytics() {
MostlyGoodMetrics.configure({
apiKey: process.env.REACT_APP_MGM_API_KEY!,
environment: process.env.NODE_ENV,
appVersion: process.env.REACT_APP_VERSION,
});
}
// src/index.tsx
import { initAnalytics } from './analytics';
initAnalytics();Next.js
// lib/analytics.ts
import { MostlyGoodMetrics } from '@mostly-good-metrics/javascript';
export function initAnalytics() {
if (typeof window !== 'undefined') {
MostlyGoodMetrics.configure({
apiKey: process.env.NEXT_PUBLIC_MGM_API_KEY!,
environment: process.env.NODE_ENV,
});
}
}
// app/layout.tsx or pages/_app.tsx
'use client';
import { useEffect } from 'react';
import { initAnalytics } from '@/lib/analytics';
export default function RootLayout({ children }) {
useEffect(() => {
initAnalytics();
}, []);
return <html>...</html>;
}Vue
// src/plugins/analytics.ts
import { MostlyGoodMetrics } from '@mostly-good-metrics/javascript';
export default {
install() {
MostlyGoodMetrics.configure({
apiKey: import.meta.env.VITE_MGM_API_KEY,
environment: import.meta.env.MODE,
});
}
};
// src/main.ts
import analytics from './plugins/analytics';
app.use(analytics);Custom Storage
You can provide a custom storage adapter for environments where localStorage isn't available:
import { MostlyGoodMetrics, IEventStorage, InMemoryEventStorage } from '@mostly-good-metrics/javascript';
// Use in-memory storage
MostlyGoodMetrics.configure({
apiKey: 'mgm_proj_your_api_key',
storage: new InMemoryEventStorage(10000),
});
// Or implement your own
class MyCustomStorage implements IEventStorage {
async store(event: MGMEvent): Promise<void> { /* ... */ }
async fetchEvents(limit: number): Promise<MGMEvent[]> { /* ... */ }
// clientEventIds contains the stable IDs of the successfully sent events.
// Prefer removing by ID so concurrent queue changes cannot remove unsent events.
async removeEvents(count: number, clientEventIds?: string[]): Promise<void> { /* ... */ }
async eventCount(): Promise<number> { /* ... */ }
async clear(): Promise<void> { /* ... */ }
}License
MIT
