@mostly-good-metrics/javascript 0.1.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.
@@ -144,6 +144,70 @@ describe('MostlyGoodMetrics', () => {
144
144
  expect(events[0].environment).toBe('production');
145
145
  expect(events[0].platform).toBeDefined();
146
146
  });
147
+
148
+ it('should include $sdk property defaulting to javascript', async () => {
149
+ MostlyGoodMetrics.track('test_event');
150
+
151
+ await new Promise((resolve) => setTimeout(resolve, 10));
152
+
153
+ const events = await storage.fetchEvents(1);
154
+ expect(events[0].properties?.$sdk).toBe('javascript');
155
+ });
156
+ });
157
+
158
+ describe('platform and sdk configuration', () => {
159
+ it('should use configured platform', async () => {
160
+ MostlyGoodMetrics.reset();
161
+ MostlyGoodMetrics.configure({
162
+ apiKey: 'test-key',
163
+ storage,
164
+ networkClient,
165
+ trackAppLifecycleEvents: false,
166
+ platform: 'ios',
167
+ });
168
+
169
+ MostlyGoodMetrics.track('test_event');
170
+ await new Promise((resolve) => setTimeout(resolve, 10));
171
+
172
+ const events = await storage.fetchEvents(1);
173
+ expect(events[0].platform).toBe('ios');
174
+ });
175
+
176
+ it('should use configured sdk', async () => {
177
+ MostlyGoodMetrics.reset();
178
+ MostlyGoodMetrics.configure({
179
+ apiKey: 'test-key',
180
+ storage,
181
+ networkClient,
182
+ trackAppLifecycleEvents: false,
183
+ sdk: 'react-native',
184
+ });
185
+
186
+ MostlyGoodMetrics.track('test_event');
187
+ await new Promise((resolve) => setTimeout(resolve, 10));
188
+
189
+ const events = await storage.fetchEvents(1);
190
+ expect(events[0].properties?.$sdk).toBe('react-native');
191
+ });
192
+
193
+ it('should allow both platform and sdk to be configured together', async () => {
194
+ MostlyGoodMetrics.reset();
195
+ MostlyGoodMetrics.configure({
196
+ apiKey: 'test-key',
197
+ storage,
198
+ networkClient,
199
+ trackAppLifecycleEvents: false,
200
+ platform: 'android',
201
+ sdk: 'react-native',
202
+ });
203
+
204
+ MostlyGoodMetrics.track('test_event');
205
+ await new Promise((resolve) => setTimeout(resolve, 10));
206
+
207
+ const events = await storage.fetchEvents(1);
208
+ expect(events[0].platform).toBe('android');
209
+ expect(events[0].properties?.$sdk).toBe('react-native');
210
+ });
147
211
  });
148
212
 
149
213
  describe('identify', () => {
@@ -342,5 +406,122 @@ describe('MostlyGoodMetrics', () => {
342
406
  await expect(MostlyGoodMetrics.clearPendingEvents()).resolves.not.toThrow();
343
407
  await expect(MostlyGoodMetrics.getPendingEventCount()).resolves.toBe(0);
344
408
  });
409
+
410
+ it('should not throw for super property methods when not configured', () => {
411
+ expect(() => MostlyGoodMetrics.setSuperProperty('key', 'value')).not.toThrow();
412
+ expect(() => MostlyGoodMetrics.setSuperProperties({ key: 'value' })).not.toThrow();
413
+ expect(() => MostlyGoodMetrics.removeSuperProperty('key')).not.toThrow();
414
+ expect(() => MostlyGoodMetrics.clearSuperProperties()).not.toThrow();
415
+ expect(MostlyGoodMetrics.getSuperProperties()).toEqual({});
416
+ });
417
+ });
418
+
419
+ describe('super properties', () => {
420
+ beforeEach(() => {
421
+ MostlyGoodMetrics.configure({
422
+ apiKey: 'test-key',
423
+ storage,
424
+ networkClient,
425
+ trackAppLifecycleEvents: false,
426
+ });
427
+ });
428
+
429
+ afterEach(() => {
430
+ MostlyGoodMetrics.clearSuperProperties();
431
+ });
432
+
433
+ it('should set a single super property', () => {
434
+ MostlyGoodMetrics.setSuperProperty('plan', 'premium');
435
+
436
+ const props = MostlyGoodMetrics.getSuperProperties();
437
+ expect(props.plan).toBe('premium');
438
+ });
439
+
440
+ it('should set multiple super properties at once', () => {
441
+ MostlyGoodMetrics.setSuperProperties({
442
+ plan: 'enterprise',
443
+ role: 'admin',
444
+ });
445
+
446
+ const props = MostlyGoodMetrics.getSuperProperties();
447
+ expect(props.plan).toBe('enterprise');
448
+ expect(props.role).toBe('admin');
449
+ });
450
+
451
+ it('should merge with existing super properties', () => {
452
+ MostlyGoodMetrics.setSuperProperty('existing', 'value');
453
+ MostlyGoodMetrics.setSuperProperties({
454
+ new_prop: 'new_value',
455
+ });
456
+
457
+ const props = MostlyGoodMetrics.getSuperProperties();
458
+ expect(props.existing).toBe('value');
459
+ expect(props.new_prop).toBe('new_value');
460
+ });
461
+
462
+ it('should remove a single super property', () => {
463
+ MostlyGoodMetrics.setSuperProperties({
464
+ keep: 'this',
465
+ remove: 'this',
466
+ });
467
+
468
+ MostlyGoodMetrics.removeSuperProperty('remove');
469
+
470
+ const props = MostlyGoodMetrics.getSuperProperties();
471
+ expect(props.keep).toBe('this');
472
+ expect(props.remove).toBeUndefined();
473
+ });
474
+
475
+ it('should clear all super properties', () => {
476
+ MostlyGoodMetrics.setSuperProperties({
477
+ prop1: 'value1',
478
+ prop2: 'value2',
479
+ });
480
+
481
+ MostlyGoodMetrics.clearSuperProperties();
482
+
483
+ const props = MostlyGoodMetrics.getSuperProperties();
484
+ expect(Object.keys(props)).toHaveLength(0);
485
+ });
486
+
487
+ it('should include super properties in tracked events', async () => {
488
+ MostlyGoodMetrics.setSuperProperties({
489
+ user_tier: 'gold',
490
+ source: 'mobile',
491
+ });
492
+
493
+ MostlyGoodMetrics.track('purchase');
494
+
495
+ await new Promise((resolve) => setTimeout(resolve, 10));
496
+
497
+ const events = await storage.fetchEvents(1);
498
+ expect(events[0].properties?.user_tier).toBe('gold');
499
+ expect(events[0].properties?.source).toBe('mobile');
500
+ });
501
+
502
+ it('should allow event properties to override super properties', async () => {
503
+ MostlyGoodMetrics.setSuperProperty('source', 'default');
504
+
505
+ MostlyGoodMetrics.track('click', {
506
+ source: 'override',
507
+ });
508
+
509
+ await new Promise((resolve) => setTimeout(resolve, 10));
510
+
511
+ const events = await storage.fetchEvents(1);
512
+ expect(events[0].properties?.source).toBe('override');
513
+ });
514
+
515
+ it('should not allow super properties to override system properties', async () => {
516
+ MostlyGoodMetrics.setSuperProperty('$device_type', 'hacked');
517
+
518
+ MostlyGoodMetrics.track('test_event');
519
+
520
+ await new Promise((resolve) => setTimeout(resolve, 10));
521
+
522
+ const events = await storage.fetchEvents(1);
523
+ // System properties should take precedence
524
+ expect(events[0].properties?.$device_type).not.toBe('hacked');
525
+ });
345
526
  });
346
527
  });
package/src/client.ts CHANGED
@@ -16,11 +16,12 @@ import {
16
16
  import {
17
17
  delay,
18
18
  detectDeviceType,
19
- detectPlatform,
20
19
  generateUUID,
21
20
  getDeviceModel,
22
21
  getISOTimestamp,
22
+ getLocale,
23
23
  getOSVersion,
24
+ getTimezone,
24
25
  resolveConfiguration,
25
26
  sanitizeProperties,
26
27
  validateEventName,
@@ -165,6 +166,41 @@ export class MostlyGoodMetrics {
165
166
  return MostlyGoodMetrics.instance?.getPendingEventCount() ?? Promise.resolve(0);
166
167
  }
167
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
+
168
204
  // ============================================================
169
205
  // Instance properties
170
206
  // ============================================================
@@ -213,12 +249,16 @@ export class MostlyGoodMetrics {
213
249
  }
214
250
 
215
251
  const sanitizedProperties = sanitizeProperties(properties);
252
+ const superProperties = persistence.getSuperProperties();
216
253
 
217
- // Add system properties
254
+ // Merge properties: super properties < event properties < system properties
255
+ // Event properties override super properties, system properties are always added
218
256
  const mergedProperties: EventProperties = {
257
+ ...superProperties,
258
+ ...sanitizedProperties,
219
259
  [SystemProperties.DEVICE_TYPE]: detectDeviceType(),
220
260
  [SystemProperties.DEVICE_MODEL]: getDeviceModel(),
221
- ...sanitizedProperties,
261
+ [SystemProperties.SDK]: this.config.sdk,
222
262
  };
223
263
 
224
264
  const event: MGMEvent = {
@@ -226,10 +266,12 @@ export class MostlyGoodMetrics {
226
266
  timestamp: getISOTimestamp(),
227
267
  userId: this.userId ?? undefined,
228
268
  sessionId: this.sessionIdValue,
229
- platform: detectPlatform(),
269
+ platform: this.config.platform,
230
270
  appVersion: this.config.appVersion || undefined,
231
271
  osVersion: this.config.osVersion || getOSVersion() || undefined,
232
272
  environment: this.config.environment,
273
+ locale: getLocale(),
274
+ timezone: getTimezone(),
233
275
  properties: Object.keys(mergedProperties).length > 0 ? mergedProperties : undefined,
234
276
  };
235
277
 
@@ -307,6 +349,45 @@ export class MostlyGoodMetrics {
307
349
  return this.storage.eventCount();
308
350
  }
309
351
 
352
+ /**
353
+ * Set a single super property that will be included with every event.
354
+ */
355
+ setSuperProperty(key: string, value: EventProperties[string]): void {
356
+ logger.debug(`Setting super property: ${key}`);
357
+ persistence.setSuperProperty(key, value);
358
+ }
359
+
360
+ /**
361
+ * Set multiple super properties at once.
362
+ */
363
+ setSuperProperties(properties: EventProperties): void {
364
+ logger.debug(`Setting super properties: ${Object.keys(properties).join(', ')}`);
365
+ persistence.setSuperProperties(properties);
366
+ }
367
+
368
+ /**
369
+ * Remove a single super property.
370
+ */
371
+ removeSuperProperty(key: string): void {
372
+ logger.debug(`Removing super property: ${key}`);
373
+ persistence.removeSuperProperty(key);
374
+ }
375
+
376
+ /**
377
+ * Clear all super properties.
378
+ */
379
+ clearSuperProperties(): void {
380
+ logger.debug('Clearing all super properties');
381
+ persistence.clearSuperProperties();
382
+ }
383
+
384
+ /**
385
+ * Get all current super properties.
386
+ */
387
+ getSuperProperties(): EventProperties {
388
+ return persistence.getSuperProperties();
389
+ }
390
+
310
391
  /**
311
392
  * Clean up resources (stop timers, etc.).
312
393
  */
@@ -374,12 +455,14 @@ export class MostlyGoodMetrics {
374
455
 
375
456
  private buildPayload(events: MGMEvent[]): MGMEventsPayload {
376
457
  const context: MGMEventContext = {
377
- platform: detectPlatform(),
458
+ platform: this.config.platform,
378
459
  appVersion: this.config.appVersion || undefined,
379
460
  osVersion: this.config.osVersion || getOSVersion() || undefined,
380
461
  userId: this.userId ?? undefined,
381
462
  sessionId: this.sessionIdValue,
382
463
  environment: this.config.environment,
464
+ locale: getLocale(),
465
+ timezone: getTimezone(),
383
466
  };
384
467
 
385
468
  return { events, context };
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export type {
37
37
  MGMEventContext,
38
38
  MGMEventsPayload,
39
39
  Platform,
40
+ SDK,
40
41
  DeviceType,
41
42
  MGMErrorType,
42
43
  SendResult,
@@ -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
@@ -73,6 +73,18 @@ export interface MGMConfiguration {
73
73
  */
74
74
  osVersion?: string;
75
75
 
76
+ /**
77
+ * Override the auto-detected platform.
78
+ * Use this when wrapping the SDK (e.g., React Native should pass 'ios' or 'android').
79
+ */
80
+ platform?: Platform;
81
+
82
+ /**
83
+ * The SDK identifier. Auto-set to 'javascript' but can be overridden by wrapper SDKs.
84
+ * @default "javascript"
85
+ */
86
+ sdk?: SDK;
87
+
76
88
  /**
77
89
  * Custom storage adapter. If not provided, uses localStorage in browsers
78
90
  * or in-memory storage in non-browser environments.
@@ -147,6 +159,11 @@ export interface MGMEvent {
147
159
  */
148
160
  appVersion?: string;
149
161
 
162
+ /**
163
+ * The app build number (separate from version).
164
+ */
165
+ appBuildNumber?: string;
166
+
150
167
  /**
151
168
  * The OS version string.
152
169
  */
@@ -157,6 +174,21 @@ export interface MGMEvent {
157
174
  */
158
175
  environment: string;
159
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
+
160
192
  /**
161
193
  * Custom properties attached to this event.
162
194
  */
@@ -170,10 +202,14 @@ export interface MGMEvent {
170
202
  export interface MGMEventContext {
171
203
  platform: Platform;
172
204
  appVersion?: string;
205
+ appBuildNumber?: string;
173
206
  osVersion?: string;
174
207
  userId?: string;
175
208
  sessionId?: string;
176
209
  environment: string;
210
+ deviceManufacturer?: string;
211
+ locale?: string;
212
+ timezone?: string;
177
213
  }
178
214
 
179
215
  /**
@@ -185,9 +221,14 @@ export interface MGMEventsPayload {
185
221
  }
186
222
 
187
223
  /**
188
- * Supported platforms.
224
+ * Supported platforms (the actual OS/runtime).
225
+ */
226
+ export type Platform = 'web' | 'ios' | 'android' | 'node';
227
+
228
+ /**
229
+ * SDK identifiers.
189
230
  */
190
- export type Platform = 'web' | 'ios' | 'android' | 'react-native' | 'expo' | 'node';
231
+ export type SDK = 'javascript' | 'react-native' | 'swift' | 'android';
191
232
 
192
233
  /**
193
234
  * Device types for automatic device detection.
@@ -310,6 +351,7 @@ export const SystemProperties = {
310
351
  DEVICE_MODEL: '$device_model',
311
352
  VERSION: '$version',
312
353
  PREVIOUS_VERSION: '$previous_version',
354
+ SDK: '$sdk',
313
355
  } as const;
314
356
 
315
357
  /**