11/**
2- * Copyright 2016, 2019 Optimizely
2+ * Copyright 2016, 2019-2020 Optimizely
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1717/**
1818 * Bucketer API for determining the variation id from the specified parameters
1919 */
20- var enums = require ( '../../utils/enums' ) ;
21- var murmurhash = require ( 'murmurhash' ) ;
22- var sprintf = require ( '@optimizely/js-sdk-utils' ) . sprintf ;
20+ import enums from '../../utils/enums' ;
21+ import murmurhash from 'murmurhash' ;
22+ import { sprintf } from '@optimizely/js-sdk-utils' ;
2323
2424var ERROR_MESSAGES = enums . ERROR_MESSAGES ;
2525var HASH_SEED = 1 ;
@@ -30,166 +30,171 @@ var MAX_TRAFFIC_VALUE = 10000;
3030var MODULE_NAME = 'BUCKETER' ;
3131var RANDOM_POLICY = 'random' ;
3232
33- module . exports = {
34- /**
35- * Determines ID of variation to be shown for the given input params
36- * @param {Object } bucketerParams
37- * @param {string } bucketerParams.experimentId
38- * @param {string } bucketerParams.experimentKey
39- * @param {string } bucketerParams.userId
40- * @param {Object[] } bucketerParams.trafficAllocationConfig
41- * @param {Array } bucketerParams.experimentKeyMap
42- * @param {Object } bucketerParams.groupIdMap
43- * @param {Object } bucketerParams.variationIdMap
44- * @param {string } bucketerParams.varationIdMap[].key
45- * @param {Object } bucketerParams.logger
46- * @param {string } bucketerParams.bucketingId
47- * @return Variation ID that user has been bucketed into, null if user is not bucketed into any experiment
48- */
49- bucket : function ( bucketerParams ) {
50- // Check if user is in a random group; if so, check if user is bucketed into a specific experiment
51- var experiment = bucketerParams . experimentKeyMap [ bucketerParams . experimentKey ] ;
52- var groupId = experiment [ 'groupId' ] ;
53- if ( groupId ) {
54- var group = bucketerParams . groupIdMap [ groupId ] ;
55- if ( ! group ) {
56- throw new Error ( sprintf ( ERROR_MESSAGES . INVALID_GROUP_ID , MODULE_NAME , groupId ) ) ;
57- }
58- if ( group . policy === RANDOM_POLICY ) {
59- var bucketedExperimentId = module . exports . bucketUserIntoExperiment (
60- group ,
61- bucketerParams . bucketingId ,
33+ /**
34+ * Determines ID of variation to be shown for the given input params
35+ * @param {Object } bucketerParams
36+ * @param {string } bucketerParams.experimentId
37+ * @param {string } bucketerParams.experimentKey
38+ * @param {string } bucketerParams.userId
39+ * @param {Object[] } bucketerParams.trafficAllocationConfig
40+ * @param {Array } bucketerParams.experimentKeyMap
41+ * @param {Object } bucketerParams.groupIdMap
42+ * @param {Object } bucketerParams.variationIdMap
43+ * @param {string } bucketerParams.varationIdMap[].key
44+ * @param {Object } bucketerParams.logger
45+ * @param {string } bucketerParams.bucketingId
46+ * @return Variation ID that user has been bucketed into, null if user is not bucketed into any experiment
47+ */
48+ export var bucket = function ( bucketerParams ) {
49+ // Check if user is in a random group; if so, check if user is bucketed into a specific experiment
50+ var experiment = bucketerParams . experimentKeyMap [ bucketerParams . experimentKey ] ;
51+ var groupId = experiment [ 'groupId' ] ;
52+ if ( groupId ) {
53+ var group = bucketerParams . groupIdMap [ groupId ] ;
54+ if ( ! group ) {
55+ throw new Error ( sprintf ( ERROR_MESSAGES . INVALID_GROUP_ID , MODULE_NAME , groupId ) ) ;
56+ }
57+ if ( group . policy === RANDOM_POLICY ) {
58+ var bucketedExperimentId = this . bucketUserIntoExperiment (
59+ group ,
60+ bucketerParams . bucketingId ,
61+ bucketerParams . userId ,
62+ bucketerParams . logger
63+ ) ;
64+
65+ // Return if user is not bucketed into any experiment
66+ if ( bucketedExperimentId === null ) {
67+ var notbucketedInAnyExperimentLogMessage = sprintf (
68+ LOG_MESSAGES . USER_NOT_IN_ANY_EXPERIMENT ,
69+ MODULE_NAME ,
6270 bucketerParams . userId ,
63- bucketerParams . logger
71+ groupId
6472 ) ;
73+ bucketerParams . logger . log ( LOG_LEVEL . INFO , notbucketedInAnyExperimentLogMessage ) ;
74+ return null ;
75+ }
6576
66- // Return if user is not bucketed into any experiment
67- if ( bucketedExperimentId === null ) {
68- var notbucketedInAnyExperimentLogMessage = sprintf (
69- LOG_MESSAGES . USER_NOT_IN_ANY_EXPERIMENT ,
70- MODULE_NAME ,
71- bucketerParams . userId ,
72- groupId
73- ) ;
74- bucketerParams . logger . log ( LOG_LEVEL . INFO , notbucketedInAnyExperimentLogMessage ) ;
75- return null ;
76- }
77-
78- // Return if user is bucketed into a different experiment than the one specified
79- if ( bucketedExperimentId !== bucketerParams . experimentId ) {
80- var notBucketedIntoExperimentOfGroupLogMessage = sprintf (
81- LOG_MESSAGES . USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP ,
82- MODULE_NAME ,
83- bucketerParams . userId ,
84- bucketerParams . experimentKey ,
85- groupId
86- ) ;
87- bucketerParams . logger . log ( LOG_LEVEL . INFO , notBucketedIntoExperimentOfGroupLogMessage ) ;
88- return null ;
89- }
90-
91- // Continue bucketing if user is bucketed into specified experiment
92- var bucketedIntoExperimentOfGroupLogMessage = sprintf (
93- LOG_MESSAGES . USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP ,
77+ // Return if user is bucketed into a different experiment than the one specified
78+ if ( bucketedExperimentId !== bucketerParams . experimentId ) {
79+ var notBucketedIntoExperimentOfGroupLogMessage = sprintf (
80+ LOG_MESSAGES . USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP ,
9481 MODULE_NAME ,
9582 bucketerParams . userId ,
9683 bucketerParams . experimentKey ,
9784 groupId
9885 ) ;
99- bucketerParams . logger . log ( LOG_LEVEL . INFO , bucketedIntoExperimentOfGroupLogMessage ) ;
86+ bucketerParams . logger . log ( LOG_LEVEL . INFO , notBucketedIntoExperimentOfGroupLogMessage ) ;
87+ return null ;
10088 }
101- }
102- var bucketingId = sprintf ( '%s%s' , bucketerParams . bucketingId , bucketerParams . experimentId ) ;
103- var bucketValue = module . exports . _generateBucketValue ( bucketingId ) ;
10489
105- var bucketedUserLogMessage = sprintf (
106- LOG_MESSAGES . USER_ASSIGNED_TO_VARIATION_BUCKET ,
107- MODULE_NAME ,
108- bucketValue ,
109- bucketerParams . userId
110- ) ;
111- bucketerParams . logger . log ( LOG_LEVEL . DEBUG , bucketedUserLogMessage ) ;
112-
113- var entityId = module . exports . _findBucket ( bucketValue , bucketerParams . trafficAllocationConfig ) ;
114- if ( ! entityId ) {
115- var userHasNoVariationLogMessage = sprintf (
116- LOG_MESSAGES . USER_HAS_NO_VARIATION ,
90+ // Continue bucketing if user is bucketed into specified experiment
91+ var bucketedIntoExperimentOfGroupLogMessage = sprintf (
92+ LOG_MESSAGES . USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP ,
11793 MODULE_NAME ,
11894 bucketerParams . userId ,
119- bucketerParams . experimentKey
95+ bucketerParams . experimentKey ,
96+ groupId
12097 ) ;
121- bucketerParams . logger . log ( LOG_LEVEL . DEBUG , userHasNoVariationLogMessage ) ;
122- } else if ( ! bucketerParams . variationIdMap . hasOwnProperty ( entityId ) ) {
123- var invalidVariationIdLogMessage = sprintf ( LOG_MESSAGES . INVALID_VARIATION_ID , MODULE_NAME ) ;
124- bucketerParams . logger . log ( LOG_LEVEL . WARNING , invalidVariationIdLogMessage ) ;
125- return null ;
126- } else {
127- var variationKey = bucketerParams . variationIdMap [ entityId ] . key ;
128- var userInVariationLogMessage = sprintf (
129- LOG_MESSAGES . USER_HAS_VARIATION ,
130- MODULE_NAME ,
131- bucketerParams . userId ,
132- variationKey ,
133- bucketerParams . experimentKey
134- ) ;
135- bucketerParams . logger . log ( LOG_LEVEL . INFO , userInVariationLogMessage ) ;
98+ bucketerParams . logger . log ( LOG_LEVEL . INFO , bucketedIntoExperimentOfGroupLogMessage ) ;
13699 }
100+ }
101+ var bucketingId = sprintf ( '%s%s' , bucketerParams . bucketingId , bucketerParams . experimentId ) ;
102+ var bucketValue = this . _generateBucketValue ( bucketingId ) ;
137103
138- return entityId ;
139- } ,
104+ var bucketedUserLogMessage = sprintf (
105+ LOG_MESSAGES . USER_ASSIGNED_TO_VARIATION_BUCKET ,
106+ MODULE_NAME ,
107+ bucketValue ,
108+ bucketerParams . userId
109+ ) ;
110+ bucketerParams . logger . log ( LOG_LEVEL . DEBUG , bucketedUserLogMessage ) ;
140111
141- /**
142- * Returns bucketed experiment ID to compare against experiment user is being called into
143- * @param {Object } group Group that experiment is in
144- * @param {string } bucketingId Bucketing ID
145- * @param {string } userId ID of user to be bucketed into experiment
146- * @param {Object } logger Logger implementation
147- * @return {string } ID of experiment if user is bucketed into experiment within the group, null otherwise
148- */
149- bucketUserIntoExperiment : function ( group , bucketingId , userId , logger ) {
150- var bucketingKey = sprintf ( '%s%s' , bucketingId , group . id ) ;
151- var bucketValue = module . exports . _generateBucketValue ( bucketingKey ) ;
152- logger . log (
153- LOG_LEVEL . DEBUG ,
154- sprintf ( LOG_MESSAGES . USER_ASSIGNED_TO_EXPERIMENT_BUCKET , MODULE_NAME , bucketValue , userId )
112+ var entityId = this . _findBucket ( bucketValue , bucketerParams . trafficAllocationConfig ) ;
113+ if ( ! entityId ) {
114+ var userHasNoVariationLogMessage = sprintf (
115+ LOG_MESSAGES . USER_HAS_NO_VARIATION ,
116+ MODULE_NAME ,
117+ bucketerParams . userId ,
118+ bucketerParams . experimentKey
155119 ) ;
156- var trafficAllocationConfig = group . trafficAllocation ;
157- var bucketedExperimentId = module . exports . _findBucket ( bucketValue , trafficAllocationConfig ) ;
158- return bucketedExperimentId ;
159- } ,
160-
161- /**
162- * Returns entity ID associated with bucket value
163- * @param {string } bucketValue
164- * @param {Object[] } trafficAllocationConfig
165- * @param {number } trafficAllocationConfig[].endOfRange
166- * @param {number } trafficAllocationConfig[].entityId
167- * @return {string } Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise
168- */
169- _findBucket : function ( bucketValue , trafficAllocationConfig ) {
170- for ( var i = 0 ; i < trafficAllocationConfig . length ; i ++ ) {
171- if ( bucketValue < trafficAllocationConfig [ i ] . endOfRange ) {
172- return trafficAllocationConfig [ i ] . entityId ;
173- }
174- }
120+ bucketerParams . logger . log ( LOG_LEVEL . DEBUG , userHasNoVariationLogMessage ) ;
121+ } else if ( ! bucketerParams . variationIdMap . hasOwnProperty ( entityId ) ) {
122+ var invalidVariationIdLogMessage = sprintf ( LOG_MESSAGES . INVALID_VARIATION_ID , MODULE_NAME ) ;
123+ bucketerParams . logger . log ( LOG_LEVEL . WARNING , invalidVariationIdLogMessage ) ;
175124 return null ;
176- } ,
125+ } else {
126+ var variationKey = bucketerParams . variationIdMap [ entityId ] . key ;
127+ var userInVariationLogMessage = sprintf (
128+ LOG_MESSAGES . USER_HAS_VARIATION ,
129+ MODULE_NAME ,
130+ bucketerParams . userId ,
131+ variationKey ,
132+ bucketerParams . experimentKey
133+ ) ;
134+ bucketerParams . logger . log ( LOG_LEVEL . INFO , userInVariationLogMessage ) ;
135+ }
177136
178- /**
179- * Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE)
180- * @param {string } bucketingKey String value for bucketing
181- * @return {string } the generated bucket value
182- * @throws If bucketing value is not a valid string
183- */
184- _generateBucketValue : function ( bucketingKey ) {
185- try {
186- // NOTE: the mmh library already does cast the hash value as an unsigned 32bit int
187- // https://github.com/perezd/node-murmurhash/blob/master/murmurhash.js#L115
188- var hashValue = murmurhash . v3 ( bucketingKey , HASH_SEED ) ;
189- var ratio = hashValue / MAX_HASH_VALUE ;
190- return parseInt ( ratio * MAX_TRAFFIC_VALUE , 10 ) ;
191- } catch ( ex ) {
192- throw new Error ( sprintf ( ERROR_MESSAGES . INVALID_BUCKETING_ID , MODULE_NAME , bucketingKey , ex . message ) ) ;
137+ return entityId ;
138+ }
139+
140+ /**
141+ * Returns bucketed experiment ID to compare against experiment user is being called into
142+ * @param {Object } group Group that experiment is in
143+ * @param {string } bucketingId Bucketing ID
144+ * @param {string } userId ID of user to be bucketed into experiment
145+ * @param {Object } logger Logger implementation
146+ * @return {string } ID of experiment if user is bucketed into experiment within the group, null otherwise
147+ */
148+ export var bucketUserIntoExperiment = function ( group , bucketingId , userId , logger ) {
149+ var bucketingKey = sprintf ( '%s%s' , bucketingId , group . id ) ;
150+ var bucketValue = this . _generateBucketValue ( bucketingKey ) ;
151+ logger . log (
152+ LOG_LEVEL . DEBUG ,
153+ sprintf ( LOG_MESSAGES . USER_ASSIGNED_TO_EXPERIMENT_BUCKET , MODULE_NAME , bucketValue , userId )
154+ ) ;
155+ var trafficAllocationConfig = group . trafficAllocation ;
156+ var bucketedExperimentId = this . _findBucket ( bucketValue , trafficAllocationConfig ) ;
157+ return bucketedExperimentId ;
158+ }
159+
160+ /**
161+ * Returns entity ID associated with bucket value
162+ * @param {string } bucketValue
163+ * @param {Object[] } trafficAllocationConfig
164+ * @param {number } trafficAllocationConfig[].endOfRange
165+ * @param {number } trafficAllocationConfig[].entityId
166+ * @return {string } Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise
167+ */
168+ export var _findBucket = function ( bucketValue , trafficAllocationConfig ) {
169+ for ( var i = 0 ; i < trafficAllocationConfig . length ; i ++ ) {
170+ if ( bucketValue < trafficAllocationConfig [ i ] . endOfRange ) {
171+ return trafficAllocationConfig [ i ] . entityId ;
193172 }
194- } ,
195- } ;
173+ }
174+ return null ;
175+ }
176+
177+ /**
178+ * Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE)
179+ * @param {string } bucketingKey String value for bucketing
180+ * @return {string } the generated bucket value
181+ * @throws If bucketing value is not a valid string
182+ */
183+ export var _generateBucketValue = function ( bucketingKey ) {
184+ try {
185+ // NOTE: the mmh library already does cast the hash value as an unsigned 32bit int
186+ // https://github.com/perezd/node-murmurhash/blob/master/murmurhash.js#L115
187+ var hashValue = murmurhash . v3 ( bucketingKey , HASH_SEED ) ;
188+ var ratio = hashValue / MAX_HASH_VALUE ;
189+ return parseInt ( ratio * MAX_TRAFFIC_VALUE , 10 ) ;
190+ } catch ( ex ) {
191+ throw new Error ( sprintf ( ERROR_MESSAGES . INVALID_BUCKETING_ID , MODULE_NAME , bucketingKey , ex . message ) ) ;
192+ }
193+ }
194+
195+ export default {
196+ bucket,
197+ bucketUserIntoExperiment,
198+ _findBucket,
199+ _generateBucketValue,
200+ }
0 commit comments