@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/utils.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { logger } from './logger';
|
|
2
|
+
import {
|
|
3
|
+
Constraints,
|
|
4
|
+
DefaultConfiguration,
|
|
5
|
+
DeviceType,
|
|
6
|
+
EVENT_NAME_REGEX,
|
|
7
|
+
EventProperties,
|
|
8
|
+
EventPropertyValue,
|
|
9
|
+
MGMConfiguration,
|
|
10
|
+
MGMError,
|
|
11
|
+
Platform,
|
|
12
|
+
ResolvedConfiguration,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a UUID v4 string.
|
|
17
|
+
*/
|
|
18
|
+
export function generateUUID(): string {
|
|
19
|
+
// Use crypto.randomUUID if available (modern browsers and Node.js 19+)
|
|
20
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
21
|
+
return crypto.randomUUID();
|
|
22
|
+
}
|
|
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
|
+
/**
|
|
33
|
+
* Get the current timestamp in ISO8601 format.
|
|
34
|
+
*/
|
|
35
|
+
export function getISOTimestamp(): string {
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate an event name.
|
|
41
|
+
* Must match pattern: ^$?[a-zA-Z][a-zA-Z0-9_]*$
|
|
42
|
+
* Max 255 characters.
|
|
43
|
+
*/
|
|
44
|
+
export function isValidEventName(name: string): boolean {
|
|
45
|
+
if (!name || name.length > Constraints.MAX_EVENT_NAME_LENGTH) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return EVENT_NAME_REGEX.test(name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate an event name and throw if invalid.
|
|
53
|
+
*/
|
|
54
|
+
export function validateEventName(name: string): void {
|
|
55
|
+
if (!name) {
|
|
56
|
+
throw new MGMError('INVALID_EVENT_NAME', 'Event name is required');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (name.length > Constraints.MAX_EVENT_NAME_LENGTH) {
|
|
60
|
+
throw new MGMError(
|
|
61
|
+
'INVALID_EVENT_NAME',
|
|
62
|
+
`Event name must be ${Constraints.MAX_EVENT_NAME_LENGTH} characters or less`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!EVENT_NAME_REGEX.test(name)) {
|
|
67
|
+
throw new MGMError(
|
|
68
|
+
'INVALID_EVENT_NAME',
|
|
69
|
+
'Event name must start with a letter (or $ for system events) and contain only alphanumeric characters and underscores'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sanitize event properties by truncating strings and limiting depth.
|
|
76
|
+
*/
|
|
77
|
+
export function sanitizeProperties(
|
|
78
|
+
properties: EventProperties | undefined,
|
|
79
|
+
maxDepth: number = Constraints.MAX_PROPERTY_DEPTH
|
|
80
|
+
): EventProperties | undefined {
|
|
81
|
+
if (!properties || typeof properties !== 'object') {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const sanitized = sanitizeValue(properties, 0, maxDepth);
|
|
86
|
+
if (typeof sanitized === 'object' && sanitized !== null && !Array.isArray(sanitized)) {
|
|
87
|
+
return sanitized as EventProperties;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Recursively sanitize a property value.
|
|
95
|
+
*/
|
|
96
|
+
function sanitizeValue(
|
|
97
|
+
value: EventPropertyValue,
|
|
98
|
+
depth: number,
|
|
99
|
+
maxDepth: number
|
|
100
|
+
): EventPropertyValue {
|
|
101
|
+
// Null is valid
|
|
102
|
+
if (value === null) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Primitives
|
|
107
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Strings - truncate if needed
|
|
112
|
+
if (typeof value === 'string') {
|
|
113
|
+
if (value.length > Constraints.MAX_STRING_PROPERTY_LENGTH) {
|
|
114
|
+
logger.debug(
|
|
115
|
+
`Truncating string property from ${value.length} to ${Constraints.MAX_STRING_PROPERTY_LENGTH} characters`
|
|
116
|
+
);
|
|
117
|
+
return value.substring(0, Constraints.MAX_STRING_PROPERTY_LENGTH);
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Arrays
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
if (depth >= maxDepth) {
|
|
125
|
+
logger.debug(`Max property depth reached, omitting nested array`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return value.map((item) => sanitizeValue(item, depth + 1, maxDepth));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Objects
|
|
132
|
+
if (typeof value === 'object') {
|
|
133
|
+
if (depth >= maxDepth) {
|
|
134
|
+
logger.debug(`Max property depth reached, omitting nested object`);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result: Record<string, EventPropertyValue> = {};
|
|
139
|
+
for (const [key, val] of Object.entries(value)) {
|
|
140
|
+
result[key] = sanitizeValue(val, depth + 1, maxDepth);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Unknown type - convert to null
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve configuration with defaults.
|
|
151
|
+
*/
|
|
152
|
+
export function resolveConfiguration(config: MGMConfiguration): ResolvedConfiguration {
|
|
153
|
+
const maxBatchSize = Math.min(
|
|
154
|
+
Math.max(config.maxBatchSize ?? DefaultConfiguration.maxBatchSize, Constraints.MIN_BATCH_SIZE),
|
|
155
|
+
Constraints.MAX_BATCH_SIZE
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const flushInterval = Math.max(
|
|
159
|
+
config.flushInterval ?? DefaultConfiguration.flushInterval,
|
|
160
|
+
Constraints.MIN_FLUSH_INTERVAL
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const maxStoredEvents = Math.max(
|
|
164
|
+
config.maxStoredEvents ?? DefaultConfiguration.maxStoredEvents,
|
|
165
|
+
Constraints.MIN_STORED_EVENTS
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
apiKey: config.apiKey,
|
|
170
|
+
baseURL: config.baseURL ?? DefaultConfiguration.baseURL,
|
|
171
|
+
environment: config.environment ?? DefaultConfiguration.environment,
|
|
172
|
+
maxBatchSize,
|
|
173
|
+
flushInterval,
|
|
174
|
+
maxStoredEvents,
|
|
175
|
+
enableDebugLogging: config.enableDebugLogging ?? DefaultConfiguration.enableDebugLogging,
|
|
176
|
+
trackAppLifecycleEvents:
|
|
177
|
+
config.trackAppLifecycleEvents ?? DefaultConfiguration.trackAppLifecycleEvents,
|
|
178
|
+
bundleId: config.bundleId ?? detectBundleId(),
|
|
179
|
+
appVersion: config.appVersion ?? '',
|
|
180
|
+
osVersion: config.osVersion ?? '',
|
|
181
|
+
storage: config.storage,
|
|
182
|
+
networkClient: config.networkClient,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Detect the bundle ID from the current environment.
|
|
188
|
+
*/
|
|
189
|
+
function detectBundleId(): string {
|
|
190
|
+
// In browser, use the hostname
|
|
191
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
192
|
+
return window.location.hostname;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// In Node.js, could use package.json name but that requires fs access
|
|
196
|
+
return '';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Detect the current platform.
|
|
201
|
+
*/
|
|
202
|
+
export function detectPlatform(): Platform {
|
|
203
|
+
// Check for React Native / Expo
|
|
204
|
+
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
|
|
205
|
+
// Could check for Expo-specific globals here
|
|
206
|
+
return 'react-native';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check for Node.js
|
|
210
|
+
if (typeof process !== 'undefined' && process.versions?.node) {
|
|
211
|
+
return 'node';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Default to web for browser environments
|
|
215
|
+
return 'web';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Detect the device type from user agent.
|
|
220
|
+
*/
|
|
221
|
+
export function detectDeviceType(): DeviceType {
|
|
222
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) {
|
|
223
|
+
return 'unknown';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
227
|
+
|
|
228
|
+
// Check for specific device types
|
|
229
|
+
if (/tablet|ipad|playbook|silk/i.test(ua)) {
|
|
230
|
+
return 'tablet';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (/mobile|iphone|ipod|android.*mobile|blackberry|opera mini|opera mobi/i.test(ua)) {
|
|
234
|
+
return 'phone';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (/smart-tv|smarttv|googletv|appletv|hbbtv|pov_tv|netcast.tv/i.test(ua)) {
|
|
238
|
+
return 'tv';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Default to desktop for other browsers
|
|
242
|
+
if (typeof window !== 'undefined') {
|
|
243
|
+
return 'desktop';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return 'unknown';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get the OS version string.
|
|
251
|
+
*/
|
|
252
|
+
export function getOSVersion(): string {
|
|
253
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) {
|
|
254
|
+
return '';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const ua = navigator.userAgent;
|
|
258
|
+
|
|
259
|
+
// Try to extract OS version from user agent
|
|
260
|
+
const patterns: [RegExp, string][] = [
|
|
261
|
+
[/Windows NT ([\d.]+)/i, 'Windows'],
|
|
262
|
+
[/Mac OS X ([\d_.]+)/i, 'macOS'],
|
|
263
|
+
[/iPhone OS ([\d_]+)/i, 'iOS'],
|
|
264
|
+
[/iPad.*OS ([\d_]+)/i, 'iPadOS'],
|
|
265
|
+
[/Android ([\d.]+)/i, 'Android'],
|
|
266
|
+
[/Linux/i, 'Linux'],
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
for (const [pattern, osName] of patterns) {
|
|
270
|
+
const match = ua.match(pattern);
|
|
271
|
+
if (match) {
|
|
272
|
+
const version = match[1]?.replace(/_/g, '.') ?? '';
|
|
273
|
+
return version ? `${osName} ${version}` : osName;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get the browser/device model.
|
|
282
|
+
*/
|
|
283
|
+
export function getDeviceModel(): string {
|
|
284
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) {
|
|
285
|
+
return '';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const ua = navigator.userAgent;
|
|
289
|
+
|
|
290
|
+
// Try to extract browser name and version
|
|
291
|
+
const patterns: [RegExp, string][] = [
|
|
292
|
+
[/Chrome\/([\d.]+)/i, 'Chrome'],
|
|
293
|
+
[/Firefox\/([\d.]+)/i, 'Firefox'],
|
|
294
|
+
[/Safari\/([\d.]+)/i, 'Safari'],
|
|
295
|
+
[/Edge\/([\d.]+)/i, 'Edge'],
|
|
296
|
+
[/MSIE ([\d.]+)/i, 'IE'],
|
|
297
|
+
[/Trident.*rv:([\d.]+)/i, 'IE'],
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
for (const [pattern, browserName] of patterns) {
|
|
301
|
+
const match = ua.match(pattern);
|
|
302
|
+
if (match) {
|
|
303
|
+
return `${browserName} ${match[1]}`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return '';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Delay execution for a specified number of milliseconds.
|
|
312
|
+
*/
|
|
313
|
+
export function delay(ms: number): Promise<void> {
|
|
314
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
315
|
+
}
|