@mostly-good-metrics/javascript 0.5.0 → 0.6.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 +210 -5
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/index.js +5 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/storage.js +46 -0
- package/dist/cjs/storage.js.map +1 -1
- package/dist/cjs/types.js +1 -0
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/client.js +210 -5
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/storage.js +46 -0
- package/dist/esm/storage.js.map +1 -1
- package/dist/esm/types.js +1 -0
- package/dist/esm/types.js.map +1 -1
- package/dist/types/client.d.ts +72 -5
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/storage.d.ts +20 -0
- package/dist/types/storage.d.ts.map +1 -1
- package/dist/types/types.d.ts +33 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.test.ts +346 -0
- package/src/client.ts +249 -5
- package/src/index.ts +8 -2
- package/src/storage.ts +51 -0
- package/src/types.ts +38 -0
package/src/client.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { createDefaultNetworkClient } from './network';
|
|
|
3
3
|
import { createDefaultStorage, persistence } from './storage';
|
|
4
4
|
import {
|
|
5
5
|
EventProperties,
|
|
6
|
+
Experiment,
|
|
7
|
+
ExperimentsResponse,
|
|
6
8
|
IEventStorage,
|
|
7
9
|
INetworkClient,
|
|
8
10
|
MGMConfiguration,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
ResolvedConfiguration,
|
|
13
15
|
SystemEvents,
|
|
14
16
|
SystemProperties,
|
|
17
|
+
UserProfile,
|
|
15
18
|
} from './types';
|
|
16
19
|
import {
|
|
17
20
|
delay,
|
|
@@ -46,6 +49,12 @@ export class MostlyGoodMetrics {
|
|
|
46
49
|
private anonymousIdValue: string;
|
|
47
50
|
private lifecycleSetup = false;
|
|
48
51
|
|
|
52
|
+
// A/B testing state
|
|
53
|
+
private experiments: Map<string, Experiment> = new Map();
|
|
54
|
+
private experimentsLoaded = false;
|
|
55
|
+
private experimentsReadyResolve: (() => void) | null = null;
|
|
56
|
+
private experimentsReadyPromise: Promise<void>;
|
|
57
|
+
|
|
49
58
|
/**
|
|
50
59
|
* Private constructor - use `configure` to create an instance.
|
|
51
60
|
*/
|
|
@@ -69,6 +78,11 @@ export class MostlyGoodMetrics {
|
|
|
69
78
|
// Initialize network client
|
|
70
79
|
this.networkClient = this.config.networkClient ?? createDefaultNetworkClient();
|
|
71
80
|
|
|
81
|
+
// Initialize experiments ready promise
|
|
82
|
+
this.experimentsReadyPromise = new Promise((resolve) => {
|
|
83
|
+
this.experimentsReadyResolve = resolve;
|
|
84
|
+
});
|
|
85
|
+
|
|
72
86
|
logger.info(`MostlyGoodMetrics initialized with environment: ${this.config.environment}`);
|
|
73
87
|
|
|
74
88
|
// Start auto-flush timer
|
|
@@ -78,6 +92,9 @@ export class MostlyGoodMetrics {
|
|
|
78
92
|
if (this.config.trackAppLifecycleEvents) {
|
|
79
93
|
this.setupLifecycleTracking();
|
|
80
94
|
}
|
|
95
|
+
|
|
96
|
+
// Fetch experiments in background
|
|
97
|
+
void this.fetchExperiments();
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
/**
|
|
@@ -134,10 +151,12 @@ export class MostlyGoodMetrics {
|
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
/**
|
|
137
|
-
* Identify the current user.
|
|
154
|
+
* Identify the current user with optional profile data.
|
|
155
|
+
* @param userId The user's unique identifier
|
|
156
|
+
* @param profile Optional profile data (email, name)
|
|
138
157
|
*/
|
|
139
|
-
static identify(userId: string): void {
|
|
140
|
-
MostlyGoodMetrics.instance?.identify(userId);
|
|
158
|
+
static identify(userId: string, profile?: UserProfile): void {
|
|
159
|
+
MostlyGoodMetrics.instance?.identify(userId, profile);
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
/**
|
|
@@ -210,6 +229,22 @@ export class MostlyGoodMetrics {
|
|
|
210
229
|
return MostlyGoodMetrics.instance?.getSuperProperties() ?? {};
|
|
211
230
|
}
|
|
212
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Get the variant for an experiment.
|
|
234
|
+
* Returns the assigned variant ('a', 'b', etc.) or null if experiment not found.
|
|
235
|
+
*/
|
|
236
|
+
static getVariant(experimentName: string): string | null {
|
|
237
|
+
return MostlyGoodMetrics.instance?.getVariant(experimentName) ?? null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Returns a promise that resolves when experiments have been loaded.
|
|
242
|
+
* Useful for waiting before calling getVariant() to ensure server-side experiments are available.
|
|
243
|
+
*/
|
|
244
|
+
static ready(): Promise<void> {
|
|
245
|
+
return MostlyGoodMetrics.instance?.ready() ?? Promise.resolve();
|
|
246
|
+
}
|
|
247
|
+
|
|
213
248
|
// =====================================================
|
|
214
249
|
// Instance properties
|
|
215
250
|
// =====================================================
|
|
@@ -306,9 +341,14 @@ export class MostlyGoodMetrics {
|
|
|
306
341
|
}
|
|
307
342
|
|
|
308
343
|
/**
|
|
309
|
-
* Identify the current user.
|
|
344
|
+
* Identify the current user with optional profile data.
|
|
345
|
+
* Profile data is sent to the backend via the $identify event.
|
|
346
|
+
* Debouncing: only sends $identify if payload changed or >24h since last send.
|
|
347
|
+
*
|
|
348
|
+
* @param userId The user's unique identifier
|
|
349
|
+
* @param profile Optional profile data (email, name)
|
|
310
350
|
*/
|
|
311
|
-
identify(userId: string): void {
|
|
351
|
+
identify(userId: string, profile?: UserProfile): void {
|
|
312
352
|
if (!userId) {
|
|
313
353
|
logger.warn('identify called with empty userId');
|
|
314
354
|
return;
|
|
@@ -316,14 +356,78 @@ export class MostlyGoodMetrics {
|
|
|
316
356
|
|
|
317
357
|
logger.debug(`Identifying user: ${userId}`);
|
|
318
358
|
persistence.setUserId(userId);
|
|
359
|
+
|
|
360
|
+
// If profile data is provided, check if we should send $identify event
|
|
361
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional truthy check for non-empty strings
|
|
362
|
+
if (profile && (profile.email || profile.name)) {
|
|
363
|
+
this.sendIdentifyEventIfNeeded(userId, profile);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Send $identify event if debounce conditions are met.
|
|
369
|
+
* Only sends if: hash changed OR more than 24 hours since last send.
|
|
370
|
+
*/
|
|
371
|
+
private sendIdentifyEventIfNeeded(userId: string, profile: UserProfile): void {
|
|
372
|
+
// Compute hash of the identify payload
|
|
373
|
+
const payloadString = JSON.stringify({ userId, email: profile.email, name: profile.name });
|
|
374
|
+
const currentHash = this.simpleHash(payloadString);
|
|
375
|
+
|
|
376
|
+
const storedHash = persistence.getIdentifyHash();
|
|
377
|
+
const lastSentAt = persistence.getIdentifyLastSentAt();
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
const twentyFourHoursMs = 24 * 60 * 60 * 1000;
|
|
380
|
+
|
|
381
|
+
const hashChanged = storedHash !== currentHash;
|
|
382
|
+
const expiredTime = !lastSentAt || now - lastSentAt > twentyFourHoursMs;
|
|
383
|
+
|
|
384
|
+
if (hashChanged || expiredTime) {
|
|
385
|
+
logger.debug(
|
|
386
|
+
`Sending $identify event (hashChanged=${hashChanged}, expiredTime=${expiredTime})`
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Build properties object with only defined values
|
|
390
|
+
const properties: EventProperties = {};
|
|
391
|
+
if (profile.email) {
|
|
392
|
+
properties.email = profile.email;
|
|
393
|
+
}
|
|
394
|
+
if (profile.name) {
|
|
395
|
+
properties.name = profile.name;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Track the $identify event
|
|
399
|
+
this.track(SystemEvents.IDENTIFY, properties);
|
|
400
|
+
|
|
401
|
+
// Update stored hash and timestamp
|
|
402
|
+
persistence.setIdentifyHash(currentHash);
|
|
403
|
+
persistence.setIdentifyLastSentAt(now);
|
|
404
|
+
} else {
|
|
405
|
+
logger.debug('Skipping $identify event (debounced)');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Simple hash function for debouncing.
|
|
411
|
+
* Uses a basic string hash - not cryptographic, just for comparison.
|
|
412
|
+
*/
|
|
413
|
+
private simpleHash(str: string): string {
|
|
414
|
+
let hash = 0;
|
|
415
|
+
for (let i = 0; i < str.length; i++) {
|
|
416
|
+
const char = str.charCodeAt(i);
|
|
417
|
+
hash = (hash << 5) - hash + char;
|
|
418
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
419
|
+
}
|
|
420
|
+
return hash.toString(16);
|
|
319
421
|
}
|
|
320
422
|
|
|
321
423
|
/**
|
|
322
424
|
* Reset user identity.
|
|
425
|
+
* Clears the user ID and identify debounce state.
|
|
323
426
|
*/
|
|
324
427
|
resetIdentity(): void {
|
|
325
428
|
logger.debug('Resetting user identity');
|
|
326
429
|
persistence.setUserId(null);
|
|
430
|
+
persistence.clearIdentifyState();
|
|
327
431
|
}
|
|
328
432
|
|
|
329
433
|
/**
|
|
@@ -407,6 +511,63 @@ export class MostlyGoodMetrics {
|
|
|
407
511
|
return persistence.getSuperProperties();
|
|
408
512
|
}
|
|
409
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Get the variant for an experiment.
|
|
516
|
+
* Returns the assigned variant ('a', 'b', etc.) or null if experiment not found.
|
|
517
|
+
*
|
|
518
|
+
* Assignment is deterministic based on userId + experimentName, so the same
|
|
519
|
+
* user always gets the same variant for the same experiment.
|
|
520
|
+
*
|
|
521
|
+
* The variant is automatically stored as a super property (experiment_{name}: 'variant')
|
|
522
|
+
* so it's attached to all subsequent events.
|
|
523
|
+
*/
|
|
524
|
+
getVariant(experimentName: string): string | null {
|
|
525
|
+
if (!experimentName) {
|
|
526
|
+
logger.warn('getVariant called with empty experimentName');
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Check if we have this experiment cached
|
|
531
|
+
const experiment = this.experiments.get(experimentName);
|
|
532
|
+
|
|
533
|
+
if (!experiment) {
|
|
534
|
+
if (this.experimentsLoaded) {
|
|
535
|
+
// Experiments loaded but this one doesn't exist
|
|
536
|
+
logger.debug(`Experiment not found: ${experimentName}`);
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
// Experiments not loaded yet - can't assign variant without knowing variants
|
|
540
|
+
logger.debug(`Experiments not loaded yet, cannot assign variant for: ${experimentName}`);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (!experiment.variants || experiment.variants.length === 0) {
|
|
545
|
+
logger.warn(`Experiment ${experimentName} has no variants`);
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Get the user ID for deterministic assignment
|
|
550
|
+
const userId = this.userId ?? this.anonymousIdValue;
|
|
551
|
+
|
|
552
|
+
// Compute deterministic variant assignment
|
|
553
|
+
const variant = this.computeVariant(userId, experimentName, experiment.variants);
|
|
554
|
+
|
|
555
|
+
// Store as super property so it's attached to all events
|
|
556
|
+
const propertyName = `experiment_${this.toSnakeCase(experimentName)}`;
|
|
557
|
+
this.setSuperProperty(propertyName, variant);
|
|
558
|
+
|
|
559
|
+
logger.debug(`Assigned variant '${variant}' for experiment '${experimentName}'`);
|
|
560
|
+
return variant;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Returns a promise that resolves when experiments have been loaded.
|
|
565
|
+
* Useful for waiting before calling getVariant() to ensure server-side experiments are available.
|
|
566
|
+
*/
|
|
567
|
+
ready(): Promise<void> {
|
|
568
|
+
return this.experimentsReadyPromise;
|
|
569
|
+
}
|
|
570
|
+
|
|
410
571
|
/**
|
|
411
572
|
* Clean up resources (stop timers, etc.).
|
|
412
573
|
*/
|
|
@@ -620,4 +781,87 @@ export class MostlyGoodMetrics {
|
|
|
620
781
|
// the regular flush mechanism for most events
|
|
621
782
|
logger.debug('Page unloading, attempting beacon flush');
|
|
622
783
|
}
|
|
784
|
+
|
|
785
|
+
// =====================================================
|
|
786
|
+
// A/B Testing methods
|
|
787
|
+
// =====================================================
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Fetch experiments from the server.
|
|
791
|
+
* Called automatically during initialization.
|
|
792
|
+
*/
|
|
793
|
+
private async fetchExperiments(): Promise<void> {
|
|
794
|
+
const url = `${this.config.baseURL}/v1/experiments`;
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
logger.debug('Fetching experiments...');
|
|
798
|
+
|
|
799
|
+
const response = await fetch(url, {
|
|
800
|
+
method: 'GET',
|
|
801
|
+
headers: {
|
|
802
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
803
|
+
'Content-Type': 'application/json',
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (!response.ok) {
|
|
808
|
+
logger.warn(`Failed to fetch experiments: ${response.status}`);
|
|
809
|
+
this.markExperimentsReady();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const data = (await response.json()) as ExperimentsResponse;
|
|
814
|
+
|
|
815
|
+
// Cache experiments by ID
|
|
816
|
+
this.experiments.clear();
|
|
817
|
+
for (const experiment of data.experiments || []) {
|
|
818
|
+
this.experiments.set(experiment.id, experiment);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
logger.debug(`Loaded ${this.experiments.size} experiments`);
|
|
822
|
+
} catch (e) {
|
|
823
|
+
logger.warn('Failed to fetch experiments', e);
|
|
824
|
+
} finally {
|
|
825
|
+
this.markExperimentsReady();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Mark experiments as loaded and resolve the ready promise.
|
|
831
|
+
*/
|
|
832
|
+
private markExperimentsReady(): void {
|
|
833
|
+
this.experimentsLoaded = true;
|
|
834
|
+
if (this.experimentsReadyResolve) {
|
|
835
|
+
this.experimentsReadyResolve();
|
|
836
|
+
this.experimentsReadyResolve = null;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Compute a deterministic variant assignment based on userId and experimentName.
|
|
842
|
+
* Same user + same experiment = same variant (consistent assignment).
|
|
843
|
+
*/
|
|
844
|
+
private computeVariant(userId: string, experimentName: string, variants: string[]): string {
|
|
845
|
+
// Create a deterministic hash from userId + experimentName
|
|
846
|
+
const hashInput = `${userId}:${experimentName}`;
|
|
847
|
+
const hash = this.simpleHash(hashInput);
|
|
848
|
+
|
|
849
|
+
// Convert hash to a positive number and mod by variant count
|
|
850
|
+
const hashNum = Math.abs(parseInt(hash, 16));
|
|
851
|
+
const variantIndex = hashNum % variants.length;
|
|
852
|
+
|
|
853
|
+
return variants[variantIndex];
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Convert a string to snake_case.
|
|
858
|
+
* Used for experiment property names (e.g., "button-color" -> "button_color").
|
|
859
|
+
*/
|
|
860
|
+
private toSnakeCase(str: string): string {
|
|
861
|
+
return str
|
|
862
|
+
.replace(/([A-Z])/g, '_$1')
|
|
863
|
+
.replace(/[-\s]+/g, '_')
|
|
864
|
+
.toLowerCase()
|
|
865
|
+
.replace(/^_/, '');
|
|
866
|
+
}
|
|
623
867
|
}
|
package/src/index.ts
CHANGED
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
* page: '/checkout',
|
|
20
20
|
* });
|
|
21
21
|
*
|
|
22
|
-
* // Identify users
|
|
23
|
-
* MostlyGoodMetrics.identify('user_123'
|
|
22
|
+
* // Identify users with profile data
|
|
23
|
+
* MostlyGoodMetrics.identify('user_123', {
|
|
24
|
+
* email: 'user@example.com',
|
|
25
|
+
* name: 'Jane Doe',
|
|
26
|
+
* });
|
|
24
27
|
* ```
|
|
25
28
|
*/
|
|
26
29
|
|
|
@@ -43,6 +46,9 @@ export type {
|
|
|
43
46
|
SendResult,
|
|
44
47
|
IEventStorage,
|
|
45
48
|
INetworkClient,
|
|
49
|
+
UserProfile,
|
|
50
|
+
Experiment,
|
|
51
|
+
ExperimentsResponse,
|
|
46
52
|
} from './types';
|
|
47
53
|
|
|
48
54
|
// Error class
|
package/src/storage.ts
CHANGED
|
@@ -6,6 +6,8 @@ const USER_ID_KEY = 'mostlygoodmetrics_user_id';
|
|
|
6
6
|
const ANONYMOUS_ID_KEY = 'mostlygoodmetrics_anonymous_id';
|
|
7
7
|
const APP_VERSION_KEY = 'mostlygoodmetrics_app_version';
|
|
8
8
|
const SUPER_PROPERTIES_KEY = 'mostlygoodmetrics_super_properties';
|
|
9
|
+
const IDENTIFY_HASH_KEY = 'mostlygoodmetrics_identify_hash';
|
|
10
|
+
const IDENTIFY_TIMESTAMP_KEY = 'mostlygoodmetrics_identify_timestamp';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Check if we're running in a browser environment with localStorage available.
|
|
@@ -461,6 +463,55 @@ class PersistenceManager {
|
|
|
461
463
|
}
|
|
462
464
|
}
|
|
463
465
|
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Get the stored identify hash (for debouncing).
|
|
469
|
+
*/
|
|
470
|
+
getIdentifyHash(): string | null {
|
|
471
|
+
if (isLocalStorageAvailable()) {
|
|
472
|
+
return localStorage.getItem(IDENTIFY_HASH_KEY);
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Set the identify hash.
|
|
479
|
+
*/
|
|
480
|
+
setIdentifyHash(hash: string): void {
|
|
481
|
+
if (isLocalStorageAvailable()) {
|
|
482
|
+
localStorage.setItem(IDENTIFY_HASH_KEY, hash);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Get the timestamp of the last identify event sent.
|
|
488
|
+
*/
|
|
489
|
+
getIdentifyLastSentAt(): number | null {
|
|
490
|
+
if (isLocalStorageAvailable()) {
|
|
491
|
+
const timestamp = localStorage.getItem(IDENTIFY_TIMESTAMP_KEY);
|
|
492
|
+
return timestamp ? parseInt(timestamp, 10) : null;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Set the timestamp of the last identify event sent.
|
|
499
|
+
*/
|
|
500
|
+
setIdentifyLastSentAt(timestamp: number): void {
|
|
501
|
+
if (isLocalStorageAvailable()) {
|
|
502
|
+
localStorage.setItem(IDENTIFY_TIMESTAMP_KEY, timestamp.toString());
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Clear identify debounce state (used when resetting identity).
|
|
508
|
+
*/
|
|
509
|
+
clearIdentifyState(): void {
|
|
510
|
+
if (isLocalStorageAvailable()) {
|
|
511
|
+
localStorage.removeItem(IDENTIFY_HASH_KEY);
|
|
512
|
+
localStorage.removeItem(IDENTIFY_TIMESTAMP_KEY);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
464
515
|
}
|
|
465
516
|
|
|
466
517
|
export const persistence = new PersistenceManager();
|
package/src/types.ts
CHANGED
|
@@ -389,8 +389,46 @@ export const SystemEvents = {
|
|
|
389
389
|
APP_UPDATED: '$app_updated',
|
|
390
390
|
APP_OPENED: '$app_opened',
|
|
391
391
|
APP_BACKGROUNDED: '$app_backgrounded',
|
|
392
|
+
IDENTIFY: '$identify',
|
|
392
393
|
} as const;
|
|
393
394
|
|
|
395
|
+
/**
|
|
396
|
+
* User profile data for the identify() call.
|
|
397
|
+
*/
|
|
398
|
+
export interface UserProfile {
|
|
399
|
+
/**
|
|
400
|
+
* The user's email address.
|
|
401
|
+
*/
|
|
402
|
+
email?: string;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The user's display name.
|
|
406
|
+
*/
|
|
407
|
+
name?: string;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* An experiment configuration from the server.
|
|
412
|
+
*/
|
|
413
|
+
export interface Experiment {
|
|
414
|
+
/**
|
|
415
|
+
* The experiment identifier (e.g., "button-color").
|
|
416
|
+
*/
|
|
417
|
+
id: string;
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The variants for this experiment (e.g., ["a", "b"]).
|
|
421
|
+
*/
|
|
422
|
+
variants: string[];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Response from the experiments API endpoint.
|
|
427
|
+
*/
|
|
428
|
+
export interface ExperimentsResponse {
|
|
429
|
+
experiments: Experiment[];
|
|
430
|
+
}
|
|
431
|
+
|
|
394
432
|
/**
|
|
395
433
|
* System property keys (prefixed with $).
|
|
396
434
|
*/
|