UNPKG

@optimizely/optimizely-sdk

Version:

JavaScript SDK for Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts

1,140 lines (1,121 loc) 463 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.optimizelySdk = {})); })(this, (function (exports) { 'use strict'; /** * Convert array of 16 byte values to UUID string format of the form: * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX */ var byteToHex = []; for (var i = 0; i < 256; ++i) { byteToHex.push((i + 0x100).toString(16).slice(1)); } function unsafeStringify(arr, offset = 0) { // Note: Be careful editing this code! It's been tuned for performance // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 // // Note to future-self: No, you can't remove the `toLowerCase()` call. // REF: https://github.com/uuidjs/uuid/pull/677#issuecomment-1757351351 return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } // Unique ID creation requires a high quality random # generator. In the browser we therefore // require the crypto API and do not support built-in fallback to lower quality random number // generators (like Math.random()). var getRandomValues; var rnds8 = new Uint8Array(16); function rng() { // lazy load so that environments that need to polyfill have a chance to do so if (!getRandomValues) { // getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto); if (!getRandomValues) { throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported'); } } return getRandomValues(rnds8); } var randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto); var native = { randomUUID }; function v4(options, buf, offset) { if (native.randomUUID && true && !options) { return native.randomUUID(); } options = options || {}; var rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` rnds[6] = rnds[6] & 0x0f | 0x40; rnds[8] = rnds[8] & 0x3f | 0x80; return unsafeStringify(rnds); } /** * Copyright 2017, 2019-2020, 2022-2023, 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 * * https://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. */ const MAX_SAFE_INTEGER_LIMIT = Math.pow(2, 53); 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 {}; const base = {}; assignBy(arr, key, base); return base; } function assignBy(arr, key, base) { if (!arr) return; arr.forEach((e) => { base[e[key]] = e; }); } function isNumber$1(value) { return typeof value === 'number'; } function uuid() { return v4(); } function getTimestamp() { return new Date().getTime(); } function groupBy(arr, grouperFn) { const grouper = {}; arr.forEach(item => { const key = grouperFn(item); grouper[key] = grouper[key] || []; grouper[key].push(item); }); return objectValues(grouper); } function objectValues(obj) { return Object.keys(obj).map(key => obj[key]); } function objectEntries(obj) { return Object.keys(obj).map(key => [key, obj[key]]); } function find(arr, cond) { let found; for (const item of arr) { if (cond(item)) { found = item; break; } } return found; } // TODO[OASIS-6649]: Don't use any type // eslint-disable-next-line @typescript-eslint/no-explicit-any function sprintf(format, ...args) { let i = 0; return format.replace(/%s/g, function () { const arg = args[i++]; const type = typeof arg; if (type === 'function') { return arg(); } else if (type === 'string') { return arg; } else { return String(arg); } }); } /** * Checks two string arrays for equality. * @param arrayA First Array to be compared against. * @param arrayB Second Array to be compared against. * @returns {boolean} True if both arrays are equal, otherwise returns false. */ function checkArrayEquality(arrayA, arrayB) { return arrayA.length === arrayB.length && arrayA.every((item, index) => item === arrayB[index]); } var fns = { checkArrayEquality, currentTimestamp, isSafeInteger, keyBy, uuid, isNumber: isNumber$1, getTimestamp, groupBy, objectValues, objectEntries, find, sprintf, }; class OptimizelyError extends Error { constructor(baseMessage, ...params) { super(); this.resolved = false; this.name = 'OptimizelyError'; this.baseMessage = baseMessage; this.params = params; // this is needed cause instanceof doesn't work for // custom Errors when TS is compiled to es5 Object.setPrototypeOf(this, OptimizelyError.prototype); } setMessage(resolver) { if (!this.resolved) { this.message = sprintf(resolver.resolve(this.baseMessage), ...this.params); this.resolved = true; } } } const BUCKETING_ID_NOT_STRING = '0'; const CONDITION_EVALUATOR_ERROR = '2'; const DATAFILE_FETCH_REQUEST_FAILED = '3'; const ERROR_FETCHING_DATAFILE = '4'; const EVENT_ACTION_INVALID = '5'; const EVENT_DATA_INVALID = '6'; const EVENT_KEY_NOT_FOUND = '7'; const EVENT_STORE_FULL = '8'; const EXPERIMENT_KEY_NOT_IN_DATAFILE = '9'; const FAILED_TO_DISPATCH_EVENTS = '10'; const FAILED_TO_SEND_ODP_EVENTS = '11'; const FEATURE_NOT_IN_DATAFILE = '12'; const INVALID_ATTRIBUTES$1 = '13'; const INVALID_BUCKETING_ID = '14'; const INVALID_DATAFILE = '17'; const INVALID_DATAFILE_MALFORMED = '18'; const INVALID_DATAFILE_VERSION = '19'; const INVALID_EVENT_TAGS = '20'; const INVALID_EXPERIMENT_ID = '21'; const INVALID_EXPERIMENT_KEY = '22'; const INVALID_GROUP_ID = '23'; const INVALID_INPUT_FORMAT = '24'; const INVALID_JSON = '25'; const INVALID_USER_ID = '26'; const INVALID_USER_PROFILE_SERVICE = '27'; const INVALID_VARIATION_KEY = '28'; const MISSING_INTEGRATION_KEY = '29'; const NOTIFICATION_LISTENER_EXCEPTION = '30'; const NOT_TRACKING_USER = '31'; const NO_DATAFILE_SPECIFIED = '32'; const NO_EVENT_PROCESSOR = '33'; const NO_JSON_PROVIDED = '34'; const NO_PROJECT_CONFIG_FAILURE = '35'; const NO_VARIATION_FOR_EXPERIMENT_KEY = '37'; const ODP_CONFIG_NOT_AVAILABLE = '38'; const ODP_EVENTS_SHOULD_HAVE_ATLEAST_ONE_KEY_VALUE = '39'; const ODP_EVENT_FAILED = '40'; const ODP_MANAGER_MISSING = '42'; const ODP_NOT_INTEGRATED = '43'; const ONLY_POST_REQUESTS_ARE_SUPPORTED = '44'; const OUT_OF_BOUNDS = '45'; const REQUEST_ERROR = '47'; const REQUEST_TIMEOUT = '48'; const RETRY_CANCELLED = '49'; const SEND_BEACON_FAILED = '50'; const SERVICE_NOT_RUNNING = '51'; const UNABLE_TO_CAST_VALUE = '53'; const UNABLE_TO_GET_VUID_VUID_MANAGER_NOT_AVAILABLE = '54'; const UNABLE_TO_PARSE_AND_SKIPPED_HEADER = '55'; const UNDEFINED_ATTRIBUTE = '56'; const UNEXPECTED_CONDITION_VALUE = '57'; const UNEXPECTED_RESERVED_ATTRIBUTE_PREFIX = '58'; const UNEXPECTED_TYPE = '59'; const UNKNOWN_CONDITION_TYPE = '60'; const UNKNOWN_MATCH_TYPE = '61'; const UNRECOGNIZED_ATTRIBUTE = '62'; const UNRECOGNIZED_DECIDE_OPTION = '63'; const USER_NOT_IN_FORCED_VARIATION = '65'; const USER_PROFILE_LOOKUP_ERROR = '66'; const USER_PROFILE_SAVE_ERROR = '67'; const VARIABLE_KEY_NOT_IN_DATAFILE = '68'; const VARIABLE_REQUESTED_WITH_WRONG_TYPE = '69'; const VARIATION_ID_NOT_IN_DATAFILE = '70'; const messages$1 = [ "BucketingID attribute is not a string. Defaulted to userId", "CMAB decision fetch failed with status: %s", "Error evaluating audience condition of type %s: %s", "Datafile fetch request failed with status: %s", "Error fetching datafile: %s", "Event action invalid.", "Event data invalid.", "Event key %s is not in datafile.", "Event store is full. Not saving event with id %d.", "Experiment key %s is not in datafile.", "Failed to dispatch events, status: %s", "failed to send odp events", "Feature key %s is not in datafile.", "Provided attributes are in an invalid format.", "Unable to generate hash for bucketing ID %s: %s", "Invalid CMAB fetch response", "Provided Optimizely config is in an invalid format.", "Datafile is invalid - property %s: %s", "Datafile is invalid because it is malformed.", "This version of the JavaScript SDK does not support the given datafile version: %s", "Provided event tags are in an invalid format.", "Experiment ID %s is not in datafile.", "Experiment key %s is not in datafile. It is either invalid, paused, or archived.", "Group ID %s is not in datafile.", "Provided %s is in an invalid format.", "JSON object is not valid.", "Provided user ID is in an invalid format.", "Provided user profile service instance is in an invalid format: %s.", "Provided variation key is in an invalid format.", "Integration key missing from datafile. All integrations should include a key.", "Notification listener for (%s) threw exception: %s", "Not tracking user %s.", "No datafile specified. Cannot start optimizely.", "No event processor is provided", "No JSON object to validate against schema.", "No project config available. Failing %s.", "No status code in response", "No variation key %s defined in datafile for experiment %s.", "ODP config is not available.", "ODP events should have at least one key-value pair in identifiers.", "ODP event send failed.", "ODP event manager stopped before it could start", "ODP Manager is missing. %s failed.", "ODP is not integrated", "Only POST requests are supported", "Audience condition %s evaluated to UNKNOWN because the number value for user attribute \"%s\" is not in the range [-2^53, +2^53].", "Promise value is not allowed in sync operation", "Request error", "Request timeout", "Retry cancelled", "sendBeacon failed", "%s not running", "unable to bind optimizely.close() to page unload event: \"%s\"", "Unable to cast value %s to type %s, returning null.", "Unable to get VUID - VuidManager is not available", "Unable to parse & skipped header item", "Provided attribute: %s has an undefined value.", "Audience condition %s evaluated to UNKNOWN because the condition value is not supported.", "Attribute %s unexpectedly has reserved prefix %s; using attribute ID instead of reserved attribute name.", "Audience condition %s evaluated to UNKNOWN because a value of type \"%s\" was passed for user attribute \"%s\".", "Audience condition %s has an unknown condition type. You may need to upgrade to a newer release of the Optimizely SDK.", "Audience condition %s uses an unknown match type. You may need to upgrade to a newer release of the Optimizely SDK.", "Unrecognized attribute %s provided. Pruning before sending event to Optimizely.", "Unrecognized decide option %s provided.", "Unsupported protocol: %s", "User %s is not in the forced variation map. Cannot remove their forced variation.", "Error while looking up user profile for user ID \"%s\": %s.", "Error while saving user profile for user ID \"%s\": %s.", "Variable with key \"%s\" associated with feature with key \"%s\" is not in datafile.", "Requested variable type \"%s\", but variable is of type \"%s\". Use correct API to retrieve value. Returning None.", "Variation ID %s is not in the datafile." ]; /** * Copyright 2023-2024, 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. */ /** * Sample event dispatcher implementation for tracking impression and conversions * Users of the SDK can provide their own implementation * @param {Event} eventObj * @param {Function} callback */ const dispatchEvent = function (eventObj) { const { params, url } = eventObj; const blob = new Blob([JSON.stringify(params)], { type: "application/json", }); const success = navigator.sendBeacon(url, blob); if (success) { return Promise.resolve({}); } return Promise.reject(new OptimizelyError(SEND_BEACON_FAILED)); }; const eventDispatcher$1 = { dispatchEvent, }; /** * Copyright 2019, 2024, 2025, 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. */ exports.LogLevel = void 0; (function (LogLevel) { LogLevel[LogLevel["Debug"] = 0] = "Debug"; LogLevel[LogLevel["Info"] = 1] = "Info"; LogLevel[LogLevel["Warn"] = 2] = "Warn"; LogLevel[LogLevel["Error"] = 3] = "Error"; })(exports.LogLevel || (exports.LogLevel = {})); const LogLevelToUpper = { [exports.LogLevel.Debug]: 'DEBUG', [exports.LogLevel.Info]: 'INFO', [exports.LogLevel.Warn]: 'WARN', [exports.LogLevel.Error]: 'ERROR', }; const LogLevelToLower = { [exports.LogLevel.Debug]: 'debug', [exports.LogLevel.Info]: 'info', [exports.LogLevel.Warn]: 'warn', [exports.LogLevel.Error]: 'error', }; class ConsoleLogHandler { constructor(prefix) { this.prefix = prefix || '[OPTIMIZELY]'; } log(level, message) { const log = `${this.prefix} - ${LogLevelToUpper[level]} ${this.getTime()} ${message}`; this.consoleLog(level, log); } getTime() { return new Date().toISOString(); } consoleLog(logLevel, log) { const methodName = LogLevelToLower[logLevel]; const method = console[methodName] || console.log; method.call(console, log); } } class OptimizelyLogger { constructor(config) { this.prefix = ''; this.logHandler = config.logHandler; this.infoResolver = config.infoMsgResolver; this.errorResolver = config.errorMsgResolver; this.level = config.level; if (config.name) { this.setName(config.name); } } child(name) { return new OptimizelyLogger({ logHandler: this.logHandler, infoMsgResolver: this.infoResolver, errorMsgResolver: this.errorResolver, level: this.level, name, }); } setName(name) { this.name = name; this.prefix = `${name}: `; } info(message, ...args) { this.log(exports.LogLevel.Info, message, args); } debug(message, ...args) { this.log(exports.LogLevel.Debug, message, args); } warn(message, ...args) { this.log(exports.LogLevel.Warn, message, args); } error(message, ...args) { this.log(exports.LogLevel.Error, message, args); } handleLog(level, message, args) { const log = args.length > 0 ? `${this.prefix}${sprintf(message, ...args)}` : `${this.prefix}${message}`; this.logHandler.log(level, log); } log(level, message, args) { if (level < this.level) { return; } if (message instanceof Error) { if (message instanceof OptimizelyError) { message.setMessage(this.errorResolver); } this.handleLog(level, message.message, []); return; } let resolver = this.errorResolver; if (level < exports.LogLevel.Warn) { if (!this.infoResolver) { return; } resolver = this.infoResolver; } const resolvedMessage = resolver.resolve(message); this.handleLog(level, resolvedMessage, args); } } const ADDING_AUTHORIZATION_HEADER_WITH_BEARER_TOKEN = '0'; const AUDIENCE_EVALUATION_RESULT = '1'; const CMAB_CACHE_ATTRIBUTES_MISMATCH = '2'; const CMAB_CACHE_HIT = '3'; const CMAB_CACHE_MISS = '4'; const EVALUATING_AUDIENCE = '5'; const FAILED_TO_PARSE_REVENUE = '6'; const FAILED_TO_PARSE_VALUE = '7'; const FEATURE_ENABLED_FOR_USER = '8'; const FEATURE_NOT_ENABLED_FOR_USER = '9'; const FEATURE_NOT_ENABLED_RETURN_DEFAULT_VARIABLE_VALUE = '10'; const IGNORE_CMAB_CACHE = '11'; const INVALIDATE_CMAB_CACHE = '12'; const INVALID_CLIENT_ENGINE = '13'; const INVALID_DECIDE_OPTIONS = '14'; const INVALID_DEFAULT_DECIDE_OPTIONS = '15'; const INVALID_EXPERIMENT_KEY_INFO = '16'; const MAKING_DATAFILE_REQ_TO_URL_WITH_HEADERS = '17'; const MISSING_ATTRIBUTE_VALUE = '18'; const NOT_ACTIVATING_USER = '19'; const PARSED_NUMERIC_VALUE = '20'; const PARSED_REVENUE_VALUE = '21'; const RESET_CMAB_CACHE = '22'; const RESPONSE_STATUS_CODE = '23'; const SAVED_LAST_MODIFIED_HEADER_VALUE_FROM_RESPONSE = '24'; const SAVED_USER_VARIATION = '25'; const SAVED_VARIATION_NOT_FOUND = '26'; const SHOULD_NOT_DISPATCH_ACTIVATE = '27'; const SKIPPING_JSON_VALIDATION = '28'; const TRACK_EVENT = '29'; const UNEXPECTED_TYPE_NULL = '30'; const UPDATED_OPTIMIZELY_CONFIG = '31'; const USER_HAS_NO_FORCED_VARIATION = '32'; const USER_HAS_NO_FORCED_VARIATION_FOR_EXPERIMENT = '33'; const USER_MAPPED_TO_FORCED_VARIATION = '34'; const USER_RECEIVED_DEFAULT_VARIABLE_VALUE = '35'; const USER_RECEIVED_VARIABLE_VALUE = '36'; const VALID_BUCKETING_ID = '37'; const VALID_DATAFILE = '38'; const VALID_USER_PROFILE_SERVICE = '39'; const VARIABLE_NOT_USED_RETURN_DEFAULT_VARIABLE_VALUE = '40'; const VARIATION_REMOVED_FOR_USER = '41'; const messages = [ "Adding Authorization header with Bearer Token", "Audience \"%s\" evaluated to %s.", "CMAB cache attributes mismatch for user %s and rule %s, fetching new decision.", "Cache hit for user %s and rule %s.", "Cache miss for user %s and rule %s.", "Starting to evaluate audience \"%s\" with conditions: %s.", "Failed to parse revenue value \"%s\" from event tags.", "Failed to parse event value \"%s\" from event tags.", "Feature %s is enabled for user %s.", "Feature %s is not enabled for user %s.", "Feature \"%s\" is not enabled for user %s. Returning the default variable value \"%s\".", "Ignoring CMAB cache for user %s and rule %s.", "Invalidating CMAB cache for user %s and rule %s.", "Invalid client engine passed: %s. Defaulting to node-sdk.", "Provided decide options is not an array. Using default decide options.", "Provided default decide options is not an array.", "Experiment key %s is not in datafile. It is either invalid, paused, or archived.", "Making datafile request to url %s with headers: %s", "Audience condition %s evaluated to UNKNOWN because no value was passed for user attribute \"%s\".", "Not activating user %s for experiment %s.", "Parsed event value \"%s\" from event tags.", "Parsed revenue value \"%s\" from event tags.", "Resetting CMAB cache for user %s and rule %s.", "Response status code: %s", "Saved last modified header value from response: %s", "Saved user profile for user \"%s\".", "User %s was previously bucketed into variation with ID %s for experiment %s, but no matching variation was found.", "Experiment %s is not in \"Running\" state. Not activating user.", "Skipping JSON schema validation.", "Tracking event %s for user %s.", "Audience condition %s evaluated to UNKNOWN because a null value was passed for user attribute \"%s\".", "Updated Optimizely config to revision %s (project id %s)", "User %s is not in the forced variation map.", "No experiment %s mapped to user %s in the forced variation map.", "Set variation %s for experiment %s and user %s in the forced variation map.", "User \"%s\" is not in any variation or rollout rule. Returning default value for variable \"%s\" of feature flag \"%s\".", "Got variable value \"%s\" for variable \"%s\" of feature flag \"%s\"", "BucketingId is valid: \"%s\"", "Datafile is valid.", "Valid user profile service provided.", "Variable \"%s\" is not used in variation \"%s\". Returning default value.", "Variation mapped to experiment %s has been removed for user %s." ]; const infoResolver = { resolve(baseMessage) { const messageNum = parseInt(baseMessage); return messages[messageNum] || baseMessage; } }; const errorResolver = { resolve(baseMessage) { const messageNum = parseInt(baseMessage); return messages$1[messageNum] || baseMessage; } }; /** * Copyright 2025, 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 * * https://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. */ const INVALID_LOG_HANDLER = 'Invalid log handler'; const INVALID_LEVEL_PRESET = 'Invalid level preset'; const debugPreset = { level: exports.LogLevel.Debug, infoResolver, errorResolver, }; const infoPreset = { level: exports.LogLevel.Info, infoResolver, errorResolver, }; const warnPreset = { level: exports.LogLevel.Warn, errorResolver, }; const errorPreset = { level: exports.LogLevel.Error, errorResolver, }; const levelPresetSymbol = Symbol(); const DEBUG = { [levelPresetSymbol]: debugPreset, }; const INFO = { [levelPresetSymbol]: infoPreset, }; const WARN = { [levelPresetSymbol]: warnPreset, }; const ERROR = { [levelPresetSymbol]: errorPreset, }; const extractLevelPreset = (preset) => { if (!preset || typeof preset !== 'object' || !preset[levelPresetSymbol]) { throw new Error(INVALID_LEVEL_PRESET); } return preset[levelPresetSymbol]; }; const loggerSymbol = Symbol(); const validateLogHandler = (logHandler) => { if (typeof logHandler !== 'object' || typeof logHandler.log !== 'function') { throw new Error(INVALID_LOG_HANDLER); } }; const createLogger = (config) => { const { level, infoResolver, errorResolver } = extractLevelPreset(config.level); if (config.logHandler) { validateLogHandler(config.logHandler); } const loggerName = 'Optimizely'; return { [loggerSymbol]: new OptimizelyLogger({ name: loggerName, level, infoMsgResolver: infoResolver, errorMsgResolver: errorResolver, logHandler: config.logHandler || new ConsoleLogHandler(), }), }; }; const extractLogger = (logger) => { if (!logger || typeof logger !== 'object') { return undefined; } return logger[loggerSymbol]; }; class DefaultErrorNotifier { constructor(errorHandler, messageResolver, name) { this.errorHandler = errorHandler; this.messageResolver = messageResolver; this.name = name || ''; } notify(error) { if (error instanceof OptimizelyError) { error.setMessage(this.messageResolver); } this.errorHandler.handleError(error); } child(name) { return new DefaultErrorNotifier(this.errorHandler, this.messageResolver, name); } } /** * Copyright 2025, 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 * * https://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. */ const INVALID_ERROR_HANDLER = 'Invalid error handler'; const errorNotifierSymbol = Symbol(); const validateErrorHandler = (errorHandler) => { if (!errorHandler || typeof errorHandler !== 'object' || typeof errorHandler.handleError !== 'function') { throw new Error(INVALID_ERROR_HANDLER); } }; const createErrorNotifier = (errorHandler) => { validateErrorHandler(errorHandler); return { [errorNotifierSymbol]: new DefaultErrorNotifier(errorHandler, errorResolver), }; }; const extractErrorNotifier = (errorNotifier) => { if (!errorNotifier || typeof errorNotifier !== 'object') { return undefined; } return errorNotifier[errorNotifierSymbol]; }; /**************************************************************************** * Copyright 2018, 2021, 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. * ***************************************************************************/ const AND_CONDITION = 'and'; const OR_CONDITION = 'or'; const NOT_CONDITION = 'not'; const DEFAULT_OPERATOR_TYPES = [AND_CONDITION, OR_CONDITION, NOT_CONDITION]; /** * Top level method to evaluate conditions * @param {ConditionTree<Leaf>} conditions Nested array of and/or conditions, or a single leaf * condition value of any type * Example: ['and', '0', ['or', '1', '2']] * @param {LeafEvaluator<Leaf>} leafEvaluator Function which will be called to evaluate leaf condition * values * @return {?boolean} Result of evaluating the conditions using the operator * rules and the leaf evaluator. A return value of null * indicates that the conditions are invalid or unable to be * evaluated. */ function evaluate$2(conditions, leafEvaluator) { if (Array.isArray(conditions)) { let firstOperator = conditions[0]; let restOfConditions = conditions.slice(1); if (typeof firstOperator === 'string' && DEFAULT_OPERATOR_TYPES.indexOf(firstOperator) === -1) { // Operator to apply is not explicit - assume 'or' firstOperator = OR_CONDITION; restOfConditions = conditions; } switch (firstOperator) { case AND_CONDITION: return andEvaluator(restOfConditions, leafEvaluator); case NOT_CONDITION: return notEvaluator(restOfConditions, leafEvaluator); default: // firstOperator is OR_CONDITION return orEvaluator(restOfConditions, leafEvaluator); } } const leafCondition = conditions; return leafEvaluator(leafCondition); } /** * Evaluates an array of conditions as if the evaluator had been applied * to each entry and the results AND-ed together. * @param {unknown[]} conditions Array of conditions ex: [operand_1, operand_2] * @param {LeafEvaluator<Leaf>} leafEvaluator Function which will be called to evaluate leaf condition values * @return {?boolean} Result of evaluating the conditions. A return value of null * indicates that the conditions are invalid or unable to be * evaluated. */ function andEvaluator(conditions, leafEvaluator) { let sawNullResult = false; if (Array.isArray(conditions)) { for (let i = 0; i < conditions.length; i++) { const conditionResult = evaluate$2(conditions[i], leafEvaluator); if (conditionResult === false) { return false; } if (conditionResult === null) { sawNullResult = true; } } return sawNullResult ? null : true; } return null; } /** * Evaluates an array of conditions as if the evaluator had been applied * to a single entry and NOT was applied to the result. * @param {unknown[]} conditions Array of conditions ex: [operand_1] * @param {LeafEvaluator<Leaf>} leafEvaluator Function which will be called to evaluate leaf condition values * @return {?boolean} Result of evaluating the conditions. A return value of null * indicates that the conditions are invalid or unable to be * evaluated. */ function notEvaluator(conditions, leafEvaluator) { if (Array.isArray(conditions) && conditions.length > 0) { const result = evaluate$2(conditions[0], leafEvaluator); return result === null ? null : !result; } return null; } /** * Evaluates an array of conditions as if the evaluator had been applied * to each entry and the results OR-ed together. * @param {unknown[]} conditions Array of conditions ex: [operand_1, operand_2] * @param {LeafEvaluator<Leaf>} leafEvaluator Function which will be called to evaluate leaf condition values * @return {?boolean} Result of evaluating the conditions. A return value of null * indicates that the conditions are invalid or unable to be * evaluated. */ function orEvaluator(conditions, leafEvaluator) { let sawNullResult = false; if (Array.isArray(conditions)) { for (let i = 0; i < conditions.length; i++) { const conditionResult = evaluate$2(conditions[i], leafEvaluator); if (conditionResult === true) { return true; } if (conditionResult === null) { sawNullResult = true; } } return sawNullResult ? null : false; } return null; } /** * The OptimizelyConfig class * @param {ProjectConfig} configObj * @param {string} datafile */ class OptimizelyConfig { constructor(configObj, datafile, logger) { var _a, _b; this.sdkKey = (_a = configObj.sdkKey) !== null && _a !== void 0 ? _a : ''; this.environmentKey = (_b = configObj.environmentKey) !== null && _b !== void 0 ? _b : ''; this.attributes = configObj.attributes; this.audiences = OptimizelyConfig.getAudiences(configObj); this.events = configObj.events; this.revision = configObj.revision; const featureIdVariablesMap = (configObj.featureFlags || []).reduce((resultMap, feature) => { resultMap[feature.id] = feature.variables; return resultMap; }, {}); const variableIdMap = OptimizelyConfig.getVariableIdMap(configObj); const { experimentsMapById, experimentsMapByKey } = OptimizelyConfig.getExperimentsMap(configObj, featureIdVariablesMap, variableIdMap, logger); this.experimentsMap = experimentsMapByKey; this.featuresMap = OptimizelyConfig.getFeaturesMap(configObj, featureIdVariablesMap, experimentsMapById, variableIdMap); this.datafile = datafile; } /** * Get the datafile * @returns {string} JSON string representation of the datafile that was used to create the current config object */ getDatafile() { return this.datafile; } /** * Get Unique audiences list with typedAudiences as priority * @param {ProjectConfig} configObj * @returns {OptimizelyAudience[]} Array of unique audiences */ static getAudiences(configObj) { const audiences = []; const typedAudienceIds = []; (configObj.typedAudiences || []).forEach((typedAudience) => { audiences.push({ id: typedAudience.id, conditions: JSON.stringify(typedAudience.conditions), name: typedAudience.name, }); typedAudienceIds.push(typedAudience.id); }); (configObj.audiences || []).forEach((audience) => { if (typedAudienceIds.indexOf(audience.id) === -1 && audience.id != '$opt_dummy_audience') { audiences.push({ id: audience.id, conditions: JSON.stringify(audience.conditions), name: audience.name, }); } }); return audiences; } /** * Converts list of audience conditions to serialized audiences used in experiment * for examples: * 1. Input: ["or", "1", "2"] * Output: "\"us\" OR \"female\"" * 2. Input: ["not", "1"] * Output: "NOT \"us\"" * 3. Input: ["or", "1"] * Output: "\"us\"" * 4. Input: ["and", ["or", "1", ["and", "2", "3"]], ["and", "11", ["or", "12", "13"]]] * Output: "(\"us\" OR (\"female\" AND \"adult\")) AND (\"fr\" AND (\"male\" OR \"kid\"))" * @param {Array<string | string[]>} conditions * @param {[id: string]: Audience} audiencesById * @returns {string} Serialized audiences condition string */ static getSerializedAudiences(conditions, audiencesById) { let serializedAudience = ''; if (conditions) { let cond = ''; conditions.forEach((item) => { let subAudience = ''; // Checks if item is list of conditions means it is sub audience if (item instanceof Array) { subAudience = OptimizelyConfig.getSerializedAudiences(item, audiencesById); subAudience = `(${subAudience})`; } else if (DEFAULT_OPERATOR_TYPES.indexOf(item) > -1) { cond = item.toUpperCase(); } else { // Checks if item is audience id const audienceName = audiencesById[item] ? audiencesById[item].name : item; // if audience condition is "NOT" then add "NOT" at start. Otherwise check if there is already audience id in serializedAudience then append condition between serializedAudience and item if (serializedAudience || cond === 'NOT') { cond = cond === '' ? 'OR' : cond; if (serializedAudience === '') { serializedAudience = `${cond} "${audiencesById[item].name}"`; } else { serializedAudience = serializedAudience.concat(` ${cond} "${audienceName}"`); } } else { serializedAudience = `"${audienceName}"`; } } // Checks if sub audience is empty or not if (subAudience !== '') { if (serializedAudience !== '' || cond === 'NOT') { cond = cond === '' ? 'OR' : cond; if (serializedAudience === '') { serializedAudience = `${cond} ${subAudience}`; } else { serializedAudience = serializedAudience.concat(` ${cond} ${subAudience}`); } } else { serializedAudience = serializedAudience.concat(subAudience); } } }); } return serializedAudience; } /** * Get serialized audience condition string for experiment * @param {Experiment} experiment * @param {ProjectConfig} configObj * @returns {string} Serialized audiences condition string */ static getExperimentAudiences(experiment, configObj) { if (!experiment.audienceConditions) { return ''; } return OptimizelyConfig.getSerializedAudiences(experiment.audienceConditions, configObj.audiencesById); } /** * Make map of featureVariable which are associated with given feature experiment * @param {FeatureVariablesMap} featureIdVariableMap * @param {[id: string]: FeatureVariable} variableIdMap * @param {string} featureId * @param {VariationVariable[] | undefined} featureVariableUsages * @param {boolean | undefined} isFeatureEnabled * @returns {OptimizelyVariablesMap} FeatureVariables mapped by key */ static mergeFeatureVariables(featureIdVariableMap, variableIdMap, featureId, featureVariableUsages, isFeatureEnabled) { const variablesMap = (featureIdVariableMap[featureId] || []).reduce((optlyVariablesMap, featureVariable) => { optlyVariablesMap[featureVariable.key] = { id: featureVariable.id, key: featureVariable.key, type: featureVariable.type, value: featureVariable.defaultValue, }; return optlyVariablesMap; }, {}); (featureVariableUsages || []).forEach((featureVariableUsage) => { const defaultVariable = variableIdMap[featureVariableUsage.id]; const optimizelyVariable = { id: featureVariableUsage.id, key: defaultVariable.key, type: defaultVariable.type, value: isFeatureEnabled ? featureVariableUsage.value : defaultVariable.defaultValue, }; variablesMap[defaultVariable.key] = optimizelyVariable; }); return variablesMap; } /** * Gets Map of all experiment variations and variables including rollouts * @param {Variation[]} variations * @param {FeatureVariablesMap} featureIdVariableMap * @param {{[id: string]: FeatureVariable}} variableIdMap * @param {string} featureId * @returns {[key: string]: Variation} Variations mapped by key */ static getVariationsMap(variations, featureIdVariableMap, variableIdMap, featureId) { let variationsMap = {}; variationsMap = variations.reduce((optlyVariationsMap, variation) => { const variablesMap = OptimizelyConfig.mergeFeatureVariables(featureIdVariableMap, variableIdMap, featureId, variation.variables, variation.featureEnabled); optlyVariationsMap[variation.key] = { id: variation.id, key: variation.key, featureEnabled: variation.featureEnabled, variablesMap: variablesMap, }; return optlyVariationsMap; }, {}); return variationsMap; } /** * Gets Map of FeatureVariable with respect to featureVariableId * @param {ProjectConfig} configObj * @returns {[id: string]: FeatureVariable} FeatureVariables mapped by id */ static getVariableIdMap(configObj) { let variablesIdMap = {}; variablesIdMap = (configObj.featureFlags || []).reduce((resultMap, feature) => { feature.variables.forEach((variable) => { resultMap[variable.id] = variable; }); return resultMap; }, {}); return variablesIdMap; } /** * Gets list of rollout experiments * @param {ProjectConfig} configObj * @param {FeatureVariablesMap} featureVariableIdMap * @param {string} featureId * @param {Experiment[]} experiments * @param {{[id: string]: FeatureVariable}} variableIdMap * @returns {OptimizelyExperiment[]} List of Optimizely rollout experiments */ static getDeliveryRules(configObj, featureVariableIdMap, featureId, experiments, variableIdMap) { return experiments.map((experiment) => { return { id: experiment.id, key: experiment.key, audiences: OptimizelyConfig.getExperimentAudiences(experiment, configObj), variationsMap: OptimizelyConfig.getVariationsMap(experiment.variations, featureVariableIdMap, variableIdMap, featureId), }; }); } /** * Get Experiment Ids which are part of rollout * @param {Rollout[]} rollouts * @returns {string[]} Array of experiment Ids */ static getRolloutExperimentIds(rollouts) { const experimentIds = []; (rollouts || []).forEach((rollout) => { rollout.experiments.forEach((e) => { experimentIds.push(e.id); }); }); return experimentIds; } /** * Get experiments mapped by their id's which are not part of a rollout * @param {ProjectConfig} configObj * @param {FeatureVariablesMap} featureIdVariableMap * @param {{[id: string]: FeatureVariable}} variableIdMap * @returns { experimentsMapById: { [id: string]: OptimizelyExperiment }, experimentsMapByKey: OptimizelyExperimentsMap } Experiments mapped by id and key */ static getExperimentsMap(configObj, featureIdVariableMap, variableIdMap, logger) { const rolloutExperimentIds = this.getRolloutExperimentIds(configObj.rollouts); const experimentsMapById = {}; const experimentsMapByKey = {}; const experiments = configObj.experiments || []; experiments.forEach((experiment) => { if (rolloutExperimentIds.indexOf(experiment.id) !== -1) { return; } const featureIds = configObj.experimentFeatureMap[experiment.id]; let featureId = ''; if (featureIds && featureIds.length > 0) { featureId = featureIds[0]; } const variationsMap = OptimizelyConfig.getVariationsMap(experiment.variations, featureIdVariableMap, variableIdMap, featureId.toString()); const optimizelyExperiment = { id: experiment.id, key: experiment.key, audiences: OptimizelyConfig.getExperimentAudiences(experiment, configObj), variationsMap: variationsMap, }; experimentsMapById[experiment.id] = optimizelyExperiment; if (experimentsMapByKey[experiment.key] && logger) { logger.warn(`Duplicate experiment keys found in datafile: ${experiment.key}`); } experimentsMapByKey[experiment.key] = optimizelyExperiment; }); return { experimentsMapById, experimentsMapByKey }; } /** * Get experiments mapped by their keys * @param {OptimizelyExperimentsMap} experimentsMapById * @returns {OptimizelyExperimentsMap} Experiments mapped by key */ static getExperimentsKeyMap(experimentsMapById) { const experimentKeysMap = {}; for (const id in experimentsMapById) { const experiment = experimentsMapById[id]; experimentKeysMap[experiment.key] = experiment; } return experimentKeysMap; } /** * Gets Map of all FeatureFlags and associated experiment map inside it * @param {ProjectConfig} configObj * @param {FeatureVariablesMap} featureVariableIdMap * @param {OptimizelyExperimentsMap} experimentsMapById * @param {{[id: string]: FeatureVariable}} variableIdMap * @returns {OptimizelyFeaturesMap} OptimizelyFeature mapped by key */ static getFeaturesMap(configObj, featureVariableIdMap, experimentsMapById, variableIdMap) { const featuresMap = {}; configObj.featureFlags.forEach((featureFlag) => { const featureExperimentMap = {}; const experimentRules = []; featureFlag.experimentIds.forEach(experimentId => { const experiment = experimentsMapById[experimentId]; if (experiment) { featureExperimentMap[experiment.key] = experiment; } experimentRules.push(experimentsMapById[experimentId]); }); const featureVariableMap = (featureFlag.variables || []).reduce((variables, variable) => {