import { alwaysPromise } from './alwaysPromise' ; import { BehavioralSubject } from './behavioralSubject' ; import { Condition } from './condition' ; import config from './config' ; import { setAbTestParameter } from './query' ; import { TrackEventActionType , TrackEventType , TrackingData , TrackingDataExtender , trackingDataExtenderFactory , } from './tracking' ; import { UserAgentInfo } from './userAgentInfo' ; import userSession from './userSession' ; export interface Variation { /** A descriptive unique name of this variation */ name : string ; /** A relative weight defining how many users should see this variation. Default value is 1 */ weight ? : number ; /**
     * Function to be called when this variation has been chosen and should be setup.
     * It's always called after DOMContentLoaded
     */ setup ? : ( this : SplitTest , userAgentInfo : UserAgentInfo ) => void ; /** Whether a track event should automatically be published once this variation has been setup. Default is true. */ trackEventAutoPublish ? : boolean ; } export interface InternalVariation extends Variation { normalizedWeight : number ; weight : number ; } export type State = 'uninitialized' | 'initializing' | 'initialized' | 'canceled' ; export class SplitTest { public state : State = 'uninitialized' ; public changes = new BehavioralSubject ( this ) ; private finalStateListeners : Array < ( ) => void > = [ ] ; private readonly _variations : InternalVariation [ ] = [ ] ; get variations ( ) : InternalVariation [ ] { return this . _variations ; } constructor ( public name , private userAgentInfo , private trackingDataExtender ) { this . extendTrackingData ( trackingDataExtenderFactory ( { experimentName : name , } ) ) ; } /**
     * Determines whether this test is able to run or not.
     */ public async shouldRun ( userAgentInfo ) { const conditionPromises = [ config . globalCondition ( userAgentInfo ) , this . condition ( userAgentInfo ) , ] . map ( alwaysPromise ) ; return ( await Promise . all ( conditionPromises ) ) . every ( ( a ) => a ) ; } public setCondition ( condition ) { this . condition = condition ; return this ; } public addVariation ( variation ) { if ( typeof variation . name !== 'string' || variation . name === '' || this . getVariation ( variation . name ) ) { throw new Error ( `Split test "${ this . name }": Variation must have a unique name. Was "${ variation . name }"` ) ; } this . _variations . push ( { ... variation , normalizedWeight : 0 , weight : typeof variation . weight === 'number' ? variation . weight : 1 , } ) ; this . normalizeVariationWeights ( ) ; this . changes . next ( this ) ; return this ; } public async setup ( ) { if ( this . _variations . length === 0 ) { throw new Error ( "Skift: can't setup a test without variations" ) ; } if ( this . state === 'initialized' ) { return true ; } this . transitionState ( 'initializing' ) ; const passesConditions = await this . shouldRun ( this . userAgentInfo ) ; if ( ! passesConditions ) { this . transitionState ( 'canceled' ) ; return false ; } let variation = this . getVariation ( userSession . getTestVariation ( this . name ) ) ; if ( ! variation ) { variation = this . selectRandomVariation ( ) ; userSession . setTestVariation ( this . name , variation . name ) ; } this . extendTrackingData ( trackingDataExtenderFactory ( { variationName : variation . name , } ) ) ; if ( typeof variation . setup === 'function' ) { variation . setup . call ( this , this . userAgentInfo ) ; } if ( variation . trackEventAutoPublish !== false ) { this . trackViewed ( ) ; } this . transitionState ( 'initialized' ) ; this . changes . next ( this ) ; return true ; } public async isInitialized ( ) { const { state } = this ; if ( state === 'initializing' ) { return await new Promise < boolean > ( ( resolve , reject ) => { this . subscribeStateListener ( ( ) => { resolve ( this . state === 'initialized' ) ; } ) ; } ) ; } return this . state === 'initialized' ; } public getVariation ( name ) { return this . _variations . filter ( ( v ) => v . name === name ) [ 0 ] ; } public getVariationUrl ( variationName ) { const param = `${ this . name }=${ variationName }` ; try { return ( location . protocol + '//' + location . host + location . pathname + setAbTestParameter ( location . search , btoa ( param ) ) + location . hash ) ; } catch ( e ) { return location . href ; } } /**
     * The tracking data extenders are called just before any event is published to the event handler.
     */ public extendTrackingData ( trackingDataExtender ) { const currentExtender = this . trackingDataExtender ; this . trackingDataExtender = ( trackingData , eventName ) => { return trackingDataExtender ( currentExtender ( trackingData , eventName ) , eventName ) ; } ; return this ; } /**
     * Emits an "Experiment Viewed" tracking event
     */ public trackViewed ( ) { this . trackEvent ( 'ExperimentViewed' ) ; } param action param target public trackActionPerformed ( action , target ? ) { this . trackEvent ( 'ExperimentActionPerformed' , { action , actionTarget : target || '' , } ) ; } param element param name public trackLink ( element , name ? ) { const event = 'ExperimentActionPerformed' ; const trackingData = this . trackingDataExtender ( { action : 'Click' , actionTarget : name || element . textContent , } , event ) ; config . tracking . trackLink ( element , event , trackingData ) ; } private condition : Condition = ( ) => true ; private normalizeVariationWeights ( ) { const weightsSum = this . _variations . reduce ( ( sum , variation ) => sum + variation . weight , 0 ) ; this . _variations . forEach ( ( variation ) => { variation . normalizedWeight = variation . weight / weightsSum ; } ) ; } private transitionState ( state ) { this . state = state ; if ( state !== 'initializing' ) { this . finalStateListeners . forEach ( ( l ) => l ( ) ) ; this . finalStateListeners = [ ] ; } } private subscribeStateListener ( listener ) { this . finalStateListeners . push ( listener ) ; } private selectRandomVariation ( ) { let i = 0 ; for ( let runningTotal = 0 , testSegment = Math . random ( ) ; i < this . _variations . length && ( runningTotal += this . _variations [ i ] . normalizedWeight ) < testSegment ; i ++ ) { } return this . _variations [ i ] ; } private trackEvent ( event , trackingData ? ) { const allTrackingData = this . trackingDataExtender ( trackingData || { } , event ) ; config . tracking . track ( event , allTrackingData ) ; } }