@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.
Files changed (54) hide show
  1. package/README.md +319 -0
  2. package/dist/cjs/client.js +416 -0
  3. package/dist/cjs/client.js.map +1 -0
  4. package/dist/cjs/index.js +65 -0
  5. package/dist/cjs/index.js.map +1 -0
  6. package/dist/cjs/logger.js +64 -0
  7. package/dist/cjs/logger.js.map +1 -0
  8. package/dist/cjs/network.js +192 -0
  9. package/dist/cjs/network.js.map +1 -0
  10. package/dist/cjs/storage.js +227 -0
  11. package/dist/cjs/storage.js.map +1 -0
  12. package/dist/cjs/types.js +70 -0
  13. package/dist/cjs/types.js.map +1 -0
  14. package/dist/cjs/utils.js +249 -0
  15. package/dist/cjs/utils.js.map +1 -0
  16. package/dist/esm/client.js +412 -0
  17. package/dist/esm/client.js.map +1 -0
  18. package/dist/esm/index.js +40 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/logger.js +55 -0
  21. package/dist/esm/logger.js.map +1 -0
  22. package/dist/esm/network.js +187 -0
  23. package/dist/esm/network.js.map +1 -0
  24. package/dist/esm/storage.js +221 -0
  25. package/dist/esm/storage.js.map +1 -0
  26. package/dist/esm/types.js +66 -0
  27. package/dist/esm/types.js.map +1 -0
  28. package/dist/esm/utils.js +236 -0
  29. package/dist/esm/utils.js.map +1 -0
  30. package/dist/types/client.d.ts +126 -0
  31. package/dist/types/client.d.ts.map +1 -0
  32. package/dist/types/index.d.ts +34 -0
  33. package/dist/types/index.d.ts.map +1 -0
  34. package/dist/types/logger.d.ts +37 -0
  35. package/dist/types/logger.d.ts.map +1 -0
  36. package/dist/types/network.d.ts +28 -0
  37. package/dist/types/network.d.ts.map +1 -0
  38. package/dist/types/storage.d.ts +76 -0
  39. package/dist/types/storage.d.ts.map +1 -0
  40. package/dist/types/types.d.ts +279 -0
  41. package/dist/types/types.d.ts.map +1 -0
  42. package/dist/types/utils.d.ts +48 -0
  43. package/dist/types/utils.d.ts.map +1 -0
  44. package/package.json +68 -0
  45. package/src/client.test.ts +346 -0
  46. package/src/client.ts +510 -0
  47. package/src/index.ts +79 -0
  48. package/src/logger.ts +63 -0
  49. package/src/network.ts +230 -0
  50. package/src/storage.test.ts +175 -0
  51. package/src/storage.ts +249 -0
  52. package/src/types.ts +347 -0
  53. package/src/utils.test.ts +239 -0
  54. package/src/utils.ts +315 -0
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateUUID = generateUUID;
4
+ exports.getISOTimestamp = getISOTimestamp;
5
+ exports.isValidEventName = isValidEventName;
6
+ exports.validateEventName = validateEventName;
7
+ exports.sanitizeProperties = sanitizeProperties;
8
+ exports.resolveConfiguration = resolveConfiguration;
9
+ exports.detectPlatform = detectPlatform;
10
+ exports.detectDeviceType = detectDeviceType;
11
+ exports.getOSVersion = getOSVersion;
12
+ exports.getDeviceModel = getDeviceModel;
13
+ exports.delay = delay;
14
+ const logger_1 = require("./logger");
15
+ const types_1 = require("./types");
16
+ /**
17
+ * Generate a UUID v4 string.
18
+ */
19
+ function generateUUID() {
20
+ // Use crypto.randomUUID if available (modern browsers and Node.js 19+)
21
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
22
+ return crypto.randomUUID();
23
+ }
24
+ // Fallback implementation
25
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
26
+ const r = (Math.random() * 16) | 0;
27
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
28
+ return v.toString(16);
29
+ });
30
+ }
31
+ /**
32
+ * Get the current timestamp in ISO8601 format.
33
+ */
34
+ function getISOTimestamp() {
35
+ return new Date().toISOString();
36
+ }
37
+ /**
38
+ * Validate an event name.
39
+ * Must match pattern: ^$?[a-zA-Z][a-zA-Z0-9_]*$
40
+ * Max 255 characters.
41
+ */
42
+ function isValidEventName(name) {
43
+ if (!name || name.length > types_1.Constraints.MAX_EVENT_NAME_LENGTH) {
44
+ return false;
45
+ }
46
+ return types_1.EVENT_NAME_REGEX.test(name);
47
+ }
48
+ /**
49
+ * Validate an event name and throw if invalid.
50
+ */
51
+ function validateEventName(name) {
52
+ if (!name) {
53
+ throw new types_1.MGMError('INVALID_EVENT_NAME', 'Event name is required');
54
+ }
55
+ if (name.length > types_1.Constraints.MAX_EVENT_NAME_LENGTH) {
56
+ throw new types_1.MGMError('INVALID_EVENT_NAME', `Event name must be ${types_1.Constraints.MAX_EVENT_NAME_LENGTH} characters or less`);
57
+ }
58
+ if (!types_1.EVENT_NAME_REGEX.test(name)) {
59
+ throw new types_1.MGMError('INVALID_EVENT_NAME', 'Event name must start with a letter (or $ for system events) and contain only alphanumeric characters and underscores');
60
+ }
61
+ }
62
+ /**
63
+ * Sanitize event properties by truncating strings and limiting depth.
64
+ */
65
+ function sanitizeProperties(properties, maxDepth = types_1.Constraints.MAX_PROPERTY_DEPTH) {
66
+ if (!properties || typeof properties !== 'object') {
67
+ return undefined;
68
+ }
69
+ const sanitized = sanitizeValue(properties, 0, maxDepth);
70
+ if (typeof sanitized === 'object' && sanitized !== null && !Array.isArray(sanitized)) {
71
+ return sanitized;
72
+ }
73
+ return undefined;
74
+ }
75
+ /**
76
+ * Recursively sanitize a property value.
77
+ */
78
+ function sanitizeValue(value, depth, maxDepth) {
79
+ // Null is valid
80
+ if (value === null) {
81
+ return null;
82
+ }
83
+ // Primitives
84
+ if (typeof value === 'boolean' || typeof value === 'number') {
85
+ return value;
86
+ }
87
+ // Strings - truncate if needed
88
+ if (typeof value === 'string') {
89
+ if (value.length > types_1.Constraints.MAX_STRING_PROPERTY_LENGTH) {
90
+ logger_1.logger.debug(`Truncating string property from ${value.length} to ${types_1.Constraints.MAX_STRING_PROPERTY_LENGTH} characters`);
91
+ return value.substring(0, types_1.Constraints.MAX_STRING_PROPERTY_LENGTH);
92
+ }
93
+ return value;
94
+ }
95
+ // Arrays
96
+ if (Array.isArray(value)) {
97
+ if (depth >= maxDepth) {
98
+ logger_1.logger.debug(`Max property depth reached, omitting nested array`);
99
+ return null;
100
+ }
101
+ return value.map((item) => sanitizeValue(item, depth + 1, maxDepth));
102
+ }
103
+ // Objects
104
+ if (typeof value === 'object') {
105
+ if (depth >= maxDepth) {
106
+ logger_1.logger.debug(`Max property depth reached, omitting nested object`);
107
+ return null;
108
+ }
109
+ const result = {};
110
+ for (const [key, val] of Object.entries(value)) {
111
+ result[key] = sanitizeValue(val, depth + 1, maxDepth);
112
+ }
113
+ return result;
114
+ }
115
+ // Unknown type - convert to null
116
+ return null;
117
+ }
118
+ /**
119
+ * Resolve configuration with defaults.
120
+ */
121
+ function resolveConfiguration(config) {
122
+ const maxBatchSize = Math.min(Math.max(config.maxBatchSize ?? types_1.DefaultConfiguration.maxBatchSize, types_1.Constraints.MIN_BATCH_SIZE), types_1.Constraints.MAX_BATCH_SIZE);
123
+ const flushInterval = Math.max(config.flushInterval ?? types_1.DefaultConfiguration.flushInterval, types_1.Constraints.MIN_FLUSH_INTERVAL);
124
+ const maxStoredEvents = Math.max(config.maxStoredEvents ?? types_1.DefaultConfiguration.maxStoredEvents, types_1.Constraints.MIN_STORED_EVENTS);
125
+ return {
126
+ apiKey: config.apiKey,
127
+ baseURL: config.baseURL ?? types_1.DefaultConfiguration.baseURL,
128
+ environment: config.environment ?? types_1.DefaultConfiguration.environment,
129
+ maxBatchSize,
130
+ flushInterval,
131
+ maxStoredEvents,
132
+ enableDebugLogging: config.enableDebugLogging ?? types_1.DefaultConfiguration.enableDebugLogging,
133
+ trackAppLifecycleEvents: config.trackAppLifecycleEvents ?? types_1.DefaultConfiguration.trackAppLifecycleEvents,
134
+ bundleId: config.bundleId ?? detectBundleId(),
135
+ appVersion: config.appVersion ?? '',
136
+ osVersion: config.osVersion ?? '',
137
+ storage: config.storage,
138
+ networkClient: config.networkClient,
139
+ };
140
+ }
141
+ /**
142
+ * Detect the bundle ID from the current environment.
143
+ */
144
+ function detectBundleId() {
145
+ // In browser, use the hostname
146
+ if (typeof window !== 'undefined' && window.location) {
147
+ return window.location.hostname;
148
+ }
149
+ // In Node.js, could use package.json name but that requires fs access
150
+ return '';
151
+ }
152
+ /**
153
+ * Detect the current platform.
154
+ */
155
+ function detectPlatform() {
156
+ // Check for React Native / Expo
157
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
158
+ // Could check for Expo-specific globals here
159
+ return 'react-native';
160
+ }
161
+ // Check for Node.js
162
+ if (typeof process !== 'undefined' && process.versions?.node) {
163
+ return 'node';
164
+ }
165
+ // Default to web for browser environments
166
+ return 'web';
167
+ }
168
+ /**
169
+ * Detect the device type from user agent.
170
+ */
171
+ function detectDeviceType() {
172
+ if (typeof navigator === 'undefined' || !navigator.userAgent) {
173
+ return 'unknown';
174
+ }
175
+ const ua = navigator.userAgent.toLowerCase();
176
+ // Check for specific device types
177
+ if (/tablet|ipad|playbook|silk/i.test(ua)) {
178
+ return 'tablet';
179
+ }
180
+ if (/mobile|iphone|ipod|android.*mobile|blackberry|opera mini|opera mobi/i.test(ua)) {
181
+ return 'phone';
182
+ }
183
+ if (/smart-tv|smarttv|googletv|appletv|hbbtv|pov_tv|netcast.tv/i.test(ua)) {
184
+ return 'tv';
185
+ }
186
+ // Default to desktop for other browsers
187
+ if (typeof window !== 'undefined') {
188
+ return 'desktop';
189
+ }
190
+ return 'unknown';
191
+ }
192
+ /**
193
+ * Get the OS version string.
194
+ */
195
+ function getOSVersion() {
196
+ if (typeof navigator === 'undefined' || !navigator.userAgent) {
197
+ return '';
198
+ }
199
+ const ua = navigator.userAgent;
200
+ // Try to extract OS version from user agent
201
+ const patterns = [
202
+ [/Windows NT ([\d.]+)/i, 'Windows'],
203
+ [/Mac OS X ([\d_.]+)/i, 'macOS'],
204
+ [/iPhone OS ([\d_]+)/i, 'iOS'],
205
+ [/iPad.*OS ([\d_]+)/i, 'iPadOS'],
206
+ [/Android ([\d.]+)/i, 'Android'],
207
+ [/Linux/i, 'Linux'],
208
+ ];
209
+ for (const [pattern, osName] of patterns) {
210
+ const match = ua.match(pattern);
211
+ if (match) {
212
+ const version = match[1]?.replace(/_/g, '.') ?? '';
213
+ return version ? `${osName} ${version}` : osName;
214
+ }
215
+ }
216
+ return '';
217
+ }
218
+ /**
219
+ * Get the browser/device model.
220
+ */
221
+ function getDeviceModel() {
222
+ if (typeof navigator === 'undefined' || !navigator.userAgent) {
223
+ return '';
224
+ }
225
+ const ua = navigator.userAgent;
226
+ // Try to extract browser name and version
227
+ const patterns = [
228
+ [/Chrome\/([\d.]+)/i, 'Chrome'],
229
+ [/Firefox\/([\d.]+)/i, 'Firefox'],
230
+ [/Safari\/([\d.]+)/i, 'Safari'],
231
+ [/Edge\/([\d.]+)/i, 'Edge'],
232
+ [/MSIE ([\d.]+)/i, 'IE'],
233
+ [/Trident.*rv:([\d.]+)/i, 'IE'],
234
+ ];
235
+ for (const [pattern, browserName] of patterns) {
236
+ const match = ua.match(pattern);
237
+ if (match) {
238
+ return `${browserName} ${match[1]}`;
239
+ }
240
+ }
241
+ return '';
242
+ }
243
+ /**
244
+ * Delay execution for a specified number of milliseconds.
245
+ */
246
+ function delay(ms) {
247
+ return new Promise((resolve) => setTimeout(resolve, ms));
248
+ }
249
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,412 @@
1
+ import { logger, setDebugLogging } from './logger';
2
+ import { createDefaultNetworkClient } from './network';
3
+ import { createDefaultStorage, persistence } from './storage';
4
+ import { SystemEvents, SystemProperties, } from './types';
5
+ import { delay, detectDeviceType, detectPlatform, generateUUID, getDeviceModel, getISOTimestamp, getOSVersion, resolveConfiguration, sanitizeProperties, validateEventName, } from './utils';
6
+ const FLUSH_DELAY_MS = 100; // Delay between batch sends
7
+ /**
8
+ * Main client for MostlyGoodMetrics.
9
+ * Use the static `configure` method to initialize, then use static methods or the instance.
10
+ */
11
+ export class MostlyGoodMetrics {
12
+ /**
13
+ * Private constructor - use `configure` to create an instance.
14
+ */
15
+ constructor(config) {
16
+ this.flushTimer = null;
17
+ this.isFlushingInternal = false;
18
+ this.lifecycleSetup = false;
19
+ this.handleVisibilityChange = () => {
20
+ if (document.hidden) {
21
+ // App backgrounded
22
+ this.track(SystemEvents.APP_BACKGROUNDED);
23
+ void this.flush(); // Flush when going to background
24
+ }
25
+ else {
26
+ // App foregrounded
27
+ this.track(SystemEvents.APP_OPENED);
28
+ }
29
+ };
30
+ this.handleBeforeUnload = () => {
31
+ // Best-effort flush using sendBeacon if available
32
+ this.flushWithBeacon();
33
+ };
34
+ this.handlePageHide = () => {
35
+ // Best-effort flush using sendBeacon if available
36
+ this.flushWithBeacon();
37
+ };
38
+ this.config = resolveConfiguration(config);
39
+ this.sessionIdValue = generateUUID();
40
+ // Set up logging
41
+ setDebugLogging(this.config.enableDebugLogging);
42
+ // Initialize storage
43
+ this.storage = this.config.storage ?? createDefaultStorage(this.config.maxStoredEvents);
44
+ // Initialize network client
45
+ this.networkClient = this.config.networkClient ?? createDefaultNetworkClient();
46
+ logger.info(`MostlyGoodMetrics initialized with environment: ${this.config.environment}`);
47
+ // Start auto-flush timer
48
+ this.startFlushTimer();
49
+ // Set up lifecycle tracking
50
+ if (this.config.trackAppLifecycleEvents) {
51
+ this.setupLifecycleTracking();
52
+ }
53
+ }
54
+ /**
55
+ * Configure and initialize the SDK.
56
+ * Returns the singleton instance.
57
+ */
58
+ static configure(config) {
59
+ if (!config.apiKey) {
60
+ throw new Error('API key is required');
61
+ }
62
+ if (MostlyGoodMetrics.instance) {
63
+ logger.warn('MostlyGoodMetrics.configure called multiple times. Using existing instance.');
64
+ return MostlyGoodMetrics.instance;
65
+ }
66
+ MostlyGoodMetrics.instance = new MostlyGoodMetrics(config);
67
+ return MostlyGoodMetrics.instance;
68
+ }
69
+ /**
70
+ * Get the shared instance, or null if not configured.
71
+ */
72
+ static get shared() {
73
+ return MostlyGoodMetrics.instance;
74
+ }
75
+ /**
76
+ * Check if the SDK has been configured.
77
+ */
78
+ static get isConfigured() {
79
+ return MostlyGoodMetrics.instance !== null;
80
+ }
81
+ /**
82
+ * Reset the SDK (primarily for testing).
83
+ */
84
+ static reset() {
85
+ if (MostlyGoodMetrics.instance) {
86
+ MostlyGoodMetrics.instance.destroy();
87
+ MostlyGoodMetrics.instance = null;
88
+ }
89
+ }
90
+ // ============================================================
91
+ // Static convenience methods (delegate to shared instance)
92
+ // ============================================================
93
+ /**
94
+ * Track an event with the given name and optional properties.
95
+ */
96
+ static track(name, properties) {
97
+ MostlyGoodMetrics.instance?.track(name, properties);
98
+ }
99
+ /**
100
+ * Identify the current user.
101
+ */
102
+ static identify(userId) {
103
+ MostlyGoodMetrics.instance?.identify(userId);
104
+ }
105
+ /**
106
+ * Reset user identity.
107
+ */
108
+ static resetIdentity() {
109
+ MostlyGoodMetrics.instance?.resetIdentity();
110
+ }
111
+ /**
112
+ * Flush pending events to the server.
113
+ */
114
+ static flush() {
115
+ return MostlyGoodMetrics.instance?.flush() ?? Promise.resolve();
116
+ }
117
+ /**
118
+ * Start a new session.
119
+ */
120
+ static startNewSession() {
121
+ MostlyGoodMetrics.instance?.startNewSession();
122
+ }
123
+ /**
124
+ * Clear all pending events.
125
+ */
126
+ static clearPendingEvents() {
127
+ return MostlyGoodMetrics.instance?.clearPendingEvents() ?? Promise.resolve();
128
+ }
129
+ /**
130
+ * Get the count of pending events.
131
+ */
132
+ static getPendingEventCount() {
133
+ return MostlyGoodMetrics.instance?.getPendingEventCount() ?? Promise.resolve(0);
134
+ }
135
+ // ============================================================
136
+ // Instance properties
137
+ // ============================================================
138
+ /**
139
+ * Get the current user ID.
140
+ */
141
+ get userId() {
142
+ return persistence.getUserId();
143
+ }
144
+ /**
145
+ * Get the current session ID.
146
+ */
147
+ get sessionId() {
148
+ return this.sessionIdValue;
149
+ }
150
+ /**
151
+ * Check if a flush operation is in progress.
152
+ */
153
+ get isFlushing() {
154
+ return this.isFlushingInternal;
155
+ }
156
+ /**
157
+ * Get the resolved configuration.
158
+ */
159
+ get configuration() {
160
+ return { ...this.config };
161
+ }
162
+ // ============================================================
163
+ // Instance methods
164
+ // ============================================================
165
+ /**
166
+ * Track an event with the given name and optional properties.
167
+ */
168
+ track(name, properties) {
169
+ try {
170
+ validateEventName(name);
171
+ }
172
+ catch (e) {
173
+ logger.error(`Invalid event name: ${name}`, e);
174
+ return;
175
+ }
176
+ const sanitizedProperties = sanitizeProperties(properties);
177
+ // Add system properties
178
+ const mergedProperties = {
179
+ [SystemProperties.DEVICE_TYPE]: detectDeviceType(),
180
+ [SystemProperties.DEVICE_MODEL]: getDeviceModel(),
181
+ ...sanitizedProperties,
182
+ };
183
+ const event = {
184
+ name,
185
+ timestamp: getISOTimestamp(),
186
+ userId: this.userId ?? undefined,
187
+ sessionId: this.sessionIdValue,
188
+ platform: detectPlatform(),
189
+ appVersion: this.config.appVersion || undefined,
190
+ osVersion: this.config.osVersion || getOSVersion() || undefined,
191
+ environment: this.config.environment,
192
+ properties: Object.keys(mergedProperties).length > 0 ? mergedProperties : undefined,
193
+ };
194
+ logger.debug(`Tracking event: ${name}`, event);
195
+ // Store event asynchronously
196
+ this.storage.store(event).catch((e) => {
197
+ logger.error('Failed to store event', e);
198
+ });
199
+ // Check if we should flush due to batch size
200
+ void this.checkBatchSize();
201
+ }
202
+ /**
203
+ * Identify the current user.
204
+ */
205
+ identify(userId) {
206
+ if (!userId) {
207
+ logger.warn('identify called with empty userId');
208
+ return;
209
+ }
210
+ logger.debug(`Identifying user: ${userId}`);
211
+ persistence.setUserId(userId);
212
+ }
213
+ /**
214
+ * Reset user identity.
215
+ */
216
+ resetIdentity() {
217
+ logger.debug('Resetting user identity');
218
+ persistence.setUserId(null);
219
+ }
220
+ /**
221
+ * Start a new session.
222
+ */
223
+ startNewSession() {
224
+ this.sessionIdValue = generateUUID();
225
+ logger.debug(`Started new session: ${this.sessionIdValue}`);
226
+ }
227
+ /**
228
+ * Flush pending events to the server.
229
+ */
230
+ async flush() {
231
+ if (this.isFlushingInternal) {
232
+ logger.debug('Flush already in progress');
233
+ return;
234
+ }
235
+ this.isFlushingInternal = true;
236
+ logger.debug('Starting flush');
237
+ try {
238
+ await this.performFlush();
239
+ }
240
+ finally {
241
+ this.isFlushingInternal = false;
242
+ }
243
+ }
244
+ /**
245
+ * Clear all pending events.
246
+ */
247
+ async clearPendingEvents() {
248
+ logger.debug('Clearing all pending events');
249
+ await this.storage.clear();
250
+ }
251
+ /**
252
+ * Get the count of pending events.
253
+ */
254
+ async getPendingEventCount() {
255
+ return this.storage.eventCount();
256
+ }
257
+ /**
258
+ * Clean up resources (stop timers, etc.).
259
+ */
260
+ destroy() {
261
+ this.stopFlushTimer();
262
+ this.removeLifecycleListeners();
263
+ logger.debug('MostlyGoodMetrics instance destroyed');
264
+ }
265
+ // ============================================================
266
+ // Private methods
267
+ // ============================================================
268
+ async checkBatchSize() {
269
+ const count = await this.storage.eventCount();
270
+ if (count >= this.config.maxBatchSize) {
271
+ logger.debug('Batch size threshold reached, triggering flush');
272
+ void this.flush();
273
+ }
274
+ }
275
+ async performFlush() {
276
+ let hasMoreEvents = true;
277
+ while (hasMoreEvents) {
278
+ const eventCount = await this.storage.eventCount();
279
+ if (eventCount === 0) {
280
+ logger.debug('No events to flush');
281
+ break;
282
+ }
283
+ // Check rate limiting
284
+ if (this.networkClient.isRateLimited()) {
285
+ logger.debug('Rate limited, skipping flush');
286
+ break;
287
+ }
288
+ const events = await this.storage.fetchEvents(this.config.maxBatchSize);
289
+ if (events.length === 0) {
290
+ break;
291
+ }
292
+ const payload = this.buildPayload(events);
293
+ const result = await this.networkClient.sendEvents(payload, this.config);
294
+ if (result.success) {
295
+ logger.debug(`Successfully sent ${events.length} events`);
296
+ await this.storage.removeEvents(events.length);
297
+ }
298
+ else {
299
+ logger.warn(`Failed to send events: ${result.error.message}`);
300
+ if (!result.shouldRetry) {
301
+ // Drop events on non-retryable errors (4xx)
302
+ logger.warn('Dropping events due to non-retryable error');
303
+ await this.storage.removeEvents(events.length);
304
+ }
305
+ else {
306
+ // Keep events for retry on retryable errors
307
+ hasMoreEvents = false;
308
+ }
309
+ }
310
+ // Small delay between batches to avoid overwhelming the server
311
+ await delay(FLUSH_DELAY_MS);
312
+ }
313
+ }
314
+ buildPayload(events) {
315
+ const context = {
316
+ platform: detectPlatform(),
317
+ appVersion: this.config.appVersion || undefined,
318
+ osVersion: this.config.osVersion || getOSVersion() || undefined,
319
+ userId: this.userId ?? undefined,
320
+ sessionId: this.sessionIdValue,
321
+ environment: this.config.environment,
322
+ };
323
+ return { events, context };
324
+ }
325
+ startFlushTimer() {
326
+ if (this.flushTimer) {
327
+ return;
328
+ }
329
+ this.flushTimer = setInterval(() => {
330
+ void this.flush();
331
+ }, this.config.flushInterval * 1000);
332
+ logger.debug(`Started flush timer (${this.config.flushInterval}s interval)`);
333
+ }
334
+ stopFlushTimer() {
335
+ if (this.flushTimer) {
336
+ clearInterval(this.flushTimer);
337
+ this.flushTimer = null;
338
+ logger.debug('Stopped flush timer');
339
+ }
340
+ }
341
+ setupLifecycleTracking() {
342
+ if (this.lifecycleSetup) {
343
+ return;
344
+ }
345
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
346
+ logger.debug('Not in browser environment, skipping lifecycle tracking');
347
+ return;
348
+ }
349
+ this.lifecycleSetup = true;
350
+ // Track app installed/updated
351
+ this.trackInstallOrUpdate();
352
+ // Track app opened
353
+ this.trackAppOpened();
354
+ // Track visibility changes (background/foreground)
355
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
356
+ // Flush on page unload
357
+ window.addEventListener('beforeunload', this.handleBeforeUnload);
358
+ window.addEventListener('pagehide', this.handlePageHide);
359
+ logger.debug('Lifecycle tracking enabled');
360
+ }
361
+ removeLifecycleListeners() {
362
+ if (typeof document !== 'undefined') {
363
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
364
+ }
365
+ if (typeof window !== 'undefined') {
366
+ window.removeEventListener('beforeunload', this.handleBeforeUnload);
367
+ window.removeEventListener('pagehide', this.handlePageHide);
368
+ }
369
+ }
370
+ trackInstallOrUpdate() {
371
+ const currentVersion = this.config.appVersion;
372
+ const previousVersion = persistence.getAppVersion();
373
+ if (!currentVersion) {
374
+ // No version configured, skip install/update tracking
375
+ return;
376
+ }
377
+ if (persistence.isFirstLaunch()) {
378
+ // First launch ever - track install
379
+ this.track(SystemEvents.APP_INSTALLED, {
380
+ [SystemProperties.VERSION]: currentVersion,
381
+ });
382
+ persistence.setAppVersion(currentVersion);
383
+ }
384
+ else if (previousVersion && previousVersion !== currentVersion) {
385
+ // Version changed - track update
386
+ this.track(SystemEvents.APP_UPDATED, {
387
+ [SystemProperties.VERSION]: currentVersion,
388
+ [SystemProperties.PREVIOUS_VERSION]: previousVersion,
389
+ });
390
+ persistence.setAppVersion(currentVersion);
391
+ }
392
+ else if (!previousVersion) {
393
+ // First time with version tracking
394
+ persistence.setAppVersion(currentVersion);
395
+ }
396
+ }
397
+ trackAppOpened() {
398
+ this.track(SystemEvents.APP_OPENED);
399
+ }
400
+ flushWithBeacon() {
401
+ // Use sendBeacon for reliable delivery during page unload
402
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) {
403
+ return;
404
+ }
405
+ // Note: This is a synchronous, best-effort send
406
+ // We can't use async storage operations here, so we rely on
407
+ // the regular flush mechanism for most events
408
+ logger.debug('Page unloading, attempting beacon flush');
409
+ }
410
+ }
411
+ MostlyGoodMetrics.instance = null;
412
+ //# sourceMappingURL=client.js.map