UNPKG

@optimizely/optimizely-sdk

Version:
1,057 lines (1,044 loc) 233 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 { LocalStoragePendingEventsDispatcher, LogTierV1EventProcessor } from '@optimizely/js-sdk-event-processor'; import { NOTIFICATION_TYPES as NOTIFICATION_TYPES$1, sprintf, generateUUID, keyBy as keyBy$1, objectValues } from '@optimizely/js-sdk-utils'; import { HttpPollingDatafileManager } from '@optimizely/js-sdk-datafile-manager'; import murmurhash from 'murmurhash'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __spreadArrays() { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; } /**************************************************************************** * 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.4.0'; 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', EXPERIMENT: 'experiment', }; 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, }; 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, __spreadArrays([target], 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'; } var fns = { assign: assign, currentTimestamp: currentTimestamp, isSafeInteger: isSafeInteger, keyBy: keyBy, uuid: generateUUID, isNumber: isNumber, }; /** * 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 = fns.assign({}, datafile); datafileCopy.audiences = (datafile.audiences || []).map(function (audience) { return fns.assign({}, audience); }); datafileCopy.experiments = (datafile.experiments || []).map(function (experiment) { return fns.assign({}, experiment); }); datafileCopy.featureFlags = (datafile.featureFlags || []).map(function (featureFlag) { return fns.assign({}, featureFlag); }); datafileCopy.groups = (datafile.groups || []).map(function (group) { var groupCopy = fns.assign({}, group); groupCopy.experiments = (group.experiments || []).map(function (experiment) { return fns.assign({}, experiment); }); return groupCopy; }); datafileCopy.rollouts = (datafile.rollouts || []).map(function (rollout) { var rolloutCopy = fns.assign({}, rollout); rolloutCopy.experiments = (rollout.experiments || []).map(function (experiment) { return fns.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 = fns.keyBy(projectConfig.audiences, 'id'); fns.assign(projectConfig.audiencesById, fns.keyBy(projectConfig.typedAudiences, 'id')); projectConfig.attributeKeyMap = fns.keyBy(projectConfig.attributes, 'key'); projectConfig.eventKeyMap = fns.keyBy(projectConfig.events, 'key'); projectConfig.groupIdMap = fns.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(fns.assign(experiment, { groupId: Id })); }); }); projectConfig.rolloutIdMap = fns.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 = fns.keyBy(experiment.variations, 'key'); }); }); projectConfig.experimentKeyMap = fns.keyBy(projectConfig.experiments, 'key'); projectConfig.experimentIdMap = fns.keyBy(projectConfig.experiments, 'id'); projectConfig.variationIdMap = {}; projectConfig.variationVariableUsageMap = {}; (projectConfig.experiments || []).forEach(function (experiment) { // Creates { <variationKey>: <variation> } map inside of the experiment experiment.variationKeyMap = fns.keyBy(experiment.variations, 'key'); // Creates { <variationId>: { key: <variationKey>, id: <variationId> } } mapping for quick lookup fns.assign(projectConfig.variationIdMap, fns.keyBy(experiment.variations, 'id')); objectValues(experiment.variationKeyMap || {}).forEach(function (variation) { if (variation.variables) { projectConfig.variationVariableUsageMap[variation.id] = fns.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 = fns.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 = fns.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|null} 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, }; }; /** * Get the send flag decisions value * @param {ProjectConfig} projectConfig * @return {boolean} A boolean value that indicates if we should send flag decisions */ var getSendFlagDecisionsValue = function (projectConfig) { return !!projectConfig.sendFlagDecisions; }; var projectConfig = { createProjectConfig: createProjectConfig, getExperimentId: getExperimentId, getLayerId: getLayerId, getAttributeId: getAttributeId, getEventId: getEventId, getExperimentStatus: getExperimentStatus, isActive: isActive, isRunni