import type { Context , Experiment , FeatureResult , Result , SubscriptionFunction , FeatureDefinition , FeatureResultSource , Attributes , } from "./types/" ; import type { ConditionInterface } from "./types/mongrule" ; import { getUrlRegExp , isIncluded , getBucketRanges , hash , chooseVariation , getQueryStringOverride , inNamespace , } from "./util" ; import { evalCondition } from "./mongrule" ; export type { Context , Experiment , Result , FeatureDefinition , FeatureRule , Attributes , FeatureResult , ExperimentOverride , } from "./types/" ; export type { ConditionInterface } from "./types/mongrule" ; const isBrowser = typeof window !== "undefined" ; class GrowthBook { private context : Context ; private _renderer : null | ( ( ) => void ) = null ; private _trackedExperiments = new Set ( ) ; public debug = false ; private subscriptions = new Set < SubscriptionFunction > ( ) ; private assigned = new Map < string , { experiment : Experiment < any > ; result : Result < any > ; } > ( ) ; private _forcedFeatureValues = new Map < string , any > ( ) ; private _attributeOverrides : Attributes = { } ; constructor ( context = { } ) { this . context = context ; if ( isBrowser && ! context . disableDevTools ) { window . _growthbook = this ; document . dispatchEvent ( new Event ( "gbloaded" ) ) ; } } private render ( ) { if ( this . _renderer ) { this . _renderer ( ) ; } } public setFeatures ( features ) { this . context . features = features ; this . render ( ) ; } public setAttributes ( attributes ) { this . context . attributes = attributes ; this . render ( ) ; } public setAttributeOverrides ( overrides ) { this . _attributeOverrides = overrides ; this . render ( ) ; } public setForcedVariations ( vars ) { this . context . forcedVariations = vars || { } ; this . render ( ) ; } public setForcedFeatures ( map ) { this . _forcedFeatureValues = map ; this . render ( ) ; } public getAttributes ( ) { return { ... this . context . attributes , ... this . _attributeOverrides } ; } public getFeatures ( ) { return this . context . features || { } ; } public subscribe ( cb ) { this . subscriptions . add ( cb ) ; return ( ) => { this . subscriptions . delete ( cb ) ; } ; } public getAllResults ( ) { return new Map ( this . assigned ) ; } public destroy ( ) { this . subscriptions . clear ( ) ; this . assigned . clear ( ) ; this . _trackedExperiments . clear ( ) ; if ( isBrowser && window . _growthbook === this ) { delete window . _growthbook ; } } public setRenderer ( renderer ) { this . _renderer = renderer ; } public forceVariation ( key , variation ) { this . context . forcedVariations = this . context . forcedVariations || { } ; this . context . forcedVariations [ key ] = variation ; this . render ( ) ; } public run < T > ( experiment ) { const result = this . _run ( experiment ) ; const key = experiment . key ; const prev = this . assigned . get ( key ) ; if ( ! prev || prev . result . inExperiment !== result . inExperiment || prev . result . variationId !== result . variationId ) { this . assigned . set ( key , { experiment , result } ) ; this . subscriptions . forEach ( ( cb ) => { try { cb ( experiment , result ) ; } catch ( e ) { console . error ( e ) ; } } ) ; } return result ; } private getFeatureResult < T > ( value , source , experiment = null , result = null ) { const ret = { value , on : ! ! value , off : ! value , source } ; if ( experiment ) ret . experiment = experiment ; if ( result ) ret . experimentResult = result ; return ret ; } public feature < T = any > ( id ) { if ( this . _forcedFeatureValues . has ( id ) ) { process . env . NODE_ENV !== "production" && this . log ( "Global override" , { id , value : this . _forcedFeatureValues . get ( id ) } ) ; return this . getFeatureResult ( this . _forcedFeatureValues . get ( id ) , "override" ) ; } if ( ! this . context . features || ! this . context . features [ id ] ) { process . env . NODE_ENV !== "production" && this . log ( "Unknown feature" , { id } ) ; return this . getFeatureResult ( null , "unknownFeature" ) ; } const feature = this . context . features [ id ] ; if ( feature . rules ) { for ( const rule of feature . rules ) { if ( rule . condition && ! this . conditionPasses ( rule . condition ) ) { process . env . NODE_ENV !== "production" && this . log ( "Skip rule because of condition" , { id , rule } ) ; continue ; } if ( rule . force ) { if ( "coverage" in rule ) { const { hashValue } = this . getHashAttribute ( rule . hashAttribute ) ; if ( ! hashValue ) { process . env . NODE_ENV !== "production" && this . log ( "Skip rule because of missing hashAttribute" , { id , rule } ) ; continue ; } const n = hash ( hashValue + id ) ; if ( n > ( rule . coverage as number ) ) { process . env . NODE_ENV !== "production" && this . log ( "Skip rule because of coverage" , { id , rule } ) ; continue ; } } process . env . NODE_ENV !== "production" && this . log ( "Force value from rule" , { id , rule } ) ; return this . getFeatureResult ( rule . force , "force" ) ; } if ( ! rule . variations ) { process . env . NODE_ENV !== "production" && this . log ( "Skip invalid rule" , { id , rule } ) ; continue ; } const exp = { variations : rule . variations as [ T , T , ... T [ ] ] , key : rule . key || id } ; if ( "coverage" in rule ) exp . coverage = rule . coverage ; if ( rule . weights ) exp . weights = rule . weights ; if ( rule . hashAttribute ) exp . hashAttribute = rule . hashAttribute ; if ( rule . namespace ) exp . namespace = rule . namespace ; const res = this . run ( exp ) ; if ( res . inExperiment ) { return this . getFeatureResult ( res . value , "experiment" , exp , res ) ; } } } process . env . NODE_ENV !== "production" && this . log ( "Use default value" , { id , value : feature . defaultValue ?? null } ) ; return this . getFeatureResult ( feature . defaultValue ?? null , "defaultValue" ) ; } private conditionPasses ( condition ) { return evalCondition ( this . getAttributes ( ) , condition ) ; } private _run < T > ( experiment ) { const key = experiment . key ; const numVariations = experiment . variations . length ; if ( numVariations < 2 ) { process . env . NODE_ENV !== "production" && this . log ( "Invalid experiment" , { id : key } ) ; return this . getResult ( experiment ) ; } if ( this . context . enabled === false ) { process . env . NODE_ENV !== "production" && this . log ( "Context disabled" , { id : key } ) ; return this . getResult ( experiment ) ; } experiment = this . mergeOverrides ( experiment ) ; const qsOverride = getQueryStringOverride ( key , this . getContextUrl ( ) , numVariations ) ; if ( qsOverride !== null ) { process . env . NODE_ENV !== "production" && this . log ( "Force via querystring" , { id : key , variation : qsOverride } ) ; return this . getResult ( experiment , qsOverride ) ; } if ( this . context . forcedVariations && key in this . context . forcedVariations ) { const variation = this . context . forcedVariations [ key ] ; process . env . NODE_ENV !== "production" && this . log ( "Force via dev tools" , { id : key , variation } ) ; return this . getResult ( experiment , variation ) ; } if ( experiment . status === "draft" || experiment . active === false ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because inactive" , { id : key } ) ; return this . getResult ( experiment ) ; } const { hashValue } = this . getHashAttribute ( experiment . hashAttribute ) ; if ( ! hashValue ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because missing hashAttribute" , { id : key } ) ; return this . getResult ( experiment ) ; } if ( experiment . namespace && ! inNamespace ( hashValue , experiment . namespace ) ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because of namespace" , { id : key } ) ; return this . getResult ( experiment ) ; } if ( experiment . include && ! isIncluded ( experiment . include ) ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because of include function" , { id : key } ) ; return this . getResult ( experiment ) ; } if ( experiment . condition && ! this . conditionPasses ( experiment . condition ) ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because of condition" , { id : key } ) ; return this . getResult ( experiment ) ; } if ( experiment . groups && ! this . hasGroupOverlap ( experiment . groups as string [ ] ) ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because of groups" , { id : key } ) ; return this . getResult ( experiment ) ; } if ( experiment . url && ! this . urlIsValid ( experiment . url as RegExp ) ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because of url" , { id : key } ) ; return this . getResult ( experiment ) ; } const ranges = getBucketRanges ( experiment . variations . length , experiment . coverage || 1 , experiment . weights ) ; const n = hash ( hashValue + key ) ; const assigned = chooseVariation ( n , ranges ) ; if ( assigned < 0 ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because of coverage" , { id : key } ) ; return this . getResult ( experiment ) ; } if ( "force" in experiment ) { process . env . NODE_ENV !== "production" && this . log ( "Force variation" , { id : key , variation : experiment . force } ) ; return this . getResult ( experiment , experiment . force ) ; } if ( this . context . qaMode ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because QA mode" , { id : key } ) ; return this . getResult ( experiment ) ; } if ( experiment . status === "stopped" ) { process . env . NODE_ENV !== "production" && this . log ( "Skip because stopped" , { id : key } ) ; return this . getResult ( experiment ) ; } const result = this . getResult ( experiment , assigned , true ) ; this . track ( experiment , result ) ; process . env . NODE_ENV !== "production" && this . log ( "In experiment" , { id : key , variation : result . variationId } ) ; return result ; } private log ( msg , ctx ) { if ( ! this . debug ) return ; if ( this . context . log ) this . context . log ( msg , ctx ) ; else console . log ( msg , ctx ) ; } private track < T > ( experiment , result ) { if ( ! this . context . trackingCallback ) return ; const key = experiment . key ; const k = result . hashAttribute + result . hashValue + key + result . variationId ; if ( this . _trackedExperiments . has ( k ) ) return ; this . _trackedExperiments . add ( k ) ; try { this . context . trackingCallback ( experiment , result ) ; } catch ( e ) { console . error ( e ) ; } } private mergeOverrides < T > ( experiment ) { const key = experiment . key ; const o = this . context . overrides ; if ( o && o [ key ] ) { experiment = Object . assign ( { } , experiment , o [ key ] ) ; if ( typeof experiment . url === "string" ) { experiment . url = getUrlRegExp ( experiment . url as any ) ; } } return experiment ; } private getHashAttribute ( attr ? ) { const hashAttribute = attr || "id" ; let hashValue = "" ; if ( this . _attributeOverrides [ hashAttribute ] ) { hashValue = this . _attributeOverrides [ hashAttribute ] ; } else if ( this . context . attributes ) { hashValue = this . context . attributes [ hashAttribute ] || "" ; } else if ( this . context . user ) { hashValue = this . context . user [ hashAttribute ] || "" ; } return { hashAttribute , hashValue } ; } private getResult < T > ( experiment , variationIndex = 0 , inExperiment = false ) { if ( variationIndex < 0 || variationIndex >= experiment . variations . length ) { variationIndex = 0 ; } const { hashAttribute , hashValue } = this . getHashAttribute ( experiment . hashAttribute ) ; return { inExperiment , variationId : variationIndex , value : experiment . variations [ variationIndex ] , hashAttribute , hashValue } ; } private getContextUrl ( ) { return this . context . url || ( isBrowser ? window . location . href : "" ) ; } private urlIsValid ( urlRegex ) { const url = this . getContextUrl ( ) ; if ( ! url ) return false ; const pathOnly = url . replace ( /^https?:\/\// , "" ) . replace ( /^[^/]*\// , "/" ) ; if ( urlRegex . test ( url ) ) return true ; if ( urlRegex . test ( pathOnly ) ) return true ; return false ; } private hasGroupOverlap ( expGroups ) { const groups = this . context . groups || { } ; for ( let i = 0 ; i < expGroups . length ; i ++ ) { if ( groups [ expGroups [ i ] ] ) return true ; } return false ; } } export { GrowthBook } ;