19
19
*/
20
20
import { sprintf } from '@optimizely/js-sdk-utils' ;
21
21
import murmurhash from 'murmurhash' ;
22
+ import { LogHandler } from '@optimizely/js-sdk-logging' ;
23
+ import {
24
+ DecisionResponse ,
25
+ Experiment ,
26
+ Variation
27
+ } from '../../shared_types' ;
22
28
23
29
import {
24
30
ERROR_MESSAGES ,
25
31
LOG_LEVEL ,
26
32
LOG_MESSAGES ,
27
33
} from '../../utils/enums' ;
28
34
29
- var HASH_SEED = 1 ;
30
- var MAX_HASH_VALUE = Math . pow ( 2 , 32 ) ;
31
- var MAX_TRAFFIC_VALUE = 10000 ;
32
- var MODULE_NAME = 'BUCKETER' ;
33
- var RANDOM_POLICY = 'random' ;
35
+ const HASH_SEED = 1 ;
36
+ const MAX_HASH_VALUE = Math . pow ( 2 , 32 ) ;
37
+ const MAX_TRAFFIC_VALUE = 10000 ;
38
+ const MODULE_NAME = 'BUCKETER' ;
39
+ const RANDOM_POLICY = 'random' ;
40
+
41
+ interface TrafficAllocation {
42
+ entityId : string ;
43
+ endOfRange : number ;
44
+ }
45
+
46
+ interface Group {
47
+ id : string ;
48
+ policy : string ;
49
+ trafficAllocation : TrafficAllocation [ ] ;
50
+ }
51
+
52
+ interface BucketerParams {
53
+ experimentId : string ;
54
+ experimentKey : string ;
55
+ userId : string ;
56
+ trafficAllocationConfig : TrafficAllocation [ ] ;
57
+ experimentKeyMap : { [ key : string ] : Experiment } ;
58
+ groupIdMap : { [ key : string ] : Group } ;
59
+ variationIdMap : { [ id : string ] : Variation } ;
60
+ varationIdMapKey : string ;
61
+ logger : LogHandler ;
62
+ bucketingId : string ;
63
+ }
34
64
35
65
/**
36
66
* Determines ID of variation to be shown for the given input params
@@ -48,18 +78,18 @@ var RANDOM_POLICY = 'random';
48
78
* @return {Object } DecisionResponse DecisionResponse containing variation ID that user has been bucketed into,
49
79
* null if user is not bucketed into any experiment and the decide reasons.
50
80
*/
51
- export var bucket = function ( bucketerParams ) {
52
- var decideReasons = [ ] ;
81
+ export const bucket = function ( bucketerParams : BucketerParams ) : DecisionResponse < string | null > {
82
+ const decideReasons : string [ ] = [ ] ;
53
83
// Check if user is in a random group; if so, check if user is bucketed into a specific experiment
54
- var experiment = bucketerParams . experimentKeyMap [ bucketerParams . experimentKey ] ;
55
- var groupId = experiment [ 'groupId' ] ;
84
+ const experiment = bucketerParams . experimentKeyMap [ bucketerParams . experimentKey ] ;
85
+ const groupId = experiment [ 'groupId' ] ;
56
86
if ( groupId ) {
57
- var group = bucketerParams . groupIdMap [ groupId ] ;
87
+ const group = bucketerParams . groupIdMap [ groupId ] ;
58
88
if ( ! group ) {
59
89
throw new Error ( sprintf ( ERROR_MESSAGES . INVALID_GROUP_ID , MODULE_NAME , groupId ) ) ;
60
90
}
61
91
if ( group . policy === RANDOM_POLICY ) {
62
- var bucketedExperimentId = this . bucketUserIntoExperiment (
92
+ const bucketedExperimentId = bucketUserIntoExperiment (
63
93
group ,
64
94
bucketerParams . bucketingId ,
65
95
bucketerParams . userId ,
@@ -68,7 +98,7 @@ export var bucket = function(bucketerParams) {
68
98
69
99
// Return if user is not bucketed into any experiment
70
100
if ( bucketedExperimentId === null ) {
71
- var notbucketedInAnyExperimentLogMessage = sprintf (
101
+ const notbucketedInAnyExperimentLogMessage = sprintf (
72
102
LOG_MESSAGES . USER_NOT_IN_ANY_EXPERIMENT ,
73
103
MODULE_NAME ,
74
104
bucketerParams . userId ,
@@ -84,7 +114,7 @@ export var bucket = function(bucketerParams) {
84
114
85
115
// Return if user is bucketed into a different experiment than the one specified
86
116
if ( bucketedExperimentId !== bucketerParams . experimentId ) {
87
- var notBucketedIntoExperimentOfGroupLogMessage = sprintf (
117
+ const notBucketedIntoExperimentOfGroupLogMessage = sprintf (
88
118
LOG_MESSAGES . USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP ,
89
119
MODULE_NAME ,
90
120
bucketerParams . userId ,
@@ -100,7 +130,7 @@ export var bucket = function(bucketerParams) {
100
130
}
101
131
102
132
// Continue bucketing if user is bucketed into specified experiment
103
- var bucketedIntoExperimentOfGroupLogMessage = sprintf (
133
+ const bucketedIntoExperimentOfGroupLogMessage = sprintf (
104
134
LOG_MESSAGES . USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP ,
105
135
MODULE_NAME ,
106
136
bucketerParams . userId ,
@@ -111,10 +141,10 @@ export var bucket = function(bucketerParams) {
111
141
decideReasons . push ( bucketedIntoExperimentOfGroupLogMessage ) ;
112
142
}
113
143
}
114
- var bucketingId = sprintf ( '%s%s' , bucketerParams . bucketingId , bucketerParams . experimentId ) ;
115
- var bucketValue = this . _generateBucketValue ( bucketingId ) ;
144
+ const bucketingId = sprintf ( '%s%s' , bucketerParams . bucketingId , bucketerParams . experimentId ) ;
145
+ const bucketValue = _generateBucketValue ( bucketingId ) ;
116
146
117
- var bucketedUserLogMessage = sprintf (
147
+ const bucketedUserLogMessage = sprintf (
118
148
LOG_MESSAGES . USER_ASSIGNED_TO_EXPERIMENT_BUCKET ,
119
149
MODULE_NAME ,
120
150
bucketValue ,
@@ -123,18 +153,19 @@ export var bucket = function(bucketerParams) {
123
153
bucketerParams . logger . log ( LOG_LEVEL . DEBUG , bucketedUserLogMessage ) ;
124
154
decideReasons . push ( bucketedUserLogMessage ) ;
125
155
126
- var entityId = this . _findBucket ( bucketValue , bucketerParams . trafficAllocationConfig ) ;
127
-
128
- if ( ! bucketerParams . variationIdMap . hasOwnProperty ( entityId ) ) {
129
- if ( entityId ) {
130
- var invalidVariationIdLogMessage = sprintf ( LOG_MESSAGES . INVALID_VARIATION_ID , MODULE_NAME ) ;
131
- bucketerParams . logger . log ( LOG_LEVEL . WARNING , invalidVariationIdLogMessage ) ;
132
- decideReasons . push ( invalidVariationIdLogMessage ) ;
156
+ const entityId = _findBucket ( bucketValue , bucketerParams . trafficAllocationConfig ) ;
157
+ if ( entityId !== null ) {
158
+ if ( ! bucketerParams . variationIdMap [ entityId ] ) {
159
+ if ( entityId ) {
160
+ const invalidVariationIdLogMessage = sprintf ( LOG_MESSAGES . INVALID_VARIATION_ID , MODULE_NAME ) ;
161
+ bucketerParams . logger . log ( LOG_LEVEL . WARNING , invalidVariationIdLogMessage ) ;
162
+ decideReasons . push ( invalidVariationIdLogMessage ) ;
163
+ }
164
+ return {
165
+ result : null ,
166
+ reasons : decideReasons ,
167
+ } ;
133
168
}
134
- return {
135
- result : null ,
136
- reasons : decideReasons ,
137
- } ;
138
169
}
139
170
140
171
return {
@@ -145,54 +176,63 @@ export var bucket = function(bucketerParams) {
145
176
146
177
/**
147
178
* Returns bucketed experiment ID to compare against experiment user is being called into
148
- * @param { Object } group Group that experiment is in
149
- * @param {string } bucketingId Bucketing ID
150
- * @param {string } userId ID of user to be bucketed into experiment
151
- * @param { Object } logger Logger implementation
152
- * @return {string|null } ID of experiment if user is bucketed into experiment within the group, null otherwise
179
+ * @param { Group } group Group that experiment is in
180
+ * @param {string } bucketingId Bucketing ID
181
+ * @param {string } userId ID of user to be bucketed into experiment
182
+ * @param { LogHandler } logger Logger implementation
183
+ * @return {string|null } ID of experiment if user is bucketed into experiment within the group, null otherwise
153
184
*/
154
- export var bucketUserIntoExperiment = function ( group , bucketingId , userId , logger ) {
155
- var bucketingKey = sprintf ( '%s%s' , bucketingId , group . id ) ;
156
- var bucketValue = this . _generateBucketValue ( bucketingKey ) ;
185
+ export const bucketUserIntoExperiment = function (
186
+ group : Group ,
187
+ bucketingId : string ,
188
+ userId : string ,
189
+ logger : LogHandler
190
+ ) : string | null {
191
+ const bucketingKey = sprintf ( '%s%s' , bucketingId , group . id ) ;
192
+ const bucketValue = _generateBucketValue ( bucketingKey ) ;
157
193
logger . log (
158
194
LOG_LEVEL . DEBUG ,
159
195
sprintf ( LOG_MESSAGES . USER_ASSIGNED_TO_EXPERIMENT_BUCKET , MODULE_NAME , bucketValue , userId )
160
196
) ;
161
- var trafficAllocationConfig = group . trafficAllocation ;
162
- var bucketedExperimentId = this . _findBucket ( bucketValue , trafficAllocationConfig ) ;
197
+ const trafficAllocationConfig = group . trafficAllocation ;
198
+ const bucketedExperimentId = _findBucket ( bucketValue , trafficAllocationConfig ) ;
163
199
return bucketedExperimentId ;
164
200
} ;
165
201
166
202
/**
167
203
* Returns entity ID associated with bucket value
168
- * @param {string } bucketValue
169
- * @param {Object [] } trafficAllocationConfig
170
- * @param {number } trafficAllocationConfig[].endOfRange
171
- * @param {number } trafficAllocationConfig[].entityId
172
- * @return {string|null } Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise
204
+ * @param {number } bucketValue
205
+ * @param {TrafficAllocation [] } trafficAllocationConfig
206
+ * @param {number } trafficAllocationConfig[].endOfRange
207
+ * @param {string } trafficAllocationConfig[].entityId
208
+ * @return {string|null } Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise
173
209
*/
174
- export var _findBucket = function ( bucketValue , trafficAllocationConfig ) {
175
- for ( var i = 0 ; i < trafficAllocationConfig . length ; i ++ ) {
210
+ export const _findBucket = function (
211
+ bucketValue : number ,
212
+ trafficAllocationConfig : TrafficAllocation [ ]
213
+ ) : string | null {
214
+ for ( let i = 0 ; i < trafficAllocationConfig . length ; i ++ ) {
176
215
if ( bucketValue < trafficAllocationConfig [ i ] . endOfRange ) {
177
216
return trafficAllocationConfig [ i ] . entityId ;
178
217
}
179
218
}
219
+
180
220
return null ;
181
221
} ;
182
222
183
223
/**
184
224
* Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE)
185
- * @param {string } bucketingKey String value for bucketing
186
- * @return {string } the generated bucket value
187
- * @throws If bucketing value is not a valid string
225
+ * @param {string } bucketingKey String value for bucketing
226
+ * @return {number } The generated bucket value
227
+ * @throws If bucketing value is not a valid string
188
228
*/
189
- export var _generateBucketValue = function ( bucketingKey ) {
229
+ export const _generateBucketValue = function ( bucketingKey : string ) : number {
190
230
try {
191
231
// NOTE: the mmh library already does cast the hash value as an unsigned 32bit int
192
232
// https://github.com/perezd/node-murmurhash/blob/master/murmurhash.js#L115
193
- var hashValue = murmurhash . v3 ( bucketingKey , HASH_SEED ) ;
194
- var ratio = hashValue / MAX_HASH_VALUE ;
195
- return parseInt ( ratio * MAX_TRAFFIC_VALUE , 10 ) ;
233
+ const hashValue = murmurhash . v3 ( bucketingKey , HASH_SEED ) ;
234
+ const ratio = hashValue / MAX_HASH_VALUE ;
235
+ return Math . floor ( ratio * MAX_TRAFFIC_VALUE ) ;
196
236
} catch ( ex ) {
197
237
throw new Error ( sprintf ( ERROR_MESSAGES . INVALID_BUCKETING_ID , MODULE_NAME , bucketingKey , ex . message ) ) ;
198
238
}
@@ -201,6 +241,4 @@ export var _generateBucketValue = function(bucketingKey) {
201
241
export default {
202
242
bucket : bucket ,
203
243
bucketUserIntoExperiment : bucketUserIntoExperiment ,
204
- _findBucket : _findBucket ,
205
- _generateBucketValue : _generateBucketValue ,
206
244
} ;
0 commit comments