@mostly-good-metrics/javascript 0.2.0 → 0.4.1

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/src/client.ts CHANGED
@@ -19,7 +19,9 @@ import {
19
19
  generateUUID,
20
20
  getDeviceModel,
21
21
  getISOTimestamp,
22
+ getLocale,
22
23
  getOSVersion,
24
+ getTimezone,
23
25
  resolveConfiguration,
24
26
  sanitizeProperties,
25
27
  validateEventName,
@@ -164,6 +166,41 @@ export class MostlyGoodMetrics {
164
166
  return MostlyGoodMetrics.instance?.getPendingEventCount() ?? Promise.resolve(0);
165
167
  }
166
168
 
169
+ /**
170
+ * Set a single super property that will be included with every event.
171
+ */
172
+ static setSuperProperty(key: string, value: EventProperties[string]): void {
173
+ MostlyGoodMetrics.instance?.setSuperProperty(key, value);
174
+ }
175
+
176
+ /**
177
+ * Set multiple super properties at once.
178
+ */
179
+ static setSuperProperties(properties: EventProperties): void {
180
+ MostlyGoodMetrics.instance?.setSuperProperties(properties);
181
+ }
182
+
183
+ /**
184
+ * Remove a single super property.
185
+ */
186
+ static removeSuperProperty(key: string): void {
187
+ MostlyGoodMetrics.instance?.removeSuperProperty(key);
188
+ }
189
+
190
+ /**
191
+ * Clear all super properties.
192
+ */
193
+ static clearSuperProperties(): void {
194
+ MostlyGoodMetrics.instance?.clearSuperProperties();
195
+ }
196
+
197
+ /**
198
+ * Get all current super properties.
199
+ */
200
+ static getSuperProperties(): EventProperties {
201
+ return MostlyGoodMetrics.instance?.getSuperProperties() ?? {};
202
+ }
203
+
167
204
  // ============================================================
168
205
  // Instance properties
169
206
  // ============================================================
@@ -212,17 +249,21 @@ export class MostlyGoodMetrics {
212
249
  }
213
250
 
214
251
  const sanitizedProperties = sanitizeProperties(properties);
252
+ const superProperties = persistence.getSuperProperties();
215
253
 
216
- // Add system properties
254
+ // Merge properties: super properties < event properties < system properties
255
+ // Event properties override super properties, system properties are always added
217
256
  const mergedProperties: EventProperties = {
257
+ ...superProperties,
258
+ ...sanitizedProperties,
218
259
  [SystemProperties.DEVICE_TYPE]: detectDeviceType(),
219
260
  [SystemProperties.DEVICE_MODEL]: getDeviceModel(),
220
261
  [SystemProperties.SDK]: this.config.sdk,
221
- ...sanitizedProperties,
222
262
  };
223
263
 
224
264
  const event: MGMEvent = {
225
265
  name,
266
+ client_event_id: generateUUID(),
226
267
  timestamp: getISOTimestamp(),
227
268
  userId: this.userId ?? undefined,
228
269
  sessionId: this.sessionIdValue,
@@ -230,6 +271,8 @@ export class MostlyGoodMetrics {
230
271
  appVersion: this.config.appVersion || undefined,
231
272
  osVersion: this.config.osVersion || getOSVersion() || undefined,
232
273
  environment: this.config.environment,
274
+ locale: getLocale(),
275
+ timezone: getTimezone(),
233
276
  properties: Object.keys(mergedProperties).length > 0 ? mergedProperties : undefined,
234
277
  };
235
278
 
@@ -307,6 +350,45 @@ export class MostlyGoodMetrics {
307
350
  return this.storage.eventCount();
308
351
  }
309
352
 
353
+ /**
354
+ * Set a single super property that will be included with every event.
355
+ */
356
+ setSuperProperty(key: string, value: EventProperties[string]): void {
357
+ logger.debug(`Setting super property: ${key}`);
358
+ persistence.setSuperProperty(key, value);
359
+ }
360
+
361
+ /**
362
+ * Set multiple super properties at once.
363
+ */
364
+ setSuperProperties(properties: EventProperties): void {
365
+ logger.debug(`Setting super properties: ${Object.keys(properties).join(', ')}`);
366
+ persistence.setSuperProperties(properties);
367
+ }
368
+
369
+ /**
370
+ * Remove a single super property.
371
+ */
372
+ removeSuperProperty(key: string): void {
373
+ logger.debug(`Removing super property: ${key}`);
374
+ persistence.removeSuperProperty(key);
375
+ }
376
+
377
+ /**
378
+ * Clear all super properties.
379
+ */
380
+ clearSuperProperties(): void {
381
+ logger.debug('Clearing all super properties');
382
+ persistence.clearSuperProperties();
383
+ }
384
+
385
+ /**
386
+ * Get all current super properties.
387
+ */
388
+ getSuperProperties(): EventProperties {
389
+ return persistence.getSuperProperties();
390
+ }
391
+
310
392
  /**
311
393
  * Clean up resources (stop timers, etc.).
312
394
  */
@@ -380,6 +462,8 @@ export class MostlyGoodMetrics {
380
462
  userId: this.userId ?? undefined,
381
463
  sessionId: this.sessionIdValue,
382
464
  environment: this.config.environment,
465
+ locale: getLocale(),
466
+ timezone: getTimezone(),
383
467
  };
384
468
 
385
469
  return { events, context };
@@ -0,0 +1,244 @@
1
+ import { TextEncoder } from 'util';
2
+ import { FetchNetworkClient } from './network';
3
+ import { MGMEventsPayload, ResolvedConfiguration } from './types';
4
+
5
+ // Polyfill TextEncoder for Jest environment
6
+ global.TextEncoder = TextEncoder;
7
+
8
+ describe('FetchNetworkClient', () => {
9
+ let networkClient: FetchNetworkClient;
10
+ let mockFetch: jest.Mock;
11
+ let capturedHeaders: Record<string, string>;
12
+
13
+ const createMockConfig = (
14
+ overrides: Partial<ResolvedConfiguration> = {}
15
+ ): ResolvedConfiguration => ({
16
+ apiKey: 'test-api-key',
17
+ baseURL: 'https://api.example.com',
18
+ maxStoredEvents: 1000,
19
+ flushIntervalMs: 30000,
20
+ maxBatchSize: 100,
21
+ environment: 'production',
22
+ platform: 'web',
23
+ sdk: 'javascript',
24
+ trackAppLifecycleEvents: false,
25
+ ...overrides,
26
+ });
27
+
28
+ const createMockPayload = (): MGMEventsPayload => ({
29
+ events: [
30
+ {
31
+ name: 'test_event',
32
+ client_event_id: '123e4567-e89b-12d3-a456-426614174000',
33
+ timestamp: new Date().toISOString(),
34
+ platform: 'web',
35
+ environment: 'production',
36
+ },
37
+ ],
38
+ });
39
+
40
+ beforeEach(() => {
41
+ networkClient = new FetchNetworkClient();
42
+ capturedHeaders = {};
43
+
44
+ mockFetch = jest.fn().mockImplementation((_url: string, options: RequestInit) => {
45
+ capturedHeaders = options.headers as Record<string, string>;
46
+ return Promise.resolve({
47
+ status: 204,
48
+ headers: new Headers(),
49
+ });
50
+ });
51
+
52
+ global.fetch = mockFetch;
53
+ });
54
+
55
+ afterEach(() => {
56
+ jest.restoreAllMocks();
57
+ });
58
+
59
+ describe('SDK identification headers', () => {
60
+ it('should include X-MGM-SDK header from config', async () => {
61
+ const config = createMockConfig({ sdk: 'react-native' });
62
+ await networkClient.sendEvents(createMockPayload(), config);
63
+
64
+ expect(capturedHeaders['X-MGM-SDK']).toBe('react-native');
65
+ });
66
+
67
+ it('should include X-MGM-SDK-Version header', async () => {
68
+ const config = createMockConfig();
69
+ await networkClient.sendEvents(createMockPayload(), config);
70
+
71
+ expect(capturedHeaders['X-MGM-SDK-Version']).toBeDefined();
72
+ expect(capturedHeaders['X-MGM-SDK-Version']).toMatch(/^\d+\.\d+\.\d+$/);
73
+ });
74
+
75
+ it('should include X-MGM-Platform header from config', async () => {
76
+ const config = createMockConfig({ platform: 'ios' });
77
+ await networkClient.sendEvents(createMockPayload(), config);
78
+
79
+ expect(capturedHeaders['X-MGM-Platform']).toBe('ios');
80
+ });
81
+
82
+ it('should include X-MGM-Platform-Version when osVersion is configured', async () => {
83
+ const config = createMockConfig({ osVersion: '17.0' });
84
+ await networkClient.sendEvents(createMockPayload(), config);
85
+
86
+ expect(capturedHeaders['X-MGM-Platform-Version']).toBe('17.0');
87
+ });
88
+
89
+ it('should include X-MGM-Key header with API key', async () => {
90
+ const config = createMockConfig({ apiKey: 'my-secret-key' });
91
+ await networkClient.sendEvents(createMockPayload(), config);
92
+
93
+ expect(capturedHeaders['X-MGM-Key']).toBe('my-secret-key');
94
+ });
95
+
96
+ it('should include X-MGM-Bundle-Id when bundleId is configured', async () => {
97
+ const config = createMockConfig({ bundleId: 'com.example.app' });
98
+ await networkClient.sendEvents(createMockPayload(), config);
99
+
100
+ expect(capturedHeaders['X-MGM-Bundle-Id']).toBe('com.example.app');
101
+ });
102
+
103
+ it('should not include X-MGM-Bundle-Id when bundleId is not configured', async () => {
104
+ const config = createMockConfig();
105
+ await networkClient.sendEvents(createMockPayload(), config);
106
+
107
+ expect(capturedHeaders['X-MGM-Bundle-Id']).toBeUndefined();
108
+ });
109
+
110
+ it('should include all SDK headers together', async () => {
111
+ const config = createMockConfig({
112
+ apiKey: 'test-key',
113
+ sdk: 'javascript',
114
+ platform: 'web',
115
+ osVersion: '14.0',
116
+ bundleId: 'com.test.app',
117
+ });
118
+ await networkClient.sendEvents(createMockPayload(), config);
119
+
120
+ expect(capturedHeaders['X-MGM-Key']).toBe('test-key');
121
+ expect(capturedHeaders['X-MGM-SDK']).toBe('javascript');
122
+ expect(capturedHeaders['X-MGM-SDK-Version']).toBeDefined();
123
+ expect(capturedHeaders['X-MGM-Platform']).toBe('web');
124
+ expect(capturedHeaders['X-MGM-Platform-Version']).toBe('14.0');
125
+ expect(capturedHeaders['X-MGM-Bundle-Id']).toBe('com.test.app');
126
+ });
127
+ });
128
+
129
+ describe('sendEvents', () => {
130
+ it('should return success on 204 response', async () => {
131
+ mockFetch.mockResolvedValueOnce({
132
+ status: 204,
133
+ headers: new Headers(),
134
+ });
135
+
136
+ const result = await networkClient.sendEvents(createMockPayload(), createMockConfig());
137
+
138
+ expect(result.success).toBe(true);
139
+ });
140
+
141
+ it('should return success on 200 response', async () => {
142
+ mockFetch.mockResolvedValueOnce({
143
+ status: 200,
144
+ headers: new Headers(),
145
+ });
146
+
147
+ const result = await networkClient.sendEvents(createMockPayload(), createMockConfig());
148
+
149
+ expect(result.success).toBe(true);
150
+ });
151
+
152
+ it('should handle rate limiting (429) with Retry-After header', async () => {
153
+ mockFetch.mockResolvedValueOnce({
154
+ status: 429,
155
+ headers: new Headers({ 'Retry-After': '120' }),
156
+ });
157
+
158
+ const result = await networkClient.sendEvents(createMockPayload(), createMockConfig());
159
+
160
+ expect(result.success).toBe(false);
161
+ expect(result.shouldRetry).toBe(true);
162
+ expect(result.error?.type).toBe('RATE_LIMITED');
163
+ });
164
+
165
+ it('should handle client errors (4xx) without retry', async () => {
166
+ mockFetch.mockResolvedValueOnce({
167
+ status: 400,
168
+ headers: new Headers(),
169
+ });
170
+
171
+ const result = await networkClient.sendEvents(createMockPayload(), createMockConfig());
172
+
173
+ expect(result.success).toBe(false);
174
+ expect(result.shouldRetry).toBe(false);
175
+ expect(result.error?.type).toBe('BAD_REQUEST');
176
+ });
177
+
178
+ it('should handle server errors (5xx) with retry', async () => {
179
+ mockFetch.mockResolvedValueOnce({
180
+ status: 500,
181
+ headers: new Headers(),
182
+ });
183
+
184
+ const result = await networkClient.sendEvents(createMockPayload(), createMockConfig());
185
+
186
+ expect(result.success).toBe(false);
187
+ expect(result.shouldRetry).toBe(true);
188
+ expect(result.error?.type).toBe('SERVER_ERROR');
189
+ });
190
+
191
+ it('should handle network errors with retry', async () => {
192
+ mockFetch.mockRejectedValueOnce(new Error('Network failure'));
193
+
194
+ const result = await networkClient.sendEvents(createMockPayload(), createMockConfig());
195
+
196
+ expect(result.success).toBe(false);
197
+ expect(result.shouldRetry).toBe(true);
198
+ expect(result.error?.type).toBe('NETWORK_ERROR');
199
+ });
200
+
201
+ it('should send to correct endpoint', async () => {
202
+ const config = createMockConfig({ baseURL: 'https://api.test.com' });
203
+ await networkClient.sendEvents(createMockPayload(), config);
204
+
205
+ expect(mockFetch).toHaveBeenCalledWith('https://api.test.com/v1/events', expect.any(Object));
206
+ });
207
+
208
+ it('should use POST method', async () => {
209
+ await networkClient.sendEvents(createMockPayload(), createMockConfig());
210
+
211
+ expect(mockFetch).toHaveBeenCalledWith(
212
+ expect.any(String),
213
+ expect.objectContaining({ method: 'POST' })
214
+ );
215
+ });
216
+ });
217
+
218
+ describe('rate limiting', () => {
219
+ it('should not be rate limited initially', () => {
220
+ expect(networkClient.isRateLimited()).toBe(false);
221
+ });
222
+
223
+ it('should return null for retry time initially', () => {
224
+ expect(networkClient.getRetryAfterTime()).toBeNull();
225
+ });
226
+
227
+ it('should skip requests when rate limited', async () => {
228
+ // First request gets rate limited
229
+ mockFetch.mockResolvedValueOnce({
230
+ status: 429,
231
+ headers: new Headers({ 'Retry-After': '60' }),
232
+ });
233
+
234
+ await networkClient.sendEvents(createMockPayload(), createMockConfig());
235
+
236
+ // Second request should be skipped
237
+ const result = await networkClient.sendEvents(createMockPayload(), createMockConfig());
238
+
239
+ expect(result.success).toBe(false);
240
+ expect(result.error?.type).toBe('RATE_LIMITED');
241
+ expect(mockFetch).toHaveBeenCalledTimes(1); // Only first request was made
242
+ });
243
+ });
244
+ });
package/src/network.ts CHANGED
@@ -7,9 +7,11 @@ import {
7
7
  ResolvedConfiguration,
8
8
  SendResult,
9
9
  } from './types';
10
+ import { getOSVersion } from './utils';
10
11
 
11
12
  const EVENTS_ENDPOINT = '/v1/events';
12
13
  const REQUEST_TIMEOUT_MS = 60000; // 60 seconds
14
+ const SDK_VERSION = '0.4.0';
13
15
 
14
16
  /**
15
17
  * Compress data using gzip if available (browser CompressionStream API).
@@ -72,9 +74,14 @@ export class FetchNetworkClient implements INetworkClient {
72
74
  const jsonBody = JSON.stringify(payload);
73
75
  const { data, compressed } = await compressIfNeeded(jsonBody);
74
76
 
77
+ const osVersion = config.osVersion || getOSVersion();
75
78
  const headers: Record<string, string> = {
76
79
  'Content-Type': 'application/json',
77
80
  'X-MGM-Key': config.apiKey,
81
+ 'X-MGM-SDK': config.sdk,
82
+ 'X-MGM-SDK-Version': SDK_VERSION,
83
+ 'X-MGM-Platform': config.platform,
84
+ ...(osVersion && { 'X-MGM-Platform-Version': osVersion }),
78
85
  };
79
86
 
80
87
  if (config.bundleId) {
@@ -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();