@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
JavaScript
(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) => {