@optimizely/optimizely-sdk
Version:
JavaScript SDK for Optimizely X Full Stack
1,056 lines (1,047 loc) • 227 kB
JavaScript
import { ConsoleLogHandler, getLogger, setLogHandler, setLogLevel, LogLevel, setErrorHandler, getErrorHandler } from '@optimizely/js-sdk-logging';
export { setLogLevel, setLogHandler as setLogger } from '@optimizely/js-sdk-logging';
import { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '@optimizely/js-sdk-event-processor';
import { keyBy as keyBy$1, NOTIFICATION_TYPES as NOTIFICATION_TYPES$1, sprintf, objectValues, generateUUID } from '@optimizely/js-sdk-utils';
import { HttpPollingDatafileManager } from '@optimizely/js-sdk-datafile-manager';
import murmurhash from 'murmurhash';
/**
* Copyright 2017, 2019-2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var MAX_SAFE_INTEGER_LIMIT = Math.pow(2, 53);
// eslint-disable-next-line
function assign(target) {
var sources = [];
for (var _i = 1; _i < arguments.length; _i++) {
sources[_i - 1] = arguments[_i];
}
if (!target) {
return {};
}
if (typeof Object.assign === 'function') {
return Object.assign.apply(Object, [target].concat(sources));
}
else {
var to = Object(target);
for (var index = 0; index < sources.length; index++) {
var nextSource = sources[index];
if (nextSource !== null && nextSource !== undefined) {
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
}
}
function currentTimestamp() {
return Math.round(new Date().getTime());
}
function isSafeInteger(number) {
return typeof number == 'number' && Math.abs(number) <= MAX_SAFE_INTEGER_LIMIT;
}
function keyBy(arr, key) {
if (!arr)
return {};
return keyBy$1(arr, function (item) {
return item[key];
});
}
function isNumber(value) {
return typeof value === 'number';
}
/****************************************************************************
* Copyright 2016-2020, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
* You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
***************************************************************************/
/**
* Contains global enums used throughout the library
*/
var LOG_LEVEL = {
NOTSET: 0,
DEBUG: 1,
INFO: 2,
WARNING: 3,
ERROR: 4,
};
var ERROR_MESSAGES = {
CONDITION_EVALUATOR_ERROR: '%s: Error evaluating audience condition of type %s: %s',
DATAFILE_AND_SDK_KEY_MISSING: '%s: You must provide at least one of sdkKey or datafile. Cannot start Optimizely',
EXPERIMENT_KEY_NOT_IN_DATAFILE: '%s: Experiment key %s is not in datafile.',
FEATURE_NOT_IN_DATAFILE: '%s: Feature key %s is not in datafile.',
IMPROPERLY_FORMATTED_EXPERIMENT: '%s: Experiment key %s is improperly formatted.',
INVALID_ATTRIBUTES: '%s: Provided attributes are in an invalid format.',
INVALID_BUCKETING_ID: '%s: Unable to generate hash for bucketing ID %s: %s',
INVALID_DATAFILE: '%s: Datafile is invalid - property %s: %s',
INVALID_DATAFILE_MALFORMED: '%s: Datafile is invalid because it is malformed.',
INVALID_CONFIG: '%s: Provided Optimizely config is in an invalid format.',
INVALID_JSON: '%s: JSON object is not valid.',
INVALID_ERROR_HANDLER: '%s: Provided "errorHandler" is in an invalid format.',
INVALID_EVENT_DISPATCHER: '%s: Provided "eventDispatcher" is in an invalid format.',
INVALID_EVENT_TAGS: '%s: Provided event tags are in an invalid format.',
INVALID_EXPERIMENT_KEY: '%s: Experiment key %s is not in datafile. It is either invalid, paused, or archived.',
INVALID_EXPERIMENT_ID: '%s: Experiment ID %s is not in datafile.',
INVALID_GROUP_ID: '%s: Group ID %s is not in datafile.',
INVALID_LOGGER: '%s: Provided "logger" is in an invalid format.',
INVALID_ROLLOUT_ID: '%s: Invalid rollout ID %s attached to feature %s',
INVALID_USER_ID: '%s: Provided user ID is in an invalid format.',
INVALID_USER_PROFILE_SERVICE: '%s: Provided user profile service instance is in an invalid format: %s.',
NO_DATAFILE_SPECIFIED: '%s: No datafile specified. Cannot start optimizely.',
NO_JSON_PROVIDED: '%s: No JSON object to validate against schema.',
NO_VARIATION_FOR_EXPERIMENT_KEY: '%s: No variation key %s defined in datafile for experiment %s.',
UNDEFINED_ATTRIBUTE: '%s: Provided attribute: %s has an undefined value.',
UNRECOGNIZED_ATTRIBUTE: '%s: Unrecognized attribute %s provided. Pruning before sending event to Optimizely.',
UNABLE_TO_CAST_VALUE: '%s: Unable to cast value %s to type %s, returning null.',
USER_NOT_IN_FORCED_VARIATION: '%s: User %s is not in the forced variation map. Cannot remove their forced variation.',
USER_PROFILE_LOOKUP_ERROR: '%s: Error while looking up user profile for user ID "%s": %s.',
USER_PROFILE_SAVE_ERROR: '%s: Error while saving user profile for user ID "%s": %s.',
VARIABLE_KEY_NOT_IN_DATAFILE: '%s: Variable with key "%s" associated with feature with key "%s" is not in datafile.',
VARIATION_ID_NOT_IN_DATAFILE: '%s: No variation ID %s defined in datafile for experiment %s.',
VARIATION_ID_NOT_IN_DATAFILE_NO_EXPERIMENT: '%s: Variation ID %s is not in the datafile.',
INVALID_INPUT_FORMAT: '%s: Provided %s is in an invalid format.',
INVALID_DATAFILE_VERSION: '%s: This version of the JavaScript SDK does not support the given datafile version: %s',
INVALID_VARIATION_KEY: '%s: Provided variation key is in an invalid format.',
};
var LOG_MESSAGES = {
ACTIVATE_USER: '%s: Activating user %s in experiment %s.',
DISPATCH_CONVERSION_EVENT: '%s: Dispatching conversion event to URL %s with params %s.',
DISPATCH_IMPRESSION_EVENT: '%s: Dispatching impression event to URL %s with params %s.',
DEPRECATED_EVENT_VALUE: '%s: Event value is deprecated in %s call.',
EVENT_KEY_NOT_FOUND: '%s: Event key %s is not in datafile.',
EXPERIMENT_NOT_RUNNING: '%s: Experiment %s is not running.',
FEATURE_ENABLED_FOR_USER: '%s: Feature %s is enabled for user %s.',
FEATURE_NOT_ENABLED_FOR_USER: '%s: Feature %s is not enabled for user %s.',
FEATURE_HAS_NO_EXPERIMENTS: '%s: Feature %s is not attached to any experiments.',
FAILED_TO_PARSE_VALUE: '%s: Failed to parse event value "%s" from event tags.',
FAILED_TO_PARSE_REVENUE: '%s: Failed to parse revenue value "%s" from event tags.',
FORCED_BUCKETING_FAILED: '%s: Variation key %s is not in datafile. Not activating user %s.',
INVALID_OBJECT: '%s: Optimizely object is not valid. Failing %s.',
INVALID_CLIENT_ENGINE: '%s: Invalid client engine passed: %s. Defaulting to node-sdk.',
INVALID_VARIATION_ID: '%s: Bucketed into an invalid variation ID. Returning null.',
NOTIFICATION_LISTENER_EXCEPTION: '%s: Notification listener for (%s) threw exception: %s',
NO_ROLLOUT_EXISTS: '%s: There is no rollout of feature %s.',
NOT_ACTIVATING_USER: '%s: Not activating user %s for experiment %s.',
NOT_TRACKING_USER: '%s: Not tracking user %s.',
PARSED_REVENUE_VALUE: '%s: Parsed revenue value "%s" from event tags.',
PARSED_NUMERIC_VALUE: '%s: Parsed event value "%s" from event tags.',
RETURNING_STORED_VARIATION: '%s: Returning previously activated variation "%s" of experiment "%s" for user "%s" from user profile.',
ROLLOUT_HAS_NO_EXPERIMENTS: '%s: Rollout of feature %s has no experiments',
SAVED_VARIATION: '%s: Saved variation "%s" of experiment "%s" for user "%s".',
SAVED_VARIATION_NOT_FOUND: '%s: User %s was previously bucketed into variation with ID %s for experiment %s, but no matching variation was found.',
SHOULD_NOT_DISPATCH_ACTIVATE: '%s: Experiment %s is not in "Running" state. Not activating user.',
SKIPPING_JSON_VALIDATION: '%s: Skipping JSON schema validation.',
TRACK_EVENT: '%s: Tracking event %s for user %s.',
USER_ASSIGNED_TO_EXPERIMENT_BUCKET: '%s: Assigned bucket %s to user with bucketing ID %s.',
USER_BUCKETED_INTO_EXPERIMENT_IN_GROUP: '%s: User %s is in experiment %s of group %s.',
USER_BUCKETED_INTO_TARGETING_RULE: '%s: User %s bucketed into targeting rule %s.',
USER_IN_FEATURE_EXPERIMENT: '%s: User %s is in variation %s of experiment %s on the feature %s.',
USER_IN_ROLLOUT: '%s: User %s is in rollout of feature %s.',
USER_BUCKETED_INTO_EVERYONE_TARGETING_RULE: '%s: User %s bucketed into everyone targeting rule.',
USER_NOT_BUCKETED_INTO_EVERYONE_TARGETING_RULE: '%s: User %s not bucketed into everyone targeting rule due to traffic allocation.',
USER_NOT_BUCKETED_INTO_EXPERIMENT_IN_GROUP: '%s: User %s is not in experiment %s of group %s.',
USER_NOT_BUCKETED_INTO_ANY_EXPERIMENT_IN_GROUP: '%s: User %s is not in any experiment of group %s.',
USER_NOT_BUCKETED_INTO_TARGETING_RULE: '%s User %s not bucketed into targeting rule %s due to traffic allocation. Trying everyone rule.',
USER_NOT_IN_FEATURE_EXPERIMENT: '%s: User %s is not in any experiment on the feature %s.',
USER_NOT_IN_ROLLOUT: '%s: User %s is not in rollout of feature %s.',
USER_FORCED_IN_VARIATION: '%s: User %s is forced in variation %s.',
USER_MAPPED_TO_FORCED_VARIATION: '%s: Set variation %s for experiment %s and user %s in the forced variation map.',
USER_DOESNT_MEET_CONDITIONS_FOR_TARGETING_RULE: '%s: User %s does not meet conditions for targeting rule %s.',
USER_MEETS_CONDITIONS_FOR_TARGETING_RULE: '%s: User %s meets conditions for targeting rule %s.',
USER_HAS_VARIATION: '%s: User %s is in variation %s of experiment %s.',
USER_HAS_FORCED_VARIATION: '%s: Variation %s is mapped to experiment %s and user %s in the forced variation map.',
USER_HAS_NO_VARIATION: '%s: User %s is in no variation of experiment %s.',
USER_HAS_NO_FORCED_VARIATION: '%s: User %s is not in the forced variation map.',
USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT: '%s: No experiment %s mapped to user %s in the forced variation map.',
USER_NOT_IN_ANY_EXPERIMENT: '%s: User %s is not in any experiment of group %s.',
USER_NOT_IN_EXPERIMENT: '%s: User %s does not meet conditions to be in experiment %s.',
USER_RECEIVED_DEFAULT_VARIABLE_VALUE: '%s: User "%s" is not in any variation or rollout rule. Returning default value for variable "%s" of feature flag "%s".',
FEATURE_NOT_ENABLED_RETURN_DEFAULT_VARIABLE_VALUE: '%s: Feature "%s" is not enabled for user %s. Returning the default variable value "%s".',
VARIABLE_NOT_USED_RETURN_DEFAULT_VARIABLE_VALUE: '%s: Variable "%s" is not used in variation "%s". Returning default value.',
USER_RECEIVED_VARIABLE_VALUE: '%s: Got variable value "%s" for variable "%s" of feature flag "%s"',
VALID_DATAFILE: '%s: Datafile is valid.',
VALID_USER_PROFILE_SERVICE: '%s: Valid user profile service provided.',
VARIATION_REMOVED_FOR_USER: '%s: Variation mapped to experiment %s has been removed for user %s.',
VARIABLE_REQUESTED_WITH_WRONG_TYPE: '%s: Requested variable type "%s", but variable is of type "%s". Use correct API to retrieve value. Returning None.',
VALID_BUCKETING_ID: '%s: BucketingId is valid: "%s"',
BUCKETING_ID_NOT_STRING: '%s: BucketingID attribute is not a string. Defaulted to userId',
EVALUATING_AUDIENCE: '%s: Starting to evaluate audience "%s" with conditions: %s.',
EVALUATING_AUDIENCES_COMBINED: '%s: Evaluating audiences for %s "%s": %s.',
AUDIENCE_EVALUATION_RESULT: '%s: Audience "%s" evaluated to %s.',
AUDIENCE_EVALUATION_RESULT_COMBINED: '%s: Audiences for %s %s collectively evaluated to %s.',
MISSING_ATTRIBUTE_VALUE: '%s: Audience condition %s evaluated to UNKNOWN because no value was passed for user attribute "%s".',
UNEXPECTED_CONDITION_VALUE: '%s: Audience condition %s evaluated to UNKNOWN because the condition value is not supported.',
UNEXPECTED_TYPE: '%s: Audience condition %s evaluated to UNKNOWN because a value of type "%s" was passed for user attribute "%s".',
UNEXPECTED_TYPE_NULL: '%s: Audience condition %s evaluated to UNKNOWN because a null value was passed for user attribute "%s".',
UNKNOWN_CONDITION_TYPE: '%s: Audience condition %s has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.',
UNKNOWN_MATCH_TYPE: '%s: Audience condition %s uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK.',
UPDATED_OPTIMIZELY_CONFIG: '%s: Updated Optimizely config to revision %s (project id %s)',
OUT_OF_BOUNDS: '%s: Audience condition %s evaluated to UNKNOWN because the number value for user attribute "%s" is not in the range [-2^53, +2^53].',
UNABLE_TO_ATTACH_UNLOAD: '%s: unable to bind optimizely.close() to page unload event: "%s"',
};
var RESERVED_EVENT_KEYWORDS = {
REVENUE: 'revenue',
VALUE: 'value',
};
var CONTROL_ATTRIBUTES = {
BOT_FILTERING: '$opt_bot_filtering',
BUCKETING_ID: '$opt_bucketing_id',
STICKY_BUCKETING_KEY: '$opt_experiment_bucket_map',
USER_AGENT: '$opt_user_agent',
};
var JAVASCRIPT_CLIENT_ENGINE = 'javascript-sdk';
var NODE_CLIENT_ENGINE = 'node-sdk';
var REACT_CLIENT_ENGINE = 'react-sdk';
var NODE_CLIENT_VERSION = '4.3.4';
var VALID_CLIENT_ENGINES = [
NODE_CLIENT_ENGINE,
REACT_CLIENT_ENGINE,
JAVASCRIPT_CLIENT_ENGINE,
];
var NOTIFICATION_TYPES = NOTIFICATION_TYPES$1;
var DECISION_NOTIFICATION_TYPES = {
AB_TEST: 'ab-test',
FEATURE: 'feature',
FEATURE_TEST: 'feature-test',
FEATURE_VARIABLE: 'feature-variable',
ALL_FEATURE_VARIABLES: 'all-feature-variables',
};
/*
* Represents the source of a decision for feature management. When a feature
* is accessed through isFeatureEnabled or getVariableValue APIs, the decision
* source is used to decide whether to dispatch an impression event to
* Optimizely.
*/
var DECISION_SOURCES = {
FEATURE_TEST: 'feature-test',
ROLLOUT: 'rollout',
};
var AUDIENCE_EVALUATION_TYPES = {
RULE: 'rule',
EXPERIMENT: 'experiment',
};
/*
* Possible types of variables attached to features
*/
var FEATURE_VARIABLE_TYPES = {
BOOLEAN: 'boolean',
DOUBLE: 'double',
INTEGER: 'integer',
STRING: 'string',
JSON: 'json',
};
/*
* Supported datafile versions
*/
var DATAFILE_VERSIONS = {
V2: '2',
V3: '3',
V4: '4',
};
var enums = /*#__PURE__*/Object.freeze({
__proto__: null,
LOG_LEVEL: LOG_LEVEL,
ERROR_MESSAGES: ERROR_MESSAGES,
LOG_MESSAGES: LOG_MESSAGES,
RESERVED_EVENT_KEYWORDS: RESERVED_EVENT_KEYWORDS,
CONTROL_ATTRIBUTES: CONTROL_ATTRIBUTES,
JAVASCRIPT_CLIENT_ENGINE: JAVASCRIPT_CLIENT_ENGINE,
NODE_CLIENT_ENGINE: NODE_CLIENT_ENGINE,
REACT_CLIENT_ENGINE: REACT_CLIENT_ENGINE,
NODE_CLIENT_VERSION: NODE_CLIENT_VERSION,
VALID_CLIENT_ENGINES: VALID_CLIENT_ENGINES,
NOTIFICATION_TYPES: NOTIFICATION_TYPES,
DECISION_NOTIFICATION_TYPES: DECISION_NOTIFICATION_TYPES,
DECISION_SOURCES: DECISION_SOURCES,
AUDIENCE_EVALUATION_TYPES: AUDIENCE_EVALUATION_TYPES,
FEATURE_VARIABLE_TYPES: FEATURE_VARIABLE_TYPES,
DATAFILE_VERSIONS: DATAFILE_VERSIONS
});
/**
* Copyright 2016, 2018-2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var MODULE_NAME = 'CONFIG_VALIDATOR';
var SUPPORTED_VERSIONS = [DATAFILE_VERSIONS.V2, DATAFILE_VERSIONS.V3, DATAFILE_VERSIONS.V4];
/**
* Validates the given config options
* @param {unknown} config
* @param {object} config.errorHandler
* @param {object} config.eventDispatcher
* @param {object} config.logger
* @return {boolean} true if the config options are valid
* @throws If any of the config options are not valid
*/
var validate = function (config) {
if (typeof config === 'object' && config !== null) {
if (config['errorHandler'] && typeof config['errorHandler'].handleError !== 'function') {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_ERROR_HANDLER, MODULE_NAME));
}
if (config['eventDispatcher'] && typeof config['eventDispatcher'].dispatchEvent !== 'function') {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_EVENT_DISPATCHER, MODULE_NAME));
}
if (config['logger'] && typeof config['logger'].log !== 'function') {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_LOGGER, MODULE_NAME));
}
return true;
}
throw new Error(sprintf(ERROR_MESSAGES.INVALID_CONFIG, MODULE_NAME));
};
/**
* Validates the datafile
* @param {Object|string} datafile
* @return {Object} The datafile object if the datafile is valid
* @throws If the datafile is not valid for any of the following reasons:
- The datafile string is undefined
- The datafile string cannot be parsed as a JSON object
- The datafile version is not supported
*/
// eslint-disable-next-line
var validateDatafile = function (datafile) {
if (!datafile) {
throw new Error(sprintf(ERROR_MESSAGES.NO_DATAFILE_SPECIFIED, MODULE_NAME));
}
if (typeof datafile === 'string') {
// Attempt to parse the datafile string
try {
datafile = JSON.parse(datafile);
}
catch (ex) {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_DATAFILE_MALFORMED, MODULE_NAME));
}
}
if (typeof datafile === 'object' && !Array.isArray(datafile) && datafile !== null) {
if (SUPPORTED_VERSIONS.indexOf(datafile['version']) === -1) {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_DATAFILE_VERSION, MODULE_NAME, datafile['version']));
}
}
return datafile;
};
/**
* Provides utility methods for validating that the configuration options are valid
*/
var configValidator = {
validate: validate,
validateDatafile: validateDatafile,
};
/**
* Copyright 2016, 2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Default error handler implementation
*/
var handleError = function () {
// no-op
};
var defaultErrorHandler = {
handleError: handleError,
};
/**
* Copyright 2016-2017, 2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var POST_METHOD = 'POST';
var GET_METHOD = 'GET';
var READYSTATE_COMPLETE = 4;
/**
* Sample event dispatcher implementation for tracking impression and conversions
* Users of the SDK can provide their own implementation
* @param {Object} eventObj
* @param {Function} callback
*/
var dispatchEvent = function (eventObj, callback) {
var url = eventObj.url;
var params = eventObj.params;
var req;
if (eventObj.httpVerb === POST_METHOD) {
req = new XMLHttpRequest();
req.open(POST_METHOD, url, true);
req.setRequestHeader('Content-Type', 'application/json');
req.onreadystatechange = function () {
if (req.readyState === READYSTATE_COMPLETE && callback && typeof callback === 'function') {
try {
callback({ statusCode: req.status });
}
catch (e) {
// TODO: Log this somehow (consider adding a logger to the EventDispatcher interface)
}
}
};
req.send(JSON.stringify(params));
}
else {
// add param for cors headers to be sent by the log endpoint
url += '?wxhr=true';
if (params) {
url += '&' + toQueryString(params);
}
req = new XMLHttpRequest();
req.open(GET_METHOD, url, true);
req.onreadystatechange = function () {
if (req.readyState === READYSTATE_COMPLETE && callback && typeof callback === 'function') {
try {
callback();
}
catch (e) {
// TODO: Log this somehow (consider adding a logger to the EventDispatcher interface)
}
}
};
req.send();
}
};
var toQueryString = function (obj) {
return Object.keys(obj)
.map(function (k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]);
})
.join('&');
};
var defaultEventDispatcher = {
dispatchEvent: dispatchEvent,
};
/**
* Copyright 2016-2017, 2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
function NoOpLogger() { }
NoOpLogger.prototype.log = function () { };
var createLogger = function (opts) {
return new ConsoleLogHandler(opts);
};
var createNoOpLogger = function () {
return new NoOpLogger();
};
var loggerPlugin = {
createLogger: createLogger,
createNoOpLogger: createNoOpLogger,
};
/**
* Copyright 2016-2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var EXPERIMENT_RUNNING_STATUS = 'Running';
var RESERVED_ATTRIBUTE_PREFIX = '$opt_';
var MODULE_NAME$1 = 'PROJECT_CONFIG';
function createMutationSafeDatafileCopy(datafile) {
var datafileCopy = assign({}, datafile);
datafileCopy.audiences = (datafile.audiences || []).map(function (audience) {
return assign({}, audience);
});
datafileCopy.experiments = (datafile.experiments || []).map(function (experiment) {
return assign({}, experiment);
});
datafileCopy.featureFlags = (datafile.featureFlags || []).map(function (featureFlag) {
return assign({}, featureFlag);
});
datafileCopy.groups = (datafile.groups || []).map(function (group) {
var groupCopy = assign({}, group);
groupCopy.experiments = (group.experiments || []).map(function (experiment) {
return assign({}, experiment);
});
return groupCopy;
});
datafileCopy.rollouts = (datafile.rollouts || []).map(function (rollout) {
var rolloutCopy = assign({}, rollout);
rolloutCopy.experiments = (rollout.experiments || []).map(function (experiment) {
return assign({}, experiment);
});
return rolloutCopy;
});
return datafileCopy;
}
/**
* Creates projectConfig object to be used for quick project property lookup
* @param {Object} datafileObj JSON datafile representing the project
* @param {string=} datafileStr JSON string representation of the datafile
* @return {Object} Object representing project configuration
*/
var createProjectConfig = function (datafileObj, datafileStr) {
if (datafileStr === void 0) { datafileStr = null; }
var projectConfig = createMutationSafeDatafileCopy(datafileObj);
projectConfig.__datafileStr = datafileStr === null ? JSON.stringify(datafileObj) : datafileStr;
/*
* Conditions of audiences in projectConfig.typedAudiences are not
* expected to be string-encoded as they are here in projectConfig.audiences.
*/
(projectConfig.audiences || []).forEach(function (audience) {
audience.conditions = JSON.parse(audience.conditions);
});
projectConfig.audiencesById = keyBy(projectConfig.audiences, 'id');
assign(projectConfig.audiencesById, keyBy(projectConfig.typedAudiences, 'id'));
projectConfig.attributeKeyMap = keyBy(projectConfig.attributes, 'key');
projectConfig.eventKeyMap = keyBy(projectConfig.events, 'key');
projectConfig.groupIdMap = keyBy(projectConfig.groups, 'id');
var experiments;
Object.keys(projectConfig.groupIdMap || {}).forEach(function (Id) {
experiments = projectConfig.groupIdMap[Id].experiments;
(experiments || []).forEach(function (experiment) {
projectConfig.experiments.push(assign(experiment, { groupId: Id }));
});
});
projectConfig.rolloutIdMap = keyBy(projectConfig.rollouts || [], 'id');
objectValues(projectConfig.rolloutIdMap || {}).forEach(function (rollout) {
(rollout.experiments || []).forEach(function (experiment) {
projectConfig.experiments.push(experiment);
// Creates { <variationKey>: <variation> } map inside of the experiment
experiment.variationKeyMap = keyBy(experiment.variations, 'key');
});
});
projectConfig.experimentKeyMap = keyBy(projectConfig.experiments, 'key');
projectConfig.experimentIdMap = keyBy(projectConfig.experiments, 'id');
projectConfig.variationIdMap = {};
projectConfig.variationVariableUsageMap = {};
(projectConfig.experiments || []).forEach(function (experiment) {
// Creates { <variationKey>: <variation> } map inside of the experiment
experiment.variationKeyMap = keyBy(experiment.variations, 'key');
// Creates { <variationId>: { key: <variationKey>, id: <variationId> } } mapping for quick lookup
assign(projectConfig.variationIdMap, keyBy(experiment.variations, 'id'));
objectValues(experiment.variationKeyMap || {}).forEach(function (variation) {
if (variation.variables) {
projectConfig.variationVariableUsageMap[variation.id] = keyBy(variation.variables, 'id');
}
});
});
// Object containing experiment Ids that exist in any feature
// for checking that experiment is a feature experiment or not.
projectConfig.experimentFeatureMap = {};
projectConfig.featureKeyMap = keyBy(projectConfig.featureFlags || [], 'key');
objectValues(projectConfig.featureKeyMap || {}).forEach(function (feature) {
// Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
// Converting it to a first-class json type while creating Project Config
feature.variables.forEach(function (variable) {
if (variable.type === FEATURE_VARIABLE_TYPES.STRING && variable.subType === FEATURE_VARIABLE_TYPES.JSON) {
variable.type = FEATURE_VARIABLE_TYPES.JSON;
delete variable.subType;
}
});
feature.variableKeyMap = keyBy(feature.variables, 'key');
(feature.experimentIds || []).forEach(function (experimentId) {
// Add this experiment in experiment-feature map.
if (projectConfig.experimentFeatureMap[experimentId]) {
projectConfig.experimentFeatureMap[experimentId].push(feature.id);
}
else {
projectConfig.experimentFeatureMap[experimentId] = [feature.id];
}
var experimentInFeature = projectConfig.experimentIdMap[experimentId];
// Experiments in feature can only belong to one mutex group.
if (experimentInFeature.groupId && !feature.groupId) {
feature.groupId = experimentInFeature.groupId;
}
});
});
return projectConfig;
};
/**
* Get experiment ID for the provided experiment key
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentKey Experiment key for which ID is to be determined
* @return {string} Experiment ID corresponding to the provided experiment key
* @throws If experiment key is not in datafile
*/
var getExperimentId = function (projectConfig, experimentKey) {
var experiment = projectConfig.experimentKeyMap[experimentKey];
if (!experiment) {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME$1, experimentKey));
}
return experiment.id;
};
/**
* Get layer ID for the provided experiment key
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentId Experiment ID for which layer ID is to be determined
* @return {string} Layer ID corresponding to the provided experiment key
* @throws If experiment key is not in datafile
*/
var getLayerId = function (projectConfig, experimentId) {
var experiment = projectConfig.experimentIdMap[experimentId];
if (!experiment) {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, MODULE_NAME$1, experimentId));
}
return experiment.layerId;
};
/**
* Get attribute ID for the provided attribute key
* @param {Object} projectConfig Object representing project configuration
* @param {string} attributeKey Attribute key for which ID is to be determined
* @param {Object} logger
* @return {string|null} Attribute ID corresponding to the provided attribute key. Attribute key if it is a reserved attribute.
*/
var getAttributeId = function (projectConfig, attributeKey, logger) {
var attribute = projectConfig.attributeKeyMap[attributeKey];
var hasReservedPrefix = attributeKey.indexOf(RESERVED_ATTRIBUTE_PREFIX) === 0;
if (attribute) {
if (hasReservedPrefix) {
logger.log(LOG_LEVEL.WARN, sprintf('Attribute %s unexpectedly has reserved prefix %s; using attribute ID instead of reserved attribute name.', attributeKey, RESERVED_ATTRIBUTE_PREFIX));
}
return attribute.id;
}
else if (hasReservedPrefix) {
return attributeKey;
}
logger.log(LOG_LEVEL.DEBUG, sprintf(ERROR_MESSAGES.UNRECOGNIZED_ATTRIBUTE, MODULE_NAME$1, attributeKey));
return null;
};
/**
* Get event ID for the provided
* @param {Object} projectConfig Object representing project configuration
* @param {string} eventKey Event key for which ID is to be determined
* @return {string|null} Event ID corresponding to the provided event key
*/
var getEventId = function (projectConfig, eventKey) {
var event = projectConfig.eventKeyMap[eventKey];
if (event) {
return event.id;
}
return null;
};
/**
* Get experiment status for the provided experiment key
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentKey Experiment key for which status is to be determined
* @return {string} Experiment status corresponding to the provided experiment key
* @throws If experiment key is not in datafile
*/
var getExperimentStatus = function (projectConfig, experimentKey) {
var experiment = projectConfig.experimentKeyMap[experimentKey];
if (!experiment) {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME$1, experimentKey));
}
return experiment.status;
};
/**
* Returns whether experiment has a status of 'Running'
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentKey Experiment key for which status is to be compared with 'Running'
* @return {Boolean} true if experiment status is set to 'Running', false otherwise
*/
var isActive = function (projectConfig, experimentKey) {
return getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS;
};
/**
* Determine for given experiment if event is running, which determines whether should be dispatched or not
*/
var isRunning = function (projectConfig, experimentKey) {
return getExperimentStatus(projectConfig, experimentKey) === EXPERIMENT_RUNNING_STATUS;
};
/**
* Get audience conditions for the experiment
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentKey Experiment key for which audience conditions are to be determined
* @return {Array} Audience conditions for the experiment - can be an array of audience IDs, or a
* nested array of conditions
* Examples: ["5", "6"], ["and", ["or", "1", "2"], "3"]
* @throws If experiment key is not in datafile
*/
var getExperimentAudienceConditions = function (projectConfig, experimentKey) {
var experiment = projectConfig.experimentKeyMap[experimentKey];
if (!experiment) {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME$1, experimentKey));
}
return experiment.audienceConditions || experiment.audienceIds;
};
/**
* Get variation key given experiment key and variation ID
* @param {Object} projectConfig Object representing project configuration
* @param {string} variationId ID of the variation
* @return {string} Variation key or null if the variation ID is not found
*/
var getVariationKeyFromId = function (projectConfig, variationId) {
if (projectConfig.variationIdMap.hasOwnProperty(variationId)) {
return projectConfig.variationIdMap[variationId].key;
}
return null;
};
/**
* Get the variation ID given the experiment key and variation key
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentKey Key of the experiment the variation belongs to
* @param {string} variationKey The variation key
* @return {string} the variation ID
*/
var getVariationIdFromExperimentAndVariationKey = function (projectConfig, experimentKey, variationKey) {
var experiment = projectConfig.experimentKeyMap[experimentKey];
if (experiment.variationKeyMap.hasOwnProperty(variationKey)) {
return experiment.variationKeyMap[variationKey].id;
}
return null;
};
/**
* Get experiment from provided experiment key
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentKey Event key for which experiment IDs are to be retrieved
* @return {Object} experiment
* @throws If experiment key is not in datafile
*/
var getExperimentFromKey = function (projectConfig, experimentKey) {
if (projectConfig.experimentKeyMap.hasOwnProperty(experimentKey)) {
var experiment = projectConfig.experimentKeyMap[experimentKey];
if (experiment) {
return experiment;
}
}
throw new Error(sprintf(ERROR_MESSAGES.EXPERIMENT_KEY_NOT_IN_DATAFILE, MODULE_NAME$1, experimentKey));
};
/**
* Given an experiment key, returns the traffic allocation within that experiment
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentKey Key representing the experiment
* @return {Array<Object>} Traffic allocation for the experiment
* @throws If experiment key is not in datafile
*/
var getTrafficAllocation = function (projectConfig, experimentKey) {
var experiment = projectConfig.experimentKeyMap[experimentKey];
if (!experiment) {
throw new Error(sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_KEY, MODULE_NAME$1, experimentKey));
}
return experiment.trafficAllocation;
};
/**
* Get experiment from provided experiment id. Log an error if no experiment
* exists in the project config with the given ID.
* @param {Object} projectConfig Object representing project configuration
* @param {string} experimentId ID of desired experiment object
* @return {Object} Experiment object
*/
var getExperimentFromId = function (projectConfig, experimentId, logger) {
if (projectConfig.experimentIdMap.hasOwnProperty(experimentId)) {
var experiment = projectConfig.experimentIdMap[experimentId];
if (experiment) {
return experiment;
}
}
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.INVALID_EXPERIMENT_ID, MODULE_NAME$1, experimentId));
return null;
};
/**
* Get feature from provided feature key. Log an error if no feature exists in
* the project config with the given key.
* @param {Object} projectConfig
* @param {string} featureKey
* @param {Object} logger
* @return {Object|null} Feature object, or null if no feature with the given
* key exists
*/
var getFeatureFromKey = function (projectConfig, featureKey, logger) {
if (projectConfig.featureKeyMap.hasOwnProperty(featureKey)) {
var feature = projectConfig.featureKeyMap[featureKey];
if (feature) {
return feature;
}
}
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.FEATURE_NOT_IN_DATAFILE, MODULE_NAME$1, featureKey));
return null;
};
/**
* Get the variable with the given key associated with the feature with the
* given key. If the feature key or the variable key are invalid, log an error
* message.
* @param {Object} projectConfig
* @param {string} featureKey
* @param {string} variableKey
* @param {Object} logger
* @return {Object|null} Variable object, or null one or both of the given
* feature and variable keys are invalid
*/
var getVariableForFeature = function (projectConfig, featureKey, variableKey, logger) {
var feature = projectConfig.featureKeyMap[featureKey];
if (!feature) {
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.FEATURE_NOT_IN_DATAFILE, MODULE_NAME$1, featureKey));
return null;
}
var variable = feature.variableKeyMap[variableKey];
if (!variable) {
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.VARIABLE_KEY_NOT_IN_DATAFILE, MODULE_NAME$1, variableKey, featureKey));
return null;
}
return variable;
};
/**
* Get the value of the given variable for the given variation. If the given
* variable has no value for the given variation, return null. Log an error message if the variation is invalid. If the
* variable or variation are invalid, return null.
* @param {Object} projectConfig
* @param {Object} variable
* @param {Object} variation
* @param {Object} logger
* @return {string|null} The value of the given variable for the given
* variation, or null if the given variable has no value
* for the given variation or if the variation or variable are invalid
*/
var getVariableValueForVariation = function (projectConfig, variable, variation, logger) {
if (!variable || !variation) {
return null;
}
if (!projectConfig.variationVariableUsageMap.hasOwnProperty(variation.id)) {
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.VARIATION_ID_NOT_IN_DATAFILE_NO_EXPERIMENT, MODULE_NAME$1, variation.id));
return null;
}
var variableUsages = projectConfig.variationVariableUsageMap[variation.id];
var variableUsage = variableUsages[variable.id];
return variableUsage ? variableUsage.value : null;
};
/**
* Given a variable value in string form, try to cast it to the argument type.
* If the type cast succeeds, return the type casted value, otherwise log an
* error and return null.
* @param {string} variableValue Variable value in string form
* @param {string} variableType Type of the variable whose value was passed
* in the first argument. Must be one of
* FEATURE_VARIABLE_TYPES in
* lib/utils/enums/index.js. The return value's
* type is determined by this argument (boolean
* for BOOLEAN, number for INTEGER or DOUBLE,
* and string for STRING).
* @param {Object} logger Logger instance
* @returns {*} Variable value of the appropriate type, or
* null if the type cast failed
*/
var getTypeCastValue = function (variableValue, variableType, logger) {
var castValue;
switch (variableType) {
case FEATURE_VARIABLE_TYPES.BOOLEAN:
if (variableValue !== 'true' && variableValue !== 'false') {
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.UNABLE_TO_CAST_VALUE, MODULE_NAME$1, variableValue, variableType));
castValue = null;
}
else {
castValue = variableValue === 'true';
}
break;
case FEATURE_VARIABLE_TYPES.INTEGER:
castValue = parseInt(variableValue, 10);
if (isNaN(castValue)) {
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.UNABLE_TO_CAST_VALUE, MODULE_NAME$1, variableValue, variableType));
castValue = null;
}
break;
case FEATURE_VARIABLE_TYPES.DOUBLE:
castValue = parseFloat(variableValue);
if (isNaN(castValue)) {
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.UNABLE_TO_CAST_VALUE, MODULE_NAME$1, variableValue, variableType));
castValue = null;
}
break;
case FEATURE_VARIABLE_TYPES.JSON:
try {
castValue = JSON.parse(variableValue);
}
catch (e) {
logger.log(LOG_LEVEL.ERROR, sprintf(ERROR_MESSAGES.UNABLE_TO_CAST_VALUE, MODULE_NAME$1, variableValue, variableType));
castValue = null;
}
break;
default:
// type is STRING
castValue = variableValue;
break;
}
return castValue;
};
/**
* Returns an object containing all audiences in the project config. Keys are audience IDs
* and values are audience objects.
* @param {Object} projectConfig
* @returns {Object}
*/
var getAudiencesById = function (projectConfig) {
return projectConfig.audiencesById;
};
/**
* Returns true if an event with the given key exists in the datafile, and false otherwise
* @param {Object} projectConfig
* @param {string} eventKey
* @returns {boolean}
*/
var eventWithKeyExists = function (projectConfig, eventKey) {
return projectConfig.eventKeyMap.hasOwnProperty(eventKey);
};
/**
* Returns true if experiment belongs to any feature, false otherwise.
* @param {Object} projectConfig
* @param {string} experimentId
* @returns {boolean}
*/
var isFeatureExperiment = function (projectConfig, experimentId) {
return projectConfig.experimentFeatureMap.hasOwnProperty(experimentId);
};
/**
* Returns the JSON string representation of the datafile
* @param {Object} projectConfig
* @returns {string}
*/
var toDatafile = function (projectConfig) {
return projectConfig.__datafileStr;
};
/**
* @typedef {Object} TryCreatingProjectConfigResult
* @property {Object|null} configObj
* @property {Error|null} error
*/
/**
* Try to create a project config object from the given datafile and
* configuration properties.
* Returns an object with configObj and error properties.
* If successful, configObj is the project config object, and error is null.
* Otherwise, configObj is null and error is an error with more information.
* @param {Object} config
* @param {Object|string} config.datafile
* @param {Object} config.jsonSchemaValidator
* @param {Object} config.logger
* @returns {TryCreatingProjectConfigResult}
*/
var tryCreatingProjectConfig = function (config) {
var newDatafileObj;
try {
newDatafileObj = configValidator.validateDatafile(config.datafile);
}
catch (error) {
return { configObj: null, error: error };
}
if (config.jsonSchemaValidator) {
try {
config.jsonSchemaValidator.validate(newDatafileObj);
config.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.VALID_DATAFILE, MODULE_NAME$1));
}
catch (error) {
return { configObj: null, error: error };
}
}
else {
config.logger.log(LOG_LEVEL.INFO, sprintf(LOG_MESSAGES.SKIPPING_JSON_VALIDATION, MODULE_NAME$1));
}
var createProjectConfigArgs = [newDatafileObj];
if (typeof config.datafile === 'string') {
// Since config.datafile was validated above, we know that it is a valid JSON string
createProjectConfigArgs.push(config.datafile);
}
var newConfigObj = createProjectConfig.apply(void 0, createProjectConfigArgs);
return {
configObj: newConfigObj,
error: null,
};
};
var projectConfig = {
createProjectConfig: createProjectConfig,
getExperimentId: getExperimentId,
getLayerId: getLayerId,
getAttributeId: getAttributeId,
getEventId: getEventId,
getExperimentStatus: getExperimentStatus,
isActive: isActive,
isRunning: isRunning,
getExperimentAudienceConditions: getExperimentAudienceConditions,
getVariationKeyFromId: getVariationKeyFromId,
getVariationIdFromExperimentAndVariationKey: getVariationIdFromExperimentAndVariationKey,
getExperimentFromKey: getExperimentFromKey,
getTrafficAllocation: getTrafficAllocation,
getExperimentFromId: getExperimentFromId,
getFeatureFromKey: getFeatureFromKey,
getVariableForFeature: getVariableForFeature,
getVariableValueForVariation: getVariableValueForVariation,
getTypeCastValue: getTypeCastValue,
getAudiencesById: getAudiencesById,
eventWithKeyExists: eventWithKeyExists,
isFeatureExperiment: isFeatureExperiment,
toDatafile: toDatafile,
tryCreatingProjectConfig: tryCreatingProjectConfig,
};
/**
* Copyright 2019-2020, Optimizely
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Get Experiment Ids which are part of rollouts
function getRolloutExperimentIds(rollouts) {
return