@mostly-good-metrics/javascript 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Mostly Good Metrics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  A lightweight JavaScript/TypeScript SDK for tracking analytics events with [MostlyGoodMetrics](https://mostlygoodmetrics.com).
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Requirements](#requirements)
8
+ - [Installation](#installation)
9
+ - [Quick Start](#quick-start)
10
+ - [User Identification](#user-identification)
11
+ - [Configuration Options](#configuration-options)
12
+ - [Automatic Events](#automatic-events)
13
+ - [Automatic Properties](#automatic-properties)
14
+ - [Automatic Context](#automatic-context)
15
+ - [Event Naming](#event-naming)
16
+ - [Properties](#properties)
17
+ - [Manual Flush](#manual-flush)
18
+ - [A/B Testing (Experiments)](#ab-testing-experiments)
19
+ - [Automatic Behavior](#automatic-behavior)
20
+ - [Debug Logging](#debug-logging)
21
+ - [Framework Integration](#framework-integration)
22
+ - [Custom Storage](#custom-storage)
23
+ - [License](#license)
24
+
5
25
  ## Requirements
6
26
 
7
27
  - Node.js 16+ (for build tools)
@@ -61,46 +81,44 @@ That's it! Events are automatically batched and sent.
61
81
 
62
82
  ## User Identification
63
83
 
64
- The SDK automatically generates and persists an anonymous `user_id` (UUID) for each user. This ID:
65
- - Is auto-generated on first visit
66
- - Persists across sessions (stored in cookies and localStorage)
67
- - Is included in every event as `user_id`
68
-
69
- When you call `identify()`, the identified user ID takes precedence over the anonymous ID.
84
+ 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.
70
85
 
71
86
  ```typescript
72
- // Before identify(): user_id = "550e8400-e29b-41d4-a716-446655440000" (auto-generated)
87
+ // Before identify(): user_id = auto-generated UUID (e.g., "550e8400-e29b-41d4-a716-446655440000")
73
88
  MostlyGoodMetrics.identify('user_123');
74
89
  // After identify(): user_id = "user_123"
75
90
 
76
91
  MostlyGoodMetrics.resetIdentity();
77
- // After reset: user_id = "550e8400-e29b-41d4-a716-446655440000" (back to anonymous)
92
+ // After reset: user_id = new auto-generated UUID
78
93
  ```
79
94
 
80
95
  ### Cross-Subdomain Tracking
81
96
 
82
- By default, the anonymous ID is stored in cookies (with localStorage fallback). To share the anonymous ID across subdomains:
97
+ To track users across subdomains (e.g., `app.example.com` and `blog.example.com`), configure a shared cookie domain:
83
98
 
84
99
  ```typescript
85
100
  MostlyGoodMetrics.configure({
86
101
  apiKey: 'mgm_proj_your_api_key',
87
- cookieDomain: '.yourdomain.com', // Share across all subdomains
102
+ cookieDomain: '.example.com', // Share ID across all *.example.com subdomains
88
103
  });
89
104
  ```
90
105
 
91
- This allows tracking the same user across `app.yourdomain.com`, `www.yourdomain.com`, etc.
92
-
93
- ### Privacy Mode (No Cookies)
106
+ ### Privacy Mode
94
107
 
95
- For GDPR compliance or privacy-focused applications, you can disable cookies entirely:
108
+ For privacy-focused implementations or GDPR compliance, you can disable cookies entirely:
96
109
 
97
110
  ```typescript
98
111
  MostlyGoodMetrics.configure({
99
112
  apiKey: 'mgm_proj_your_api_key',
100
- disableCookies: true, // Only use localStorage
113
+ disableCookies: true, // Uses localStorage only, no cookies
101
114
  });
102
115
  ```
103
116
 
117
+ When `disableCookies: true`:
118
+ - Anonymous ID stored in localStorage only
119
+ - No cookies are set
120
+ - Cross-subdomain tracking is disabled
121
+
104
122
  ## Configuration Options
105
123
 
106
124
  For more control, pass additional configuration:
@@ -116,6 +134,8 @@ MostlyGoodMetrics.configure({
116
134
  maxStoredEvents: 10000,
117
135
  enableDebugLogging: process.env.NODE_ENV === 'development',
118
136
  trackAppLifecycleEvents: true,
137
+ cookieDomain: '.yourdomain.com',
138
+ disableCookies: false,
119
139
  });
120
140
  ```
121
141
 
@@ -132,10 +152,11 @@ MostlyGoodMetrics.configure({
132
152
  | `trackAppLifecycleEvents` | `false` | Auto-track lifecycle events ($app_opened, etc.) |
133
153
  | `bundleId` | auto-detected | Custom bundle identifier |
134
154
  | `cookieDomain` | - | Cookie domain for cross-subdomain tracking (e.g., `.example.com`) |
135
- | `disableCookies` | `false` | Disable cookies, use only localStorage |
136
- | `anonymousId` | auto-generated | Override anonymous ID (for wrapper SDKs like React Native) |
137
- | `storage` | auto-detected | Custom storage adapter |
155
+ | `disableCookies` | `false` | Disable cookies entirely (uses localStorage only) |
156
+ | `anonymousId` | auto-generated | Override anonymous ID (for wrapper SDKs) |
157
+ | `storage` | auto-detected | Custom storage adapter (see [Custom Storage](#custom-storage)) |
138
158
  | `networkClient` | fetch-based | Custom network client |
159
+ | `experimentStorage` | auto-detected | Custom key-value storage for the experiments cache (see [A/B Testing](#ab-testing-experiments)) |
139
160
 
140
161
  ## Automatic Events
141
162
 
@@ -154,12 +175,54 @@ When `trackAppLifecycleEvents` is enabled, the SDK automatically tracks:
154
175
 
155
176
  The SDK automatically includes these properties with every event:
156
177
 
157
- | Property | Description |
158
- |----------|-------------|
159
- | `$device_type` | Device type (`desktop`, `phone`, `tablet`) |
160
- | `$device_model` | Browser name and version |
178
+ | Property | Description | Example | Source |
179
+ |----------|-------------|---------|--------|
180
+ | `$device_type` | Device form factor | `desktop`, `phone`, `tablet` | Detected from user agent and screen size |
181
+ | `$device_model` | Browser name and major version | `Chrome 120.0` | Parsed from user agent |
182
+ | `$browser` | Browser name | `Chrome`, `Safari`, `Firefox` | Parsed from user agent |
183
+ | `$browser_version` | Full browser version | `120.0.6099.130` | Parsed from user agent |
184
+ | `$os` | Operating system name | `Mac OS X`, `Windows`, `Linux`, `iOS`, `Android` | Parsed from user agent |
185
+ | `$os_version` | Operating system version | `10.15.7` | Parsed from user agent |
186
+ | `$screen_width` | Screen width in pixels | `1920` | `window.screen.width` |
187
+ | `$screen_height` | Screen height in pixels | `1080` | `window.screen.height` |
188
+ | `$viewport_width` | Viewport width in pixels | `1440` | `window.innerWidth` |
189
+ | `$viewport_height` | Viewport height in pixels | `900` | `window.innerHeight` |
190
+ | `$user_agent` | Full user agent string | `Mozilla/5.0...` | `navigator.userAgent` |
191
+
192
+ > **Note:** Properties with the `$` prefix are reserved for system use. Do not use the `$` prefix for your own custom properties.
193
+
194
+ ## Automatic Context
195
+
196
+ The SDK automatically includes these fields with every event to provide rich context:
197
+
198
+ ### Identity & Session
199
+
200
+ | Field | Description | Example | Persistence |
201
+ |-------|-------------|---------|-------------|
202
+ | `user_id` | Identified user ID (set via `identify()`) or auto-generated anonymous UUID | `user_123` | Persisted in cookies + localStorage (survives page reloads) |
203
+ | `session_id` | UUID generated per page load | `abc123-def456` | Regenerated on each page load |
204
+
205
+ ### Device & Platform
206
+
207
+ | Field | Description | Example | Source |
208
+ |-------|-------------|---------|--------|
209
+ | `platform` | Platform identifier | `web` | Hardcoded |
210
+ | `locale` | User's locale | `en-US` | `Intl.DateTimeFormat().resolvedOptions().locale` |
211
+ | `timezone` | User's timezone | `America/New_York` | `Intl.DateTimeFormat().resolvedOptions().timeZone` |
161
212
 
162
- Additionally, `osVersion` and `appVersion` (if configured) are included at the event level.
213
+ ### App & Environment
214
+
215
+ | Field | Description | Example | Source |
216
+ |-------|-------------|---------|--------|
217
+ | `app_version` | App version (if configured) | `1.2.0` | Configuration option |
218
+ | `environment` | Environment name | `production`, `staging`, `development` | Configuration option (default: `production`) |
219
+
220
+ ### Event Metadata
221
+
222
+ | Field | Description | Example | Purpose |
223
+ |-------|-------------|---------|---------|
224
+ | `client_event_id` | Unique UUID for each event | `550e8400-e29b-41d4-a716-446655440000` | Deduplication (prevents processing the same event twice) |
225
+ | `timestamp` | ISO 8601 timestamp when event was tracked | `2024-01-15T10:30:00.000Z` | Event ordering and time-based analysis |
163
226
 
164
227
  ## Event Naming
165
228
 
@@ -168,6 +231,8 @@ Event names must:
168
231
  - Contain only alphanumeric characters and underscores
169
232
  - Be 255 characters or less
170
233
 
234
+ > **Note:** The `$` prefix is reserved for system events (like `$app_opened`, `$app_installed`). Do not use `$` for custom event names.
235
+
171
236
  ```typescript
172
237
  // Valid
173
238
  MostlyGoodMetrics.track('button_clicked');
@@ -178,6 +243,7 @@ MostlyGoodMetrics.track('step_1_completed');
178
243
  MostlyGoodMetrics.track('123_event'); // starts with number
179
244
  MostlyGoodMetrics.track('event-name'); // contains hyphen
180
245
  MostlyGoodMetrics.track('event name'); // contains space
246
+ MostlyGoodMetrics.track('$custom_event'); // $ prefix is reserved
181
247
  ```
182
248
 
183
249
  ## Properties
@@ -218,20 +284,97 @@ const count = await MostlyGoodMetrics.getPendingEventCount();
218
284
  console.log(`${count} events pending`);
219
285
  ```
220
286
 
221
- ## Automatic Behavior
287
+ ## A/B Testing (Experiments)
288
+
289
+ Variants are assigned server-side (`GET /v1/experiments`) so the same user always gets the same variant for the same experiment.
290
+
291
+ ```typescript
292
+ // Wait for experiments to load (resolves immediately if cached).
293
+ // Resolves after 5 seconds at the latest so a hanging network never
294
+ // blocks startup - pass a custom timeout with ready(timeoutMs).
295
+ // ready() never rejects; if it times out, getVariant() returns fallbacks
296
+ // until the in-flight fetch completes (a late response is still applied).
297
+ await MostlyGoodMetrics.ready();
298
+
299
+ // Get the assigned variant, with an optional fallback for when the
300
+ // experiment is unknown or experiments haven't loaded yet (default: null)
301
+ const variant = MostlyGoodMetrics.getVariant('checkout-flow', 'control');
302
+
303
+ if (variant === 'treatment') {
304
+ // show the new checkout
305
+ }
306
+ ```
222
307
 
223
- The SDK automatically:
308
+ ### Caching (stale-while-revalidate, no TTL)
224
309
 
225
- - **Generates anonymous user ID** (UUID, persisted in cookies + localStorage)
226
- - **Persists events** to localStorage (with in-memory fallback)
227
- - **Batches events** for efficient network usage
228
- - **Flushes on interval** (default: every 30 seconds)
229
- - **Flushes on visibility change** when the tab is hidden
230
- - **Compresses payloads** using gzip for large batches (>1KB)
231
- - **Retries on failure** for network errors (events are preserved)
232
- - **Handles rate limiting** with exponential backoff
233
- - **Persists identified user ID** across page loads
234
- - **Generates session IDs** per page load
310
+ Assigned variants are cached locally per user and **never expire**:
311
+
312
+ - Cached variants are served synchronously as soon as they are hydrated; `ready()` resolves without waiting for the network.
313
+ - 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 a `ready()` timeout is still applied atomically.
314
+ - A background refetch keeps assignments up to date, throttled to at most once per hour (the last-fetch timestamp is persisted).
315
+ - On `identify()` with a changed user ID, the SDK keeps serving the current in-memory variants, refetches immediately (passing `anonymous_id` alongside `user_id` so the server can alias pre-identify assignments), and atomically swaps in the response. Variants are never cleared to null mid-session.
316
+
317
+ ### Exposure tracking
318
+
319
+ 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.
320
+
321
+ The variant is also stored as a super property (`$experiment_{name}: variant`, snake_cased) so it is attached to all subsequent events.
322
+
323
+ ### Custom experiment storage (React Native)
324
+
325
+ 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:
326
+
327
+ ```typescript
328
+ import AsyncStorage from '@react-native-async-storage/async-storage';
329
+ import { MostlyGoodMetrics, IExperimentStorage } from '@mostly-good-metrics/javascript';
330
+
331
+ const experimentStorage: IExperimentStorage = {
332
+ getItem: (key) => AsyncStorage.getItem(key),
333
+ setItem: (key, value) => AsyncStorage.setItem(key, value),
334
+ };
335
+
336
+ MostlyGoodMetrics.configure({
337
+ apiKey: 'mgm_proj_your_api_key',
338
+ experimentStorage,
339
+ });
340
+ ```
341
+
342
+ Async adapters are fully supported - cache hydration completes before `ready()` resolves.
343
+
344
+ ## Automatic Behavior
345
+
346
+ The SDK handles many tasks automatically to provide a seamless analytics experience:
347
+
348
+ ### Identity Management
349
+ - **Generates anonymous user ID**: Creates a persistent UUID on first visit, stored in cookies + localStorage
350
+ - **Persists identified user ID**: Stores user ID (from `identify()`) in cookies + localStorage, automatically restored on page reload
351
+ - **Generates session IDs**: Creates a new UUID on each page load to track user sessions
352
+
353
+ ### Event Storage & Delivery
354
+ - **Persists events**: Stores events in localStorage (with in-memory fallback if unavailable)
355
+ - **Batches events**: Groups events together for efficient network usage (default: 100 events per batch)
356
+ - **Flushes on interval**: Automatically sends events every 30 seconds (configurable via `flushInterval`)
357
+ - **Flushes on visibility change**: Sends pending events when tab is hidden or page unloads
358
+ - **Compresses payloads**: Large batches (>1KB) are automatically gzip compressed
359
+ - **Retries on failure**: Preserves events on network errors and retries with exponential backoff
360
+ - **Handles rate limiting**: Automatically backs off when server rate limits are hit
361
+ - **Adds deduplication IDs**: Includes unique `client_event_id` with each event to prevent duplicate processing
362
+ - **Offline support**: Events queue locally when offline and send when connectivity returns
363
+
364
+ ### Lifecycle Tracking
365
+ When `trackAppLifecycleEvents` is enabled, the SDK automatically:
366
+ - **Detects install**: Tracks `$app_installed` event on first visit (requires `appVersion` config)
367
+ - **Detects updates**: Tracks `$app_updated` event when app version changes (requires `appVersion` config)
368
+ - **Tracks page visibility**: Fires `$app_opened` when page becomes visible
369
+ - **Tracks page hidden**: Fires `$app_backgrounded` when page is hidden or unloads
370
+
371
+ ### Platform Integration
372
+ - **Detects device type**: Automatically identifies desktop, phone, or tablet from user agent and screen size
373
+ - **Captures browser info**: Includes browser name and version with every event
374
+ - **Captures OS info**: Includes operating system name and version with every event
375
+ - **Captures screen dimensions**: Includes screen and viewport size for UI analytics
376
+ - **Captures locale**: Includes user's language/region setting
377
+ - **Captures timezone**: Includes user's timezone for accurate time-based analysis
235
378
 
236
379
  ## Debug Logging
237
380
 
@@ -252,31 +395,10 @@ Output example:
252
395
  [MostlyGoodMetrics] [DEBUG] Successfully sent 5 events
253
396
  ```
254
397
 
255
- ## Custom Storage
256
-
257
- You can provide a custom storage adapter for environments where localStorage isn't available:
258
-
259
- ```typescript
260
- import { MostlyGoodMetrics, IEventStorage, InMemoryEventStorage } from '@mostly-good-metrics/javascript';
261
-
262
- // Use in-memory storage
263
- MostlyGoodMetrics.configure({
264
- apiKey: 'mgm_proj_your_api_key',
265
- storage: new InMemoryEventStorage(10000),
266
- });
267
-
268
- // Or implement your own
269
- class MyCustomStorage implements IEventStorage {
270
- async store(event: MGMEvent): Promise<void> { /* ... */ }
271
- async fetchEvents(limit: number): Promise<MGMEvent[]> { /* ... */ }
272
- async removeEvents(count: number): Promise<void> { /* ... */ }
273
- async eventCount(): Promise<number> { /* ... */ }
274
- async clear(): Promise<void> { /* ... */ }
275
- }
276
- ```
277
-
278
398
  ## Framework Integration
279
399
 
400
+ The SDK supports both JavaScript and TypeScript projects. All type definitions are included automatically.
401
+
280
402
  ### React
281
403
 
282
404
  ```typescript
@@ -345,19 +467,27 @@ import analytics from './plugins/analytics';
345
467
  app.use(analytics);
346
468
  ```
347
469
 
348
- ## TypeScript Support
470
+ ## Custom Storage
349
471
 
350
- Full TypeScript support with exported types:
472
+ You can provide a custom storage adapter for environments where localStorage isn't available:
351
473
 
352
474
  ```typescript
353
- import {
354
- MostlyGoodMetrics,
355
- MGMConfiguration,
356
- MGMEvent,
357
- EventProperties,
358
- IEventStorage,
359
- INetworkClient,
360
- } from '@mostly-good-metrics/javascript';
475
+ import { MostlyGoodMetrics, IEventStorage, InMemoryEventStorage } from '@mostly-good-metrics/javascript';
476
+
477
+ // Use in-memory storage
478
+ MostlyGoodMetrics.configure({
479
+ apiKey: 'mgm_proj_your_api_key',
480
+ storage: new InMemoryEventStorage(10000),
481
+ });
482
+
483
+ // Or implement your own
484
+ class MyCustomStorage implements IEventStorage {
485
+ async store(event: MGMEvent): Promise<void> { /* ... */ }
486
+ async fetchEvents(limit: number): Promise<MGMEvent[]> { /* ... */ }
487
+ async removeEvents(count: number): Promise<void> { /* ... */ }
488
+ async eventCount(): Promise<number> { /* ... */ }
489
+ async clear(): Promise<void> { /* ... */ }
490
+ }
361
491
  ```
362
492
 
363
493
  ## License