This repository was archived by the owner on Feb 19, 2025. It is now read-only.

File tree

439 files changed

+53752
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searcx below for content that may be hidden.

439 files changed

+53752
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ This picture show how we get the source code.
4141
---
4242
### G
4343
* Gatsby Homepage (https://www.gatsbyjs.com) - Landing Page / [Khoa Huynh](https://.com/htdangkhoa), 2021/09/21
44+
* Glitch (https://glitch.com) - Codesandbox / Nguy Thang, 2021/09/23
4445

4546
---
4647
### H
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Shared React components for community & editor:
2+
https://.com/glitchdotcom/shared-components
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* Copyright 2016, 2018-2020, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import {
17+
sprintf
18+
} from '@optimizely/js-sdk-utils';
19+
import {
20+
getLogger
21+
} from '@optimizely/js-sdk-logging';
22+
23+
import fns from '../../utils/fns';
24+
import {
25+
LOG_LEVEL,
26+
LOG_MESSAGES,
27+
ERROR_MESSAGES,
28+
} from '../../utils/enums';
29+
import * as conditionTreeEvaluator from '../condition_tree_evaluator';
30+
import * as customAttributeConditionEvaluator from '../custom_attribute_condition_evaluator';
31+
32+
var logger = getLogger();
33+
var MODULE_NAME = 'AUDIENCE_EVALUATOR';
34+
35+
/**
36+
* Construct an instance of AudienceEvaluator with given options
37+
* @param {Object=} UNSTABLE_conditionEvaluators A map of condition evaluators provided by the consumer. This enables matching
38+
* condition types which are not supported natively by the SDK. Note that built in
39+
* Optimizely evaluators cannot be overridden.
40+
* @constructor
41+
*/
42+
function AudienceEvaluator(UNSTABLE_conditionEvaluators) {
43+
this.typeToEvaluatorMap = fns.assign({}, UNSTABLE_conditionEvaluators, {
44+
custom_attribute: customAttributeConditionEvaluator,
45+
});
46+
}
47+
48+
/**
49+
* Determine if the given user attributes satisfy the given audience conditions
50+
* @param {Array|String|null|undefined} audienceConditions Audience conditions to match the user attributes against - can be an array
51+
* of audience IDs, a nested array of conditions, or a single leaf condition.
52+
* Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"], "1"
53+
* @param {Object} audiencesById Object providing access to full audience objects for audience IDs
54+
* contained in audienceConditions. Keys should be audience IDs, values
55+
* should be full audience objects with conditions properties
56+
* @param {Object} [userAttributes] User attributes which will be used in determining if audience conditions
57+
* are met. If not provided, defaults to an empty object
58+
* @return {Boolean} true if the user attributes match the given audience conditions, false
59+
* otherwise
60+
*/
61+
AudienceEvaluator..evaluate = function(audienceConditions, audiencesById, userAttributes) {
62+
// if there are no audiences, return true because that means ALL users are included in the experiment
63+
if (!audienceConditions || audienceConditions.length === 0) {
64+
return true;
65+
}
66+
67+
if (!userAttributes) {
68+
userAttributes = {};
69+
}
70+
71+
var evaluateAudience = function(audienceId) {
72+
var audience = audiencesById[audienceId];
73+
if (audience) {
74+
logger.log(
75+
LOG_LEVEL.DEBUG,
76+
sprintf(LOG_MESSAGES.EVALUATING_AUDIENCE, MODULE_NAME, audienceId, JSON.stringify(audience.conditions))
77+
);
78+
var result = conditionTreeEvaluator.evaluate(
79+
audience.conditions,
80+
this.evaluateConditionWithUserAttributes.bind(this, userAttributes)
81+
);
82+
var resultText = result === null ? 'UNKNOWN' : result.toString().toUpperCase();
83+
logger.log(LOG_LEVEL.DEBUG, sprintf(LOG_MESSAGES.AUDIENCE_EVALUATION_RESULT, MODULE_NAME, audienceId, resultText));
84+
return result;
85+
}
86+
87+
return null;
88+
}.bind(this);
89+
90+
return conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience) || false;
91+
};
92+
93+
/**
94+
* Wrapper around evaluator.evaluate that is passed to the conditionTreeEvaluator.
95+
* Evaluates the condition provided given the user attributes if an evaluator has been defined for the condition type.
96+
* @param {Object} userAttributes A map of user attributes.
97+
* @param {Object} condition A single condition object to evaluate.
98+
* @return {Boolean|null} true if the condition is satisfied, null if a matcher is not found.
99+
*/
100+
AudienceEvaluator..evaluateConditionWithUserAttributes = function(userAttributes, condition) {
101+
var evaluator = this.typeToEvaluatorMap[condition.type];
102+
if (!evaluator) {
103+
logger.log(LOG_LEVEL.WARNING, sprintf(LOG_MESSAGES.UNKNOWN_CONDITION_TYPE, MODULE_NAME, JSON.stringify(condition)));
104+
return null;
105+
}
106+
try {
107+
return evaluator.evaluate(condition, userAttributes, logger);
108+
} catch (err) {
109+
logger.log(
110+
LOG_LEVEL.ERROR,
111+
sprintf(ERROR_MESSAGES.CONDITION_EVALUATOR_ERROR, MODULE_NAME, condition.type, err.message)
112+
);
113+
}
114+
return null;
115+
};
116+
117+
export default AudienceEvaluator;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Copyright 2016, 2019-2020, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Bucketer API for determining the variation id from the specified parameters
19+
*/
20+
import {
21+
sprintf
22+
} from '@optimizely/js-sdk-utils';
23+
import murmurhash from 'murmurhash';
24+
25+
import {
26+
ERROR_MESSAGES,
27+
LOG_LEVEL,
28+
LOG_MESSAGES,
29+
} from '../../utils/enums';
30+
31+
var HASH_SEED = 1;
32+
var MAX_HASH_VALUE = Math.pow(2, 32);
33+
var MAX_TRAFFIC_VALUE = 10000;
34+
var MODULE_NAME = 'BUCKETER';
35+
var RANDOM_POLICY = 'random';
36+
37+
/**
38+
* Determines ID of variation to be shown for the given input params
39+
* @param {Object} bucketerParams
40+
* @param {string} bucketerParams.experimentId
41+
* @param {string} bucketerParams.experimentKey
42+
* @param {string} bucketerParams.userId
43+
* @param {Object[]} bucketerParams.trafficAllocationConfig
44+
* @param {Array} bucketerParams.experimentKeyMap
45+
* @param {Object} bucketerParams.groupIdMap
46+
* @param {Object} bucketerParams.variationIdMap
47+
* @param {string} bucketerParams.varationIdMap[].key
48+
* @param {Object} bucketerParams.logger
49+
* @param {string} bucketerParams.bucketingId
50+
* @return Variation ID that user has been bucketed into, null if user is not bucketed into any experiment
51+
*/
52+
export var bucket = function(bucketerParams) {
53+
// 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'];
56+
if (groupId) {
57+
var group = bucketerParams.groupIdMap[groupId];
58+
if (!group) {
59+
throw new Error(sprintf(ERROR_MESSAGES.INVALID_GROUP_ID, MODULE_NAME, groupId));
60+
}
61+
if (group.policy === RANDOM_POLICY) {
62+
var bucketedExperimentId = this.bucketUserIntoExperiment(
63+
group,
64+
bucketerParams.bucketingId,
65+
bucketerParams.userId,
66+
bucketerParams.logger
67+
);
68+
69+
// Return if user is not bucketed into any experiment
70+
if (bucketedExperimentId === null) {
71+
var notbucketedInAnyExperimentLogMessage = sprintf(
72+
LOG_MESSAGES.USER_NOT_IN_ANY_EXPERIMENT,
73+
MODULE_NAME,
74+
bucketerParams.userId,
75+
groupId
76+
);
77+
bucketerParams.logger.log(LOG_LEVEL.INFO, notbucketedInAnyExperimentLogMessage);
78+
return null;
79+
}
80+
81+
// Return if user is bucketed into a different experiment than the one specified
82+
if (bucketedExperimentId !== bucketerParams.experimentId) {
83+
var notBucketedIntoExperimentOfGroupLogMessage = sprintf(
84+
LOG_MESSAGES.USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP,
85+
MODULE_NAME,
86+
bucketerParams.userId,
87+
bucketerParams.experimentKey,
88+
groupId
89+
);
90+
bucketerParams.logger.log(LOG_LEVEL.INFO, notBucketedIntoExperimentOfGroupLogMessage);
91+
return null;
92+
}
93+
94+
// Continue bucketing if user is bucketed into specified experiment
95+
var bucketedIntoExperimentOfGroupLogMessage = sprintf(
96+
LOG_MESSAGES.USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP,
97+
MODULE_NAME,
98+
bucketerParams.userId,
99+
bucketerParams.experimentKey,
100+
groupId
101+
);
102+
bucketerParams.logger.log(LOG_LEVEL.INFO, bucketedIntoExperimentOfGroupLogMessage);
103+
}
104+
}
105+
var bucketingId = sprintf('%s%s', bucketerParams.bucketingId, bucketerParams.experimentId);
106+
var bucketValue = this._generateBucketValue(bucketingId);
107+
108+
var bucketedUserLogMessage = sprintf(
109+
LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET,
110+
MODULE_NAME,
111+
bucketValue,
112+
bucketerParams.userId
113+
);
114+
bucketerParams.logger.log(LOG_LEVEL.DEBUG, bucketedUserLogMessage);
115+
116+
var entityId = this._findBucket(bucketValue, bucketerParams.trafficAllocationConfig);
117+
118+
if (!bucketerParams.variationIdMap.hasOwnProperty(entityId)) {
119+
if (entityId) {
120+
var invalidVariationIdLogMessage = sprintf(LOG_MESSAGES.INVALID_VARIATION_ID, MODULE_NAME);
121+
bucketerParams.logger.log(LOG_LEVEL.WARNING, invalidVariationIdLogMessage);
122+
}
123+
return null;
124+
}
125+
126+
return entityId;
127+
};
128+
129+
/**
130+
* Returns bucketed experiment ID to compare against experiment user is being called into
131+
* @param {Object} group Group that experiment is in
132+
* @param {string} bucketingId Bucketing ID
133+
* @param {string} userId ID of user to be bucketed into experiment
134+
* @param {Object} logger Logger implementation
135+
* @return {string|null} ID of experiment if user is bucketed into experiment within the group, null otherwise
136+
*/
137+
export var bucketUserIntoExperiment = function(group, bucketingId, userId, logger) {
138+
var bucketingKey = sprintf('%s%s', bucketingId, group.id);
139+
var bucketValue = this._generateBucketValue(bucketingKey);
140+
logger.log(
141+
LOG_LEVEL.DEBUG,
142+
sprintf(LOG_MESSAGES.USER_ASSIGNED_TO_EXPERIMENT_BUCKET, MODULE_NAME, bucketValue, userId)
143+
);
144+
var trafficAllocationConfig = group.trafficAllocation;
145+
var bucketedExperimentId = this._findBucket(bucketValue, trafficAllocationConfig);
146+
return bucketedExperimentId;
147+
};
148+
149+
/**
150+
* Returns entity ID associated with bucket value
151+
* @param {string} bucketValue
152+
* @param {Object[]} trafficAllocationConfig
153+
* @param {number} trafficAllocationConfig[].endOfRange
154+
* @param {number} trafficAllocationConfig[].entityId
155+
* @return {string|null} Entity ID for bucketing if bucket value is within traffic allocation boundaries, null otherwise
156+
*/
157+
export var _findBucket = function(bucketValue, trafficAllocationConfig) {
158+
for (var i = 0; i < trafficAllocationConfig.length; i++) {
159+
if (bucketValue < trafficAllocationConfig[i].endOfRange) {
160+
return trafficAllocationConfig[i].entityId;
161+
}
162+
}
163+
return null;
164+
};
165+
166+
/**
167+
* Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE)
168+
* @param {string} bucketingKey String value for bucketing
169+
* @return {string} the generated bucket value
170+
* @throws If bucketing value is not a valid string
171+
*/
172+
export var _generateBucketValue = function(bucketingKey) {
173+
try {
174+
// NOTE: the mmh library already does cast the hash value as an unsigned 32bit int
175+
// https://.com/perezd/node-murmurhash/blob/master/murmurhash.js#L115
176+
var hashValue = murmurhash.v3(bucketingKey, HASH_SEED);
177+
var ratio = hashValue / MAX_HASH_VALUE;
178+
return parseInt(ratio * MAX_TRAFFIC_VALUE, 10);
179+
} catch (ex) {
180+
throw new Error(sprintf(ERROR_MESSAGES.INVALID_BUCKETING_ID, MODULE_NAME, bucketingKey, ex.message));
181+
}
182+
};
183+
184+
export default {
185+
bucket: bucket,
186+
bucketUserIntoExperiment: bucketUserIntoExperiment,
187+
_findBucket: _findBucket,
188+
_generateBucketValue: _generateBucketValue,
189+
};

0 commit comments

Comments
 (0)