@@ -120,11 +120,26 @@ export interface FeatureFlagsOptions {
120120}
121121
122122/**
123- * Cached feature flags with timestamp .
123+ * Options for evaluating experiments .
124124 */
125- interface CachedFlags {
125+ export interface ExperimentOptions {
126+ /**
127+ * Optional profile ID for long-term user tracking.
128+ * If not provided, an anonymous profile ID will be generated server-side based on IP and user agent.
129+ * Overrides the global profileId if set.
130+ */
131+ profileId ?: string
132+ }
133+
134+ /**
135+ * Cached feature flags and experiments with timestamp.
136+ */
137+ interface CachedData {
126138 flags : Record < string , boolean >
139+ experiments : Record < string , string >
127140 timestamp : number
141+ /** The profileId used when fetching this cached data */
142+ profileId ?: string
128143}
129144
130145/**
@@ -218,7 +233,7 @@ export class Lib {
218233 private perfStatsCollected : boolean = false
219234 private activePage : string | null = null
220235 private errorListenerExists = false
221- private cachedFlags : CachedFlags | null = null
236+ private cachedData : CachedData | null = null
222237
223238 constructor ( private projectID : string , private options ?: LibOptions ) {
224239 this . trackPathChange = this . trackPathChange . bind ( this )
@@ -407,63 +422,80 @@ export class Lib {
407422 }
408423
409424 /**
410- * Fetches all feature flags for the project.
425+ * Fetches all feature flags and experiments for the project.
411426 * Results are cached for 5 minutes by default.
412427 *
413428 * @param options - Options for evaluating feature flags.
414- * @param forceRefresh - If true, bypasses the cache and fetches fresh flags .
429+ * @param forceRefresh - If true, bypasses the cache and fetches fresh data .
415430 * @returns A promise that resolves to a record of flag keys to boolean values.
416431 */
417432 async getFeatureFlags ( options ?: FeatureFlagsOptions , forceRefresh ?: boolean ) : Promise < Record < string , boolean > > {
418433 if ( ! isInBrowser ( ) ) {
419434 return { }
420435 }
421436
422- // Check cache first
423- if ( ! forceRefresh && this . cachedFlags ) {
437+ const requestedProfileId = options ?. profileId ?? this . options ?. profileId
438+
439+ // Check cache first - must match profileId and not be expired
440+ if ( ! forceRefresh && this . cachedData ) {
424441 const now = Date . now ( )
425- if ( now - this . cachedFlags . timestamp < DEFAULT_CACHE_DURATION ) {
426- return this . cachedFlags . flags
442+ const isSameProfile = this . cachedData . profileId === requestedProfileId
443+ if ( isSameProfile && now - this . cachedData . timestamp < DEFAULT_CACHE_DURATION ) {
444+ return this . cachedData . flags
427445 }
428446 }
429447
430448 try {
431- const apiBase = this . getApiBase ( )
432- const body : { pid : string ; profileId ?: string } = {
433- pid : this . projectID ,
434- }
449+ await this . fetchFlagsAndExperiments ( options )
450+ return this . cachedData ?. flags || { }
451+ } catch ( error ) {
452+ console . warn ( '[Swetrix] Error fetching feature flags:' , error )
453+ return this . cachedData ?. flags || { }
454+ }
455+ }
435456
436- // Use profileId from options, or fall back to global profileId
437- const profileId = options ?. profileId ?? this . options ?. profileId
438- if ( profileId ) {
439- body . profileId = profileId
440- }
457+ /**
458+ * Internal method to fetch both feature flags and experiments from the API.
459+ */
460+ private async fetchFlagsAndExperiments ( options ?: FeatureFlagsOptions | ExperimentOptions ) : Promise < void > {
461+ const apiBase = this . getApiBase ( )
462+ const body : { pid : string ; profileId ?: string } = {
463+ pid : this . projectID ,
464+ }
441465
442- const response = await fetch ( `${ apiBase } /feature-flag/evaluate` , {
443- method : 'POST' ,
444- headers : {
445- 'Content-Type' : 'application/json' ,
446- } ,
447- body : JSON . stringify ( body ) ,
448- } )
466+ // Use profileId from options, or fall back to global profileId
467+ const profileId = options ?. profileId ?? this . options ?. profileId
468+ if ( profileId ) {
469+ body . profileId = profileId
470+ }
449471
450- if ( ! response . ok ) {
451- console . warn ( '[Swetrix] Failed to fetch feature flags:' , response . status )
452- return this . cachedFlags ?. flags || { }
453- }
472+ const response = await fetch ( `${ apiBase } /feature-flag/evaluate` , {
473+ method : 'POST' ,
474+ headers : {
475+ 'Content-Type' : 'application/json' ,
476+ } ,
477+ body : JSON . stringify ( body ) ,
478+ } )
454479
455- const data = ( await response . json ( ) ) as { flags : Record < string , boolean > }
480+ if ( ! response . ok ) {
481+ console . warn ( '[Swetrix] Failed to fetch feature flags and experiments:' , response . status )
482+ return
483+ }
456484
457- // Update cache
458- this . cachedFlags = {
459- flags : data . flags ,
460- timestamp : Date . now ( ) ,
461- }
485+ const data = ( await response . json ( ) ) as {
486+ flags : Record < string , boolean >
487+ experiments ?: Record < string , string >
488+ }
462489
463- return data . flags
464- } catch ( error ) {
465- console . warn ( '[Swetrix] Error fetching feature flags:' , error )
466- return this . cachedFlags ?. flags || { }
490+ // Use profileId from options, or fall back to global profileId
491+ const cachedProfileId = options ?. profileId ?? this . options ?. profileId
492+
493+ // Update cache with both flags and experiments
494+ this . cachedData = {
495+ flags : data . flags || { } ,
496+ experiments : data . experiments || { } ,
497+ timestamp : Date . now ( ) ,
498+ profileId : cachedProfileId ,
467499 }
468500 }
469501
@@ -481,10 +513,84 @@ export class Lib {
481513 }
482514
483515 /**
484- * Clears the cached feature flags, forcing a fresh fetch on the next call.
516+ * Clears the cached feature flags and experiments , forcing a fresh fetch on the next call.
485517 */
486518 clearFeatureFlagsCache ( ) : void {
487- this . cachedFlags = null
519+ this . cachedData = null
520+ }
521+
522+ /**
523+ * Fetches all A/B test experiments for the project.
524+ * Results are cached for 5 minutes by default (shared cache with feature flags).
525+ *
526+ * @param options - Options for evaluating experiments.
527+ * @param forceRefresh - If true, bypasses the cache and fetches fresh data.
528+ * @returns A promise that resolves to a record of experiment IDs to variant keys.
529+ *
530+ * @example
531+ * ```typescript
532+ * const experiments = await getExperiments()
533+ * // experiments = { 'exp-123': 'variant-a', 'exp-456': 'control' }
534+ * ```
535+ */
536+ async getExperiments ( options ?: ExperimentOptions , forceRefresh ?: boolean ) : Promise < Record < string , string > > {
537+ if ( ! isInBrowser ( ) ) {
538+ return { }
539+ }
540+
541+ const requestedProfileId = options ?. profileId ?? this . options ?. profileId
542+
543+ // Check cache first - must match profileId and not be expired
544+ if ( ! forceRefresh && this . cachedData ) {
545+ const now = Date . now ( )
546+ const isSameProfile = this . cachedData . profileId === requestedProfileId
547+ if ( isSameProfile && now - this . cachedData . timestamp < DEFAULT_CACHE_DURATION ) {
548+ return this . cachedData . experiments
549+ }
550+ }
551+
552+ try {
553+ await this . fetchFlagsAndExperiments ( options )
554+ return this . cachedData ?. experiments || { }
555+ } catch ( error ) {
556+ console . warn ( '[Swetrix] Error fetching experiments:' , error )
557+ return this . cachedData ?. experiments || { }
558+ }
559+ }
560+
561+ /**
562+ * Gets the variant key for a specific A/B test experiment.
563+ *
564+ * @param experimentId - The experiment ID.
565+ * @param options - Options for evaluating the experiment.
566+ * @param defaultVariant - Default variant key to return if the experiment is not found. Defaults to null.
567+ * @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
568+ *
569+ * @example
570+ * ```typescript
571+ * const variant = await getExperiment('checkout-redesign')
572+ *
573+ * if (variant === 'new-checkout') {
574+ * // Show new checkout flow
575+ * } else {
576+ * // Show control (original) checkout
577+ * }
578+ * ```
579+ */
580+ async getExperiment (
581+ experimentId : string ,
582+ options ?: ExperimentOptions ,
583+ defaultVariant : string | null = null ,
584+ ) : Promise < string | null > {
585+ const experiments = await this . getExperiments ( options )
586+ return experiments [ experimentId ] ?? defaultVariant
587+ }
588+
589+ /**
590+ * Clears the cached experiments (alias for clearFeatureFlagsCache since they share the same cache).
591+ */
592+ clearExperimentsCache ( ) : void {
593+ this . cachedData = null
488594 }
489595
490596 /**
0 commit comments