@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
|
@@ -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
|