UNPKG

@optimizely/optimizely-sdk

Version:
1,056 lines (1,047 loc) 227 kB
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