@@ -27,6 +27,7 @@ var ERROR_MESSAGES = enums.ERROR_MESSAGES;
27
27
var LOG_LEVEL = enums . LOG_LEVEL ;
28
28
var LOG_MESSAGES = enums . LOG_MESSAGES ;
29
29
var RESERVED_ATTRIBUTE_KEY_BUCKETING_ID = '$opt_bucketing_id' ;
30
+ var DECISION_SOURCES = enums . DECISION_SOURCES ;
30
31
31
32
32
33
/**
@@ -260,11 +261,184 @@ DecisionService.prototype.__saveUserProfile = function(userProfile, experiment,
260
261
}
261
262
} ;
262
263
264
+ /**
265
+ * Given a feature, user ID, and attributes, returns an object representing a
266
+ * decision. If the user was bucketed into a variation for the given feature
267
+ * and attributes, the returned decision object will have variation and
268
+ * experiment properties (both objects), as well as a decisionSource property.
269
+ * decisionSource indicates whether the decision was due to a rollout or an
270
+ * experiment.
271
+ * @param {Object } feature A feature flag object from project configuration
272
+ * @param {String } userId A string identifying the user, for bucketing
273
+ * @param {Object } attributes Optional user attributes
274
+ * @return {Object } An object with experiment, variation, and decisionSource
275
+ * properties. If the user was not bucketed into a variation, the variation
276
+ * property is null.
277
+ */
278
+ DecisionService . prototype . getVariationForFeature = function ( feature , userId , attributes ) {
279
+ var experimentDecision = this . _getVariationForFeatureExperiment ( feature , userId , attributes ) ;
280
+ if ( experimentDecision . variation !== null ) {
281
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_IN_FEATURE_EXPERIMENT , MODULE_NAME , userId , experimentDecision . variation . key , experimentDecision . experiment . key , feature . key ) ) ;
282
+ return experimentDecision ;
283
+ }
284
+
285
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_NOT_IN_FEATURE_EXPERIMENT , MODULE_NAME , userId , feature . key ) ) ;
286
+
287
+ var rolloutDecision = this . _getVariationForRollout ( feature , userId , attributes ) ;
288
+ if ( rolloutDecision . variation !== null ) {
289
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_IN_ROLLOUT , MODULE_NAME , userId , feature . key ) ) ;
290
+ return rolloutDecision ;
291
+ }
292
+
293
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_NOT_IN_ROLLOUT , MODULE_NAME , userId , feature . key ) ) ;
294
+
295
+ return {
296
+ experiment : null ,
297
+ variation : null ,
298
+ decisionSource : null ,
299
+ } ;
300
+ } ;
301
+
302
+ DecisionService . prototype . _getVariationForFeatureExperiment = function ( feature , userId , attributes ) {
303
+ var experiment = null ;
304
+ var variationKey = null ;
305
+
306
+ if ( feature . hasOwnProperty ( 'groupId' ) ) {
307
+ var group = this . configObj . groupIdMap [ feature . groupId ] ;
308
+ if ( group ) {
309
+ experiment = this . _getExperimentInGroup ( group , userId ) ;
310
+ if ( experiment ) {
311
+ variationKey = this . getVariation ( experiment . key , userId , attributes ) ;
312
+ }
313
+ }
314
+ } else if ( feature . experimentIds . length > 0 ) {
315
+ // If the feature does not have a group ID, then it can only be associated
316
+ // with one experiment, so we look at the first experiment ID only
317
+ experiment = projectConfig . getExperimentFromId ( this . configObj , feature . experimentIds [ 0 ] , this . logger ) ;
318
+ if ( experiment ) {
319
+ variationKey = this . getVariation ( experiment . key , userId , attributes ) ;
320
+ }
321
+ } else {
322
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . FEATURE_HAS_NO_EXPERIMENTS , MODULE_NAME , feature . key ) ) ;
323
+ }
324
+
325
+ var variation = null ;
326
+ if ( variationKey !== null && experiment !== null ) {
327
+ variation = experiment . variationKeyMap [ variationKey ] ;
328
+ }
329
+ return {
330
+ experiment : experiment ,
331
+ variation : variation ,
332
+ decisionSource : DECISION_SOURCES . EXPERIMENT ,
333
+ } ;
334
+ } ;
335
+
336
+ DecisionService . prototype . _getExperimentInGroup = function ( group , userId ) {
337
+ var experimentId = bucketer . bucketUserIntoExperiment ( group , userId , userId , this . logger ) ;
338
+ if ( experimentId !== null ) {
339
+ this . logger . log ( LOG_LEVEL . INFO , sprintf ( LOG_MESSAGES . USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP , MODULE_NAME , userId , experimentId , group . id ) ) ;
340
+ var experiment = projectConfig . getExperimentFromId ( this . configObj , experimentId , this . logger ) ;
341
+ if ( experiment ) {
342
+ return experiment ;
343
+ }
344
+ }
345
+
346
+ this . logger . log ( LOG_LEVEL . INFO , sprintf ( LOG_MESSAGES . USER_NOT_BUCKETED_INTO_ANY_EXPERIMENT_IN_GROUP , MODULE_NAME , userId , group . id ) ) ;
347
+ return null ;
348
+ } ;
349
+
350
+ DecisionService . prototype . _getVariationForRollout = function ( feature , userId , attributes ) {
351
+ if ( ! feature . rolloutId ) {
352
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . NO_ROLLOUT_EXISTS , MODULE_NAME , feature . key ) ) ;
353
+ return {
354
+ experiment : null ,
355
+ variation : null ,
356
+ decisionSource : DECISION_SOURCES . ROLLOUT ,
357
+ } ;
358
+ }
359
+
360
+ var rollout = this . configObj . rolloutIdMap [ feature . rolloutId ] ;
361
+ if ( ! rollout ) {
362
+ this . logger . log ( LOG_LEVEL . ERROR , sprintf ( ERROR_MESSAGES . INVALID_ROLLOUT_ID , MODULE_NAME , feature . rolloutId , feature . key ) ) ;
363
+ return {
364
+ experiment : null ,
365
+ variation : null ,
366
+ decisionSource : DECISION_SOURCES . ROLLOUT ,
367
+ } ;
368
+ }
369
+
370
+ if ( rollout . experiments . length === 0 ) {
371
+ this . logger . log ( LOG_LEVEL . ERROR , sprintf ( LOG_MESSAGES . ROLLOUT_HAS_NO_EXPERIMENTS , MODULE_NAME , feature . rolloutId ) ) ;
372
+ return {
373
+ experiment : null ,
374
+ variation : null ,
375
+ decisionSource : DECISION_SOURCES . ROLLOUT ,
376
+ } ;
377
+ }
378
+
379
+ // The end index is length - 1 because the last experiment is assumed to be
380
+ // "everyone else", which will be evaluated separately outside this loop
381
+ var endIndex = rollout . experiments . length - 1 ;
382
+ var index ;
383
+ var experiment ;
384
+ var bucketerParams ;
385
+ var variationId ;
386
+ var variation ;
387
+ for ( index = 0 ; index < endIndex ; index ++ ) {
388
+ experiment = this . configObj . experimentKeyMap [ rollout . experiments [ index ] . key ] ;
389
+
390
+ if ( ! this . __checkIfUserIsInAudience ( experiment . key , userId , attributes ) ) {
391
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE , MODULE_NAME , userId , index + 1 ) ) ;
392
+ continue ;
393
+ }
394
+
395
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_MEETS_CONDITIONS_FOR_TARGETING_RULE , MODULE_NAME , userId , index + 1 ) ) ;
396
+ bucketerParams = this . __buildBucketerParams ( experiment . key , userId , userId ) ;
397
+ variationId = bucketer . bucket ( bucketerParams ) ;
398
+ variation = this . configObj . variationIdMap [ variationId ] ;
399
+ if ( variation ) {
400
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_BUCKETED_INTO_TARGETING_RULE , MODULE_NAME , userId , index + 1 ) ) ;
401
+ return {
402
+ experiment : experiment ,
403
+ variation : variation ,
404
+ decisionSource : DECISION_SOURCES . ROLLOUT ,
405
+ } ;
406
+ } else {
407
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_NOT_BUCKETED_INTO_TARGETING_RULE , MODULE_NAME , userId , index + 1 ) ) ;
408
+ break ;
409
+ }
410
+ }
411
+
412
+ var everyoneElseExperiment = this . configObj . experimentKeyMap [ rollout . experiments [ endIndex ] . key ] ;
413
+ if ( this . __checkIfUserIsInAudience ( everyoneElseExperiment . key , userId , attributes ) ) {
414
+ bucketerParams = this . __buildBucketerParams ( everyoneElseExperiment . key , userId , userId ) ;
415
+ variationId = bucketer . bucket ( bucketerParams ) ;
416
+ variation = this . configObj . variationIdMap [ variationId ] ;
417
+ if ( variation ) {
418
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_BUCKETED_INTO_EVERYONE_TARGETING_RULE , MODULE_NAME , userId ) ) ;
419
+ return {
420
+ experiment : everyoneElseExperiment ,
421
+ variation : variation ,
422
+ decisionSource : DECISION_SOURCES . ROLLOUT ,
423
+ } ;
424
+ } else {
425
+ this . logger . log ( LOG_LEVEL . DEBUG , sprintf ( LOG_MESSAGES . USER_NOT_BUCKETED_INTO_EVERYONE_TARGETING_RULE , MODULE_NAME , userId ) ) ;
426
+ }
427
+ }
428
+
429
+ return {
430
+ experiment : null ,
431
+ variation : null ,
432
+ decisionSource : DECISION_SOURCES . ROLLOUT ,
433
+ } ;
434
+ } ;
435
+
263
436
module . exports = {
264
437
/**
265
438
* Creates an instance of the DecisionService.
266
439
* @param {Object } options Configuration options
267
- * @param {Object } options.projectConfig
440
+ * @param {Object } options.configObj
441
+ * @param {Object } options.userProfileService
268
442
* @param {Object } options.logger
269
443
* @return {Object } An instance of the DecisionService
270
444
*/
0 commit comments