@mostly-good-metrics/javascript 0.1.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/README.md +319 -0
- package/dist/cjs/client.js +416 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/index.js +65 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/logger.js +64 -0
- package/dist/cjs/logger.js.map +1 -0
- package/dist/cjs/network.js +192 -0
- package/dist/cjs/network.js.map +1 -0
- package/dist/cjs/storage.js +227 -0
- package/dist/cjs/storage.js.map +1 -0
- package/dist/cjs/types.js +70 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils.js +249 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/esm/client.js +412 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/index.js +40 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/logger.js +55 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/network.js +187 -0
- package/dist/esm/network.js.map +1 -0
- package/dist/esm/storage.js +221 -0
- package/dist/esm/storage.js.map +1 -0
- package/dist/esm/types.js +66 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils.js +236 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/types/client.d.ts +126 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/logger.d.ts +37 -0
- package/dist/types/logger.d.ts.map +1 -0
- package/dist/types/network.d.ts +28 -0
- package/dist/types/network.d.ts.map +1 -0
- package/dist/types/storage.d.ts +76 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/types.d.ts +279 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils.d.ts +48 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +68 -0
- package/src/client.test.ts +346 -0
- package/src/client.ts +510 -0
- package/src/index.ts +79 -0
- package/src/logger.ts +63 -0
- package/src/network.ts +230 -0
- package/src/storage.test.ts +175 -0
- package/src/storage.ts +249 -0
- package/src/types.ts +347 -0
- package/src/utils.test.ts +239 -0
- package/src/utils.ts +315 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { logger, setDebugLogging } from './logger';
|
|
2
|
+
import { createDefaultNetworkClient } from './network';
|
|
3
|
+
import { createDefaultStorage, persistence } from './storage';
|
|
4
|
+
import {
|
|
5
|
+
EventProperties,
|
|
6
|
+
IEventStorage,
|
|
7
|
+
INetworkClient,
|
|
8
|
+
MGMConfiguration,
|
|
9
|
+
MGMEvent,
|
|
10
|
+
MGMEventContext,
|
|
11
|
+
MGMEventsPayload,
|
|
12
|
+
ResolvedConfiguration,
|
|
13
|
+
SystemEvents,
|
|
14
|
+
SystemProperties,
|
|
15
|
+
} from './types';
|
|
16
|
+
import {
|
|
17
|
+
delay,
|
|
18
|
+
detectDeviceType,
|
|
19
|
+
detectPlatform,
|
|
20
|
+
generateUUID,
|
|
21
|
+
getDeviceModel,
|
|
22
|
+
getISOTimestamp,
|
|
23
|
+
getOSVersion,
|
|
24
|
+
resolveConfiguration,
|
|
25
|
+
sanitizeProperties,
|
|
26
|
+
validateEventName,
|
|
27
|
+
} from './utils';
|
|
28
|
+
|
|
29
|
+
const FLUSH_DELAY_MS = 100; // Delay between batch sends
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Main client for MostlyGoodMetrics.
|
|
33
|
+
* Use the static `configure` method to initialize, then use static methods or the instance.
|
|
34
|
+
*/
|
|
35
|
+
export class MostlyGoodMetrics {
|
|
36
|
+
private static instance: MostlyGoodMetrics | null = null;
|
|
37
|
+
|
|
38
|
+
private config: ResolvedConfiguration;
|
|
39
|
+
private storage: IEventStorage;
|
|
40
|
+
private networkClient: INetworkClient;
|
|
41
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
42
|
+
private isFlushingInternal = false;
|
|
43
|
+
private sessionIdValue: string;
|
|
44
|
+
private lifecycleSetup = false;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Private constructor - use `configure` to create an instance.
|
|
48
|
+
*/
|
|
49
|
+
private constructor(config: MGMConfiguration) {
|
|
50
|
+
this.config = resolveConfiguration(config);
|
|
51
|
+
this.sessionIdValue = generateUUID();
|
|
52
|
+
|
|
53
|
+
// Set up logging
|
|
54
|
+
setDebugLogging(this.config.enableDebugLogging);
|
|
55
|
+
|
|
56
|
+
// Initialize storage
|
|
57
|
+
this.storage = this.config.storage ?? createDefaultStorage(this.config.maxStoredEvents);
|
|
58
|
+
|
|
59
|
+
// Initialize network client
|
|
60
|
+
this.networkClient = this.config.networkClient ?? createDefaultNetworkClient();
|
|
61
|
+
|
|
62
|
+
logger.info(`MostlyGoodMetrics initialized with environment: ${this.config.environment}`);
|
|
63
|
+
|
|
64
|
+
// Start auto-flush timer
|
|
65
|
+
this.startFlushTimer();
|
|
66
|
+
|
|
67
|
+
// Set up lifecycle tracking
|
|
68
|
+
if (this.config.trackAppLifecycleEvents) {
|
|
69
|
+
this.setupLifecycleTracking();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configure and initialize the SDK.
|
|
75
|
+
* Returns the singleton instance.
|
|
76
|
+
*/
|
|
77
|
+
static configure(config: MGMConfiguration): MostlyGoodMetrics {
|
|
78
|
+
if (!config.apiKey) {
|
|
79
|
+
throw new Error('API key is required');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (MostlyGoodMetrics.instance) {
|
|
83
|
+
logger.warn('MostlyGoodMetrics.configure called multiple times. Using existing instance.');
|
|
84
|
+
return MostlyGoodMetrics.instance;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
MostlyGoodMetrics.instance = new MostlyGoodMetrics(config);
|
|
88
|
+
return MostlyGoodMetrics.instance;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the shared instance, or null if not configured.
|
|
93
|
+
*/
|
|
94
|
+
static get shared(): MostlyGoodMetrics | null {
|
|
95
|
+
return MostlyGoodMetrics.instance;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if the SDK has been configured.
|
|
100
|
+
*/
|
|
101
|
+
static get isConfigured(): boolean {
|
|
102
|
+
return MostlyGoodMetrics.instance !== null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Reset the SDK (primarily for testing).
|
|
107
|
+
*/
|
|
108
|
+
static reset(): void {
|
|
109
|
+
if (MostlyGoodMetrics.instance) {
|
|
110
|
+
MostlyGoodMetrics.instance.destroy();
|
|
111
|
+
MostlyGoodMetrics.instance = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================
|
|
116
|
+
// Static convenience methods (delegate to shared instance)
|
|
117
|
+
// ============================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Track an event with the given name and optional properties.
|
|
121
|
+
*/
|
|
122
|
+
static track(name: string, properties?: EventProperties): void {
|
|
123
|
+
MostlyGoodMetrics.instance?.track(name, properties);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Identify the current user.
|
|
128
|
+
*/
|
|
129
|
+
static identify(userId: string): void {
|
|
130
|
+
MostlyGoodMetrics.instance?.identify(userId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Reset user identity.
|
|
135
|
+
*/
|
|
136
|
+
static resetIdentity(): void {
|
|
137
|
+
MostlyGoodMetrics.instance?.resetIdentity();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Flush pending events to the server.
|
|
142
|
+
*/
|
|
143
|
+
static flush(): Promise<void> {
|
|
144
|
+
return MostlyGoodMetrics.instance?.flush() ?? Promise.resolve();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Start a new session.
|
|
149
|
+
*/
|
|
150
|
+
static startNewSession(): void {
|
|
151
|
+
MostlyGoodMetrics.instance?.startNewSession();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Clear all pending events.
|
|
156
|
+
*/
|
|
157
|
+
static clearPendingEvents(): Promise<void> {
|
|
158
|
+
return MostlyGoodMetrics.instance?.clearPendingEvents() ?? Promise.resolve();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get the count of pending events.
|
|
163
|
+
*/
|
|
164
|
+
static getPendingEventCount(): Promise<number> {
|
|
165
|
+
return MostlyGoodMetrics.instance?.getPendingEventCount() ?? Promise.resolve(0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================
|
|
169
|
+
// Instance properties
|
|
170
|
+
// ============================================================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the current user ID.
|
|
174
|
+
*/
|
|
175
|
+
get userId(): string | null {
|
|
176
|
+
return persistence.getUserId();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the current session ID.
|
|
181
|
+
*/
|
|
182
|
+
get sessionId(): string {
|
|
183
|
+
return this.sessionIdValue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if a flush operation is in progress.
|
|
188
|
+
*/
|
|
189
|
+
get isFlushing(): boolean {
|
|
190
|
+
return this.isFlushingInternal;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get the resolved configuration.
|
|
195
|
+
*/
|
|
196
|
+
get configuration(): ResolvedConfiguration {
|
|
197
|
+
return { ...this.config };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================
|
|
201
|
+
// Instance methods
|
|
202
|
+
// ============================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Track an event with the given name and optional properties.
|
|
206
|
+
*/
|
|
207
|
+
track(name: string, properties?: EventProperties): void {
|
|
208
|
+
try {
|
|
209
|
+
validateEventName(name);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
logger.error(`Invalid event name: ${name}`, e);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const sanitizedProperties = sanitizeProperties(properties);
|
|
216
|
+
|
|
217
|
+
// Add system properties
|
|
218
|
+
const mergedProperties: EventProperties = {
|
|
219
|
+
[SystemProperties.DEVICE_TYPE]: detectDeviceType(),
|
|
220
|
+
[SystemProperties.DEVICE_MODEL]: getDeviceModel(),
|
|
221
|
+
...sanitizedProperties,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const event: MGMEvent = {
|
|
225
|
+
name,
|
|
226
|
+
timestamp: getISOTimestamp(),
|
|
227
|
+
userId: this.userId ?? undefined,
|
|
228
|
+
sessionId: this.sessionIdValue,
|
|
229
|
+
platform: detectPlatform(),
|
|
230
|
+
appVersion: this.config.appVersion || undefined,
|
|
231
|
+
osVersion: this.config.osVersion || getOSVersion() || undefined,
|
|
232
|
+
environment: this.config.environment,
|
|
233
|
+
properties: Object.keys(mergedProperties).length > 0 ? mergedProperties : undefined,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
logger.debug(`Tracking event: ${name}`, event);
|
|
237
|
+
|
|
238
|
+
// Store event asynchronously
|
|
239
|
+
this.storage.store(event).catch((e) => {
|
|
240
|
+
logger.error('Failed to store event', e);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Check if we should flush due to batch size
|
|
244
|
+
void this.checkBatchSize();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Identify the current user.
|
|
249
|
+
*/
|
|
250
|
+
identify(userId: string): void {
|
|
251
|
+
if (!userId) {
|
|
252
|
+
logger.warn('identify called with empty userId');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
logger.debug(`Identifying user: ${userId}`);
|
|
257
|
+
persistence.setUserId(userId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Reset user identity.
|
|
262
|
+
*/
|
|
263
|
+
resetIdentity(): void {
|
|
264
|
+
logger.debug('Resetting user identity');
|
|
265
|
+
persistence.setUserId(null);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Start a new session.
|
|
270
|
+
*/
|
|
271
|
+
startNewSession(): void {
|
|
272
|
+
this.sessionIdValue = generateUUID();
|
|
273
|
+
logger.debug(`Started new session: ${this.sessionIdValue}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Flush pending events to the server.
|
|
278
|
+
*/
|
|
279
|
+
async flush(): Promise<void> {
|
|
280
|
+
if (this.isFlushingInternal) {
|
|
281
|
+
logger.debug('Flush already in progress');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
this.isFlushingInternal = true;
|
|
286
|
+
logger.debug('Starting flush');
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await this.performFlush();
|
|
290
|
+
} finally {
|
|
291
|
+
this.isFlushingInternal = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Clear all pending events.
|
|
297
|
+
*/
|
|
298
|
+
async clearPendingEvents(): Promise<void> {
|
|
299
|
+
logger.debug('Clearing all pending events');
|
|
300
|
+
await this.storage.clear();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get the count of pending events.
|
|
305
|
+
*/
|
|
306
|
+
async getPendingEventCount(): Promise<number> {
|
|
307
|
+
return this.storage.eventCount();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clean up resources (stop timers, etc.).
|
|
312
|
+
*/
|
|
313
|
+
destroy(): void {
|
|
314
|
+
this.stopFlushTimer();
|
|
315
|
+
this.removeLifecycleListeners();
|
|
316
|
+
logger.debug('MostlyGoodMetrics instance destroyed');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ============================================================
|
|
320
|
+
// Private methods
|
|
321
|
+
// ============================================================
|
|
322
|
+
|
|
323
|
+
private async checkBatchSize(): Promise<void> {
|
|
324
|
+
const count = await this.storage.eventCount();
|
|
325
|
+
if (count >= this.config.maxBatchSize) {
|
|
326
|
+
logger.debug('Batch size threshold reached, triggering flush');
|
|
327
|
+
void this.flush();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async performFlush(): Promise<void> {
|
|
332
|
+
let hasMoreEvents = true;
|
|
333
|
+
while (hasMoreEvents) {
|
|
334
|
+
const eventCount = await this.storage.eventCount();
|
|
335
|
+
if (eventCount === 0) {
|
|
336
|
+
logger.debug('No events to flush');
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check rate limiting
|
|
341
|
+
if (this.networkClient.isRateLimited()) {
|
|
342
|
+
logger.debug('Rate limited, skipping flush');
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const events = await this.storage.fetchEvents(this.config.maxBatchSize);
|
|
347
|
+
if (events.length === 0) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const payload = this.buildPayload(events);
|
|
352
|
+
const result = await this.networkClient.sendEvents(payload, this.config);
|
|
353
|
+
|
|
354
|
+
if (result.success) {
|
|
355
|
+
logger.debug(`Successfully sent ${events.length} events`);
|
|
356
|
+
await this.storage.removeEvents(events.length);
|
|
357
|
+
} else {
|
|
358
|
+
logger.warn(`Failed to send events: ${result.error.message}`);
|
|
359
|
+
|
|
360
|
+
if (!result.shouldRetry) {
|
|
361
|
+
// Drop events on non-retryable errors (4xx)
|
|
362
|
+
logger.warn('Dropping events due to non-retryable error');
|
|
363
|
+
await this.storage.removeEvents(events.length);
|
|
364
|
+
} else {
|
|
365
|
+
// Keep events for retry on retryable errors
|
|
366
|
+
hasMoreEvents = false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Small delay between batches to avoid overwhelming the server
|
|
371
|
+
await delay(FLUSH_DELAY_MS);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private buildPayload(events: MGMEvent[]): MGMEventsPayload {
|
|
376
|
+
const context: MGMEventContext = {
|
|
377
|
+
platform: detectPlatform(),
|
|
378
|
+
appVersion: this.config.appVersion || undefined,
|
|
379
|
+
osVersion: this.config.osVersion || getOSVersion() || undefined,
|
|
380
|
+
userId: this.userId ?? undefined,
|
|
381
|
+
sessionId: this.sessionIdValue,
|
|
382
|
+
environment: this.config.environment,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
return { events, context };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private startFlushTimer(): void {
|
|
389
|
+
if (this.flushTimer) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.flushTimer = setInterval(() => {
|
|
394
|
+
void this.flush();
|
|
395
|
+
}, this.config.flushInterval * 1000);
|
|
396
|
+
|
|
397
|
+
logger.debug(`Started flush timer (${this.config.flushInterval}s interval)`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private stopFlushTimer(): void {
|
|
401
|
+
if (this.flushTimer) {
|
|
402
|
+
clearInterval(this.flushTimer);
|
|
403
|
+
this.flushTimer = null;
|
|
404
|
+
logger.debug('Stopped flush timer');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private setupLifecycleTracking(): void {
|
|
409
|
+
if (this.lifecycleSetup) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
414
|
+
logger.debug('Not in browser environment, skipping lifecycle tracking');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
this.lifecycleSetup = true;
|
|
419
|
+
|
|
420
|
+
// Track app installed/updated
|
|
421
|
+
this.trackInstallOrUpdate();
|
|
422
|
+
|
|
423
|
+
// Track app opened
|
|
424
|
+
this.trackAppOpened();
|
|
425
|
+
|
|
426
|
+
// Track visibility changes (background/foreground)
|
|
427
|
+
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
428
|
+
|
|
429
|
+
// Flush on page unload
|
|
430
|
+
window.addEventListener('beforeunload', this.handleBeforeUnload);
|
|
431
|
+
window.addEventListener('pagehide', this.handlePageHide);
|
|
432
|
+
|
|
433
|
+
logger.debug('Lifecycle tracking enabled');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private removeLifecycleListeners(): void {
|
|
437
|
+
if (typeof document !== 'undefined') {
|
|
438
|
+
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
439
|
+
}
|
|
440
|
+
if (typeof window !== 'undefined') {
|
|
441
|
+
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
|
442
|
+
window.removeEventListener('pagehide', this.handlePageHide);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private trackInstallOrUpdate(): void {
|
|
447
|
+
const currentVersion = this.config.appVersion;
|
|
448
|
+
const previousVersion = persistence.getAppVersion();
|
|
449
|
+
|
|
450
|
+
if (!currentVersion) {
|
|
451
|
+
// No version configured, skip install/update tracking
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (persistence.isFirstLaunch()) {
|
|
456
|
+
// First launch ever - track install
|
|
457
|
+
this.track(SystemEvents.APP_INSTALLED, {
|
|
458
|
+
[SystemProperties.VERSION]: currentVersion,
|
|
459
|
+
});
|
|
460
|
+
persistence.setAppVersion(currentVersion);
|
|
461
|
+
} else if (previousVersion && previousVersion !== currentVersion) {
|
|
462
|
+
// Version changed - track update
|
|
463
|
+
this.track(SystemEvents.APP_UPDATED, {
|
|
464
|
+
[SystemProperties.VERSION]: currentVersion,
|
|
465
|
+
[SystemProperties.PREVIOUS_VERSION]: previousVersion,
|
|
466
|
+
});
|
|
467
|
+
persistence.setAppVersion(currentVersion);
|
|
468
|
+
} else if (!previousVersion) {
|
|
469
|
+
// First time with version tracking
|
|
470
|
+
persistence.setAppVersion(currentVersion);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private trackAppOpened(): void {
|
|
475
|
+
this.track(SystemEvents.APP_OPENED);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private handleVisibilityChange = (): void => {
|
|
479
|
+
if (document.hidden) {
|
|
480
|
+
// App backgrounded
|
|
481
|
+
this.track(SystemEvents.APP_BACKGROUNDED);
|
|
482
|
+
void this.flush(); // Flush when going to background
|
|
483
|
+
} else {
|
|
484
|
+
// App foregrounded
|
|
485
|
+
this.track(SystemEvents.APP_OPENED);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
private handleBeforeUnload = (): void => {
|
|
490
|
+
// Best-effort flush using sendBeacon if available
|
|
491
|
+
this.flushWithBeacon();
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
private handlePageHide = (): void => {
|
|
495
|
+
// Best-effort flush using sendBeacon if available
|
|
496
|
+
this.flushWithBeacon();
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
private flushWithBeacon(): void {
|
|
500
|
+
// Use sendBeacon for reliable delivery during page unload
|
|
501
|
+
if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Note: This is a synchronous, best-effort send
|
|
506
|
+
// We can't use async storage operations here, so we rely on
|
|
507
|
+
// the regular flush mechanism for most events
|
|
508
|
+
logger.debug('Page unloading, attempting beacon flush');
|
|
509
|
+
}
|
|
510
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MostlyGoodMetrics JavaScript SDK
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, framework-agnostic analytics library for web applications.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { MostlyGoodMetrics } from '@mostly-good-metrics/javascript';
|
|
9
|
+
*
|
|
10
|
+
* // Initialize the SDK
|
|
11
|
+
* MostlyGoodMetrics.configure({
|
|
12
|
+
* apiKey: 'mgm_proj_your_api_key',
|
|
13
|
+
* environment: 'production',
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Track events
|
|
17
|
+
* MostlyGoodMetrics.track('button_clicked', {
|
|
18
|
+
* button_id: 'submit',
|
|
19
|
+
* page: '/checkout',
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Identify users
|
|
23
|
+
* MostlyGoodMetrics.identify('user_123');
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// Main client
|
|
28
|
+
export { MostlyGoodMetrics } from './client';
|
|
29
|
+
|
|
30
|
+
// Types
|
|
31
|
+
export type {
|
|
32
|
+
MGMConfiguration,
|
|
33
|
+
ResolvedConfiguration,
|
|
34
|
+
EventProperties,
|
|
35
|
+
EventPropertyValue,
|
|
36
|
+
MGMEvent,
|
|
37
|
+
MGMEventContext,
|
|
38
|
+
MGMEventsPayload,
|
|
39
|
+
Platform,
|
|
40
|
+
DeviceType,
|
|
41
|
+
MGMErrorType,
|
|
42
|
+
SendResult,
|
|
43
|
+
IEventStorage,
|
|
44
|
+
INetworkClient,
|
|
45
|
+
} from './types';
|
|
46
|
+
|
|
47
|
+
// Error class
|
|
48
|
+
export { MGMError } from './types';
|
|
49
|
+
|
|
50
|
+
// Constants
|
|
51
|
+
export {
|
|
52
|
+
SystemEvents,
|
|
53
|
+
SystemProperties,
|
|
54
|
+
DefaultConfiguration,
|
|
55
|
+
Constraints,
|
|
56
|
+
EVENT_NAME_REGEX,
|
|
57
|
+
} from './types';
|
|
58
|
+
|
|
59
|
+
// Storage implementations (for custom storage adapters)
|
|
60
|
+
export { InMemoryEventStorage, LocalStorageEventStorage, createDefaultStorage } from './storage';
|
|
61
|
+
|
|
62
|
+
// Network client (for custom network implementations)
|
|
63
|
+
export { FetchNetworkClient, createDefaultNetworkClient } from './network';
|
|
64
|
+
|
|
65
|
+
// Utilities (for advanced usage)
|
|
66
|
+
export {
|
|
67
|
+
generateUUID,
|
|
68
|
+
getISOTimestamp,
|
|
69
|
+
isValidEventName,
|
|
70
|
+
validateEventName,
|
|
71
|
+
sanitizeProperties,
|
|
72
|
+
detectPlatform,
|
|
73
|
+
detectDeviceType,
|
|
74
|
+
getOSVersion,
|
|
75
|
+
getDeviceModel,
|
|
76
|
+
} from './utils';
|
|
77
|
+
|
|
78
|
+
// Logger (for debugging)
|
|
79
|
+
export { logger } from './logger';
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal logger for the MostlyGoodMetrics SDK.
|
|
3
|
+
* Only outputs when debug logging is enabled.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const LOG_PREFIX = '[MostlyGoodMetrics]';
|
|
7
|
+
|
|
8
|
+
let debugEnabled = false;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enable or disable debug logging.
|
|
12
|
+
*/
|
|
13
|
+
export function setDebugLogging(enabled: boolean): void {
|
|
14
|
+
debugEnabled = enabled;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if debug logging is enabled.
|
|
19
|
+
*/
|
|
20
|
+
export function isDebugEnabled(): boolean {
|
|
21
|
+
return debugEnabled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Log a debug message (only when debug logging is enabled).
|
|
26
|
+
*/
|
|
27
|
+
export function debug(message: string, ...args: unknown[]): void {
|
|
28
|
+
if (debugEnabled) {
|
|
29
|
+
console.log(`${LOG_PREFIX} [DEBUG]`, message, ...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Log an info message (only when debug logging is enabled).
|
|
35
|
+
*/
|
|
36
|
+
export function info(message: string, ...args: unknown[]): void {
|
|
37
|
+
if (debugEnabled) {
|
|
38
|
+
console.info(`${LOG_PREFIX} [INFO]`, message, ...args);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Log a warning message (always shown).
|
|
44
|
+
*/
|
|
45
|
+
export function warn(message: string, ...args: unknown[]): void {
|
|
46
|
+
console.warn(`${LOG_PREFIX} [WARN]`, message, ...args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Log an error message (always shown).
|
|
51
|
+
*/
|
|
52
|
+
export function error(message: string, ...args: unknown[]): void {
|
|
53
|
+
console.error(`${LOG_PREFIX} [ERROR]`, message, ...args);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const logger = {
|
|
57
|
+
setDebugLogging,
|
|
58
|
+
isDebugEnabled,
|
|
59
|
+
debug,
|
|
60
|
+
info,
|
|
61
|
+
warn,
|
|
62
|
+
error,
|
|
63
|
+
};
|