@mostly-good-metrics/javascript 0.2.0 → 0.4.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/dist/cjs/client.js +73 -2
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/storage.js +61 -0
- package/dist/cjs/storage.js.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/cjs/utils.js +22 -0
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/client.js +74 -3
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/storage.js +61 -0
- package/dist/esm/storage.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils.js +20 -0
- package/dist/esm/utils.js.map +1 -1
- package/dist/types/client.d.ts +40 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/storage.d.ts +23 -1
- package/dist/types/storage.d.ts.map +1 -1
- package/dist/types/types.d.ts +20 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +8 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.test.ts +117 -0
- package/src/client.ts +85 -2
- package/src/storage.test.ts +150 -1
- package/src/storage.ts +66 -1
- package/src/types.ts +24 -0
- package/src/utils.test.ts +31 -0
- package/src/utils.ts +21 -0
package/src/storage.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InMemoryEventStorage, LocalStorageEventStorage } from './storage';
|
|
1
|
+
import { InMemoryEventStorage, LocalStorageEventStorage, persistence } from './storage';
|
|
2
2
|
import { MGMEvent } from './types';
|
|
3
3
|
|
|
4
4
|
const createMockEvent = (name: string): MGMEvent => ({
|
|
@@ -173,3 +173,152 @@ describe('LocalStorageEventStorage', () => {
|
|
|
173
173
|
expect(events).toHaveLength(0);
|
|
174
174
|
});
|
|
175
175
|
});
|
|
176
|
+
|
|
177
|
+
describe('PersistenceManager super properties', () => {
|
|
178
|
+
beforeEach(() => {
|
|
179
|
+
// Mock localStorage
|
|
180
|
+
const localStorageMock = (() => {
|
|
181
|
+
let store: Record<string, string> = {};
|
|
182
|
+
return {
|
|
183
|
+
getItem: jest.fn((key: string) => store[key] ?? null),
|
|
184
|
+
setItem: jest.fn((key: string, value: string) => {
|
|
185
|
+
store[key] = value;
|
|
186
|
+
}),
|
|
187
|
+
removeItem: jest.fn((key: string) => {
|
|
188
|
+
delete store[key];
|
|
189
|
+
}),
|
|
190
|
+
clear: jest.fn(() => {
|
|
191
|
+
store = {};
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
})();
|
|
195
|
+
|
|
196
|
+
Object.defineProperty(window, 'localStorage', {
|
|
197
|
+
value: localStorageMock,
|
|
198
|
+
writable: true,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Clear any existing super properties
|
|
202
|
+
persistence.clearSuperProperties();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
afterEach(() => {
|
|
206
|
+
jest.clearAllMocks();
|
|
207
|
+
persistence.clearSuperProperties();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should return empty object when no super properties are set', () => {
|
|
211
|
+
const props = persistence.getSuperProperties();
|
|
212
|
+
expect(props).toEqual({});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should set and get a single super property', () => {
|
|
216
|
+
persistence.setSuperProperty('tier', 'premium');
|
|
217
|
+
|
|
218
|
+
const props = persistence.getSuperProperties();
|
|
219
|
+
expect(props.tier).toBe('premium');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should set and get multiple super properties', () => {
|
|
223
|
+
persistence.setSuperProperties({
|
|
224
|
+
tier: 'enterprise',
|
|
225
|
+
region: 'us-west',
|
|
226
|
+
beta_user: true,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const props = persistence.getSuperProperties();
|
|
230
|
+
expect(props.tier).toBe('enterprise');
|
|
231
|
+
expect(props.region).toBe('us-west');
|
|
232
|
+
expect(props.beta_user).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should merge properties when setting multiple times', () => {
|
|
236
|
+
persistence.setSuperProperty('first', 'value1');
|
|
237
|
+
persistence.setSuperProperties({
|
|
238
|
+
second: 'value2',
|
|
239
|
+
third: 'value3',
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const props = persistence.getSuperProperties();
|
|
243
|
+
expect(props.first).toBe('value1');
|
|
244
|
+
expect(props.second).toBe('value2');
|
|
245
|
+
expect(props.third).toBe('value3');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should override existing property with same key', () => {
|
|
249
|
+
persistence.setSuperProperty('key', 'original');
|
|
250
|
+
persistence.setSuperProperty('key', 'updated');
|
|
251
|
+
|
|
252
|
+
const props = persistence.getSuperProperties();
|
|
253
|
+
expect(props.key).toBe('updated');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should remove a single super property', () => {
|
|
257
|
+
persistence.setSuperProperties({
|
|
258
|
+
keep: 'this',
|
|
259
|
+
remove: 'this',
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
persistence.removeSuperProperty('remove');
|
|
263
|
+
|
|
264
|
+
const props = persistence.getSuperProperties();
|
|
265
|
+
expect(props.keep).toBe('this');
|
|
266
|
+
expect(props.remove).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should handle removing non-existent property gracefully', () => {
|
|
270
|
+
persistence.setSuperProperty('exists', 'value');
|
|
271
|
+
|
|
272
|
+
expect(() => persistence.removeSuperProperty('nonexistent')).not.toThrow();
|
|
273
|
+
|
|
274
|
+
const props = persistence.getSuperProperties();
|
|
275
|
+
expect(props.exists).toBe('value');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should clear all super properties', () => {
|
|
279
|
+
persistence.setSuperProperties({
|
|
280
|
+
prop1: 'value1',
|
|
281
|
+
prop2: 'value2',
|
|
282
|
+
prop3: 'value3',
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
persistence.clearSuperProperties();
|
|
286
|
+
|
|
287
|
+
const props = persistence.getSuperProperties();
|
|
288
|
+
expect(Object.keys(props)).toHaveLength(0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should persist super properties to localStorage', () => {
|
|
292
|
+
persistence.setSuperProperties({
|
|
293
|
+
persistent: 'value',
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(localStorage.setItem).toHaveBeenCalledWith(
|
|
297
|
+
'mostlygoodmetrics_super_properties',
|
|
298
|
+
JSON.stringify({ persistent: 'value' })
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should handle various value types', () => {
|
|
303
|
+
persistence.setSuperProperties({
|
|
304
|
+
string_val: 'text',
|
|
305
|
+
number_val: 42,
|
|
306
|
+
boolean_val: true,
|
|
307
|
+
null_val: null,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const props = persistence.getSuperProperties();
|
|
311
|
+
expect(props.string_val).toBe('text');
|
|
312
|
+
expect(props.number_val).toBe(42);
|
|
313
|
+
expect(props.boolean_val).toBe(true);
|
|
314
|
+
expect(props.null_val).toBe(null);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should handle localStorage getItem returning corrupted JSON', () => {
|
|
318
|
+
(localStorage.getItem as jest.Mock).mockReturnValueOnce('not valid json {');
|
|
319
|
+
|
|
320
|
+
// Should not throw and should return empty object
|
|
321
|
+
const props = persistence.getSuperProperties();
|
|
322
|
+
expect(props).toEqual({});
|
|
323
|
+
});
|
|
324
|
+
});
|
package/src/storage.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { logger } from './logger';
|
|
2
|
-
import { Constraints, IEventStorage, MGMError, MGMEvent } from './types';
|
|
2
|
+
import { Constraints, EventProperties, IEventStorage, MGMError, MGMEvent } from './types';
|
|
3
3
|
|
|
4
4
|
const STORAGE_KEY = 'mostlygoodmetrics_events';
|
|
5
5
|
const USER_ID_KEY = 'mostlygoodmetrics_user_id';
|
|
6
6
|
const APP_VERSION_KEY = 'mostlygoodmetrics_app_version';
|
|
7
|
+
const SUPER_PROPERTIES_KEY = 'mostlygoodmetrics_super_properties';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Check if we're running in a browser environment with localStorage available.
|
|
@@ -177,6 +178,7 @@ export function createDefaultStorage(maxEvents: number): IEventStorage {
|
|
|
177
178
|
class PersistenceManager {
|
|
178
179
|
private inMemoryUserId: string | null = null;
|
|
179
180
|
private inMemoryAppVersion: string | null = null;
|
|
181
|
+
private inMemorySuperProperties: EventProperties = {};
|
|
180
182
|
|
|
181
183
|
/**
|
|
182
184
|
* Get the persisted user ID.
|
|
@@ -244,6 +246,69 @@ class PersistenceManager {
|
|
|
244
246
|
}
|
|
245
247
|
return false;
|
|
246
248
|
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get all super properties.
|
|
252
|
+
*/
|
|
253
|
+
getSuperProperties(): EventProperties {
|
|
254
|
+
if (isLocalStorageAvailable()) {
|
|
255
|
+
try {
|
|
256
|
+
const stored = localStorage.getItem(SUPER_PROPERTIES_KEY);
|
|
257
|
+
if (stored) {
|
|
258
|
+
return JSON.parse(stored) as EventProperties;
|
|
259
|
+
}
|
|
260
|
+
} catch (e) {
|
|
261
|
+
logger.warn('Failed to load super properties from localStorage', e);
|
|
262
|
+
}
|
|
263
|
+
return {};
|
|
264
|
+
}
|
|
265
|
+
return { ...this.inMemorySuperProperties };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Set a single super property.
|
|
270
|
+
*/
|
|
271
|
+
setSuperProperty(key: string, value: EventProperties[string]): void {
|
|
272
|
+
const properties = this.getSuperProperties();
|
|
273
|
+
properties[key] = value;
|
|
274
|
+
this.saveSuperProperties(properties);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Set multiple super properties at once.
|
|
279
|
+
*/
|
|
280
|
+
setSuperProperties(properties: EventProperties): void {
|
|
281
|
+
const current = this.getSuperProperties();
|
|
282
|
+
const merged = { ...current, ...properties };
|
|
283
|
+
this.saveSuperProperties(merged);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Remove a single super property.
|
|
288
|
+
*/
|
|
289
|
+
removeSuperProperty(key: string): void {
|
|
290
|
+
const properties = this.getSuperProperties();
|
|
291
|
+
delete properties[key];
|
|
292
|
+
this.saveSuperProperties(properties);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Clear all super properties.
|
|
297
|
+
*/
|
|
298
|
+
clearSuperProperties(): void {
|
|
299
|
+
this.saveSuperProperties({});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private saveSuperProperties(properties: EventProperties): void {
|
|
303
|
+
this.inMemorySuperProperties = properties;
|
|
304
|
+
if (isLocalStorageAvailable()) {
|
|
305
|
+
try {
|
|
306
|
+
localStorage.setItem(SUPER_PROPERTIES_KEY, JSON.stringify(properties));
|
|
307
|
+
} catch (e) {
|
|
308
|
+
logger.warn('Failed to save super properties to localStorage', e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
247
312
|
}
|
|
248
313
|
|
|
249
314
|
export const persistence = new PersistenceManager();
|
package/src/types.ts
CHANGED
|
@@ -159,6 +159,11 @@ export interface MGMEvent {
|
|
|
159
159
|
*/
|
|
160
160
|
appVersion?: string;
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The app build number (separate from version).
|
|
164
|
+
*/
|
|
165
|
+
appBuildNumber?: string;
|
|
166
|
+
|
|
162
167
|
/**
|
|
163
168
|
* The OS version string.
|
|
164
169
|
*/
|
|
@@ -169,6 +174,21 @@ export interface MGMEvent {
|
|
|
169
174
|
*/
|
|
170
175
|
environment: string;
|
|
171
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The device manufacturer (e.g., "Apple", "Samsung").
|
|
179
|
+
*/
|
|
180
|
+
deviceManufacturer?: string;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The user's locale (e.g., "en-US").
|
|
184
|
+
*/
|
|
185
|
+
locale?: string;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The user's timezone (e.g., "America/New_York").
|
|
189
|
+
*/
|
|
190
|
+
timezone?: string;
|
|
191
|
+
|
|
172
192
|
/**
|
|
173
193
|
* Custom properties attached to this event.
|
|
174
194
|
*/
|
|
@@ -182,10 +202,14 @@ export interface MGMEvent {
|
|
|
182
202
|
export interface MGMEventContext {
|
|
183
203
|
platform: Platform;
|
|
184
204
|
appVersion?: string;
|
|
205
|
+
appBuildNumber?: string;
|
|
185
206
|
osVersion?: string;
|
|
186
207
|
userId?: string;
|
|
187
208
|
sessionId?: string;
|
|
188
209
|
environment: string;
|
|
210
|
+
deviceManufacturer?: string;
|
|
211
|
+
locale?: string;
|
|
212
|
+
timezone?: string;
|
|
189
213
|
}
|
|
190
214
|
|
|
191
215
|
/**
|
package/src/utils.test.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
validateEventName,
|
|
6
6
|
sanitizeProperties,
|
|
7
7
|
resolveConfiguration,
|
|
8
|
+
getLocale,
|
|
9
|
+
getTimezone,
|
|
8
10
|
} from './utils';
|
|
9
11
|
import { Constraints, DefaultConfiguration, MGMError } from './types';
|
|
10
12
|
|
|
@@ -237,3 +239,32 @@ describe('resolveConfiguration', () => {
|
|
|
237
239
|
expect(config.maxStoredEvents).toBe(Constraints.MIN_STORED_EVENTS);
|
|
238
240
|
});
|
|
239
241
|
});
|
|
242
|
+
|
|
243
|
+
describe('getLocale', () => {
|
|
244
|
+
it('should return a non-empty string', () => {
|
|
245
|
+
const locale = getLocale();
|
|
246
|
+
expect(typeof locale).toBe('string');
|
|
247
|
+
expect(locale.length).toBeGreaterThan(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return a locale-like string (e.g., en, en-US)', () => {
|
|
251
|
+
const locale = getLocale();
|
|
252
|
+
// Locale should be something like "en", "en-US", "fr-FR", etc.
|
|
253
|
+
expect(locale).toMatch(/^[a-z]{2}(-[A-Z]{2})?$/i);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('getTimezone', () => {
|
|
258
|
+
it('should return a string', () => {
|
|
259
|
+
const timezone = getTimezone();
|
|
260
|
+
expect(typeof timezone).toBe('string');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should return a valid IANA timezone or empty string', () => {
|
|
264
|
+
const timezone = getTimezone();
|
|
265
|
+
// Should be empty or a valid IANA timezone like "America/New_York" or "UTC"
|
|
266
|
+
if (timezone) {
|
|
267
|
+
expect(timezone).toMatch(/^([A-Za-z_]+\/[A-Za-z_]+|UTC)$/);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
package/src/utils.ts
CHANGED
|
@@ -310,3 +310,24 @@ export function getDeviceModel(): string {
|
|
|
310
310
|
export function delay(ms: number): Promise<void> {
|
|
311
311
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
312
312
|
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get the user's locale.
|
|
316
|
+
*/
|
|
317
|
+
export function getLocale(): string {
|
|
318
|
+
if (typeof navigator !== 'undefined') {
|
|
319
|
+
return navigator.language || 'en';
|
|
320
|
+
}
|
|
321
|
+
return 'en';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the user's timezone.
|
|
326
|
+
*/
|
|
327
|
+
export function getTimezone(): string {
|
|
328
|
+
try {
|
|
329
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || '';
|
|
330
|
+
} catch {
|
|
331
|
+
return '';
|
|
332
|
+
}
|
|
333
|
+
}
|