@amplitude/experiment-js-client
Version:
Amplitude Experiment Javascript Client SDK
1,376 lines (1,352 loc) • 108 kB
JavaScript
/* @amplitude/experiment-js-client v1.19.0 - For license info see https://app.unpkg.com/@amplitude/experiment-js-client@1.19.0/files/LICENSE */
import { safeGlobal, TimeoutError, isLocalStorageAvailable, getGlobalScope, topologicalSort, FetchError, EvaluationEngine, Poller, SdkFlagApi, SdkEvaluationApi } from '@amplitude/experiment-core';
import { AnalyticsConnector } from '@amplitude/analytics-connector';
import { UAParser } from '@amplitude/ua-parser-js';
/**
* @deprecated Update your version of the amplitude analytics-js SDK to 8.17.0+ and for seamless
* integration with the amplitude analytics SDK.
*/
var AmplitudeUserProvider = /** @class */ (function () {
function AmplitudeUserProvider(amplitudeInstance) {
this.amplitudeInstance = amplitudeInstance;
}
AmplitudeUserProvider.prototype.getUser = function () {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
return {
device_id: (_b = (_a = this.amplitudeInstance) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.deviceId,
user_id: (_d = (_c = this.amplitudeInstance) === null || _c === void 0 ? void 0 : _c.options) === null || _d === void 0 ? void 0 : _d.userId,
version: (_f = (_e = this.amplitudeInstance) === null || _e === void 0 ? void 0 : _e.options) === null || _f === void 0 ? void 0 : _f.versionName,
language: (_h = (_g = this.amplitudeInstance) === null || _g === void 0 ? void 0 : _g.options) === null || _h === void 0 ? void 0 : _h.language,
platform: (_k = (_j = this.amplitudeInstance) === null || _j === void 0 ? void 0 : _j.options) === null || _k === void 0 ? void 0 : _k.platform,
os: this.getOs(),
device_model: this.getDeviceModel(),
};
};
AmplitudeUserProvider.prototype.getOs = function () {
var _a, _b, _c, _d, _e, _f;
return [
(_c = (_b = (_a = this.amplitudeInstance) === null || _a === void 0 ? void 0 : _a._ua) === null || _b === void 0 ? void 0 : _b.browser) === null || _c === void 0 ? void 0 : _c.name,
(_f = (_e = (_d = this.amplitudeInstance) === null || _d === void 0 ? void 0 : _d._ua) === null || _e === void 0 ? void 0 : _e.browser) === null || _f === void 0 ? void 0 : _f.major,
]
.filter(function (e) { return e !== null && e !== undefined; })
.join(' ');
};
AmplitudeUserProvider.prototype.getDeviceModel = function () {
var _a, _b, _c;
return (_c = (_b = (_a = this.amplitudeInstance) === null || _a === void 0 ? void 0 : _a._ua) === null || _b === void 0 ? void 0 : _b.os) === null || _c === void 0 ? void 0 : _c.name;
};
return AmplitudeUserProvider;
}());
/**
* @deprecated Update your version of the amplitude analytics-js SDK to 8.17.0+ and for seamless
* integration with the amplitude analytics SDK.
*/
var AmplitudeAnalyticsProvider = /** @class */ (function () {
function AmplitudeAnalyticsProvider(amplitudeInstance) {
this.amplitudeInstance = amplitudeInstance;
}
AmplitudeAnalyticsProvider.prototype.track = function (event) {
this.amplitudeInstance.logEvent(event.name, event.properties);
};
AmplitudeAnalyticsProvider.prototype.setUserProperty = function (event) {
var _a;
var _b;
// if the variant has a value, set the user property and log an event
this.amplitudeInstance.setUserProperties((_a = {},
_a[event.userProperty] = (_b = event.variant) === null || _b === void 0 ? void 0 : _b.value,
_a));
};
AmplitudeAnalyticsProvider.prototype.unsetUserProperty = function (event) {
var _a;
// if the variant does not have a value, unset the user property
this.amplitudeInstance['_logEvent']('$identify', null, null, {
$unset: (_a = {}, _a[event.userProperty] = '-', _a),
});
};
return AmplitudeAnalyticsProvider;
}());
/******************************************************************************
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 __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = {
label: 0,
sent: function () {
if (t[0] & 1) throw t[1];
return t[1];
},
trys: [],
ops: []
},
f,
y,
t,
g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function () {
return this;
}), g;
function verb(n) {
return function (v) {
return step([n, v]);
};
}
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0:
case 1:
t = op;
break;
case 4:
_.label++;
return {
value: op[1],
done: false
};
case 5:
_.label++;
y = op[1];
op = [0];
continue;
case 7:
op = _.ops.pop();
_.trys.pop();
continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
_ = 0;
continue;
}
if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
_.label = op[1];
break;
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1];
t = op;
break;
}
if (t && _.label < t[2]) {
_.label = t[2];
_.ops.push(op);
break;
}
if (t[2]) _.ops.pop();
_.trys.pop();
continue;
}
op = body.call(thisArg, _);
} catch (e) {
op = [6, e];
y = 0;
} finally {
f = t = 0;
}
if (op[0] & 5) throw op[1];
return {
value: op[0] ? op[1] : void 0,
done: true
};
}
}
function __values(o) {
var s = typeof Symbol === "function" && Symbol.iterator,
m = s && o[s],
i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return {
value: o && o[i++],
done: !o
};
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
}
function __read(o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o),
r,
ar = [],
e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
} catch (error) {
e = {
error: error
};
} finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
} finally {
if (e) throw e.error;
}
}
return ar;
}
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var parseAmplitudeCookie = function (apiKey, newFormat) {
var e_1, _a;
if (newFormat === void 0) { newFormat = false; }
// Get the cookie value
var key = generateKey(apiKey, newFormat);
var value = undefined;
var cookies = safeGlobal.document.cookie.split('; ');
try {
for (var cookies_1 = __values(cookies), cookies_1_1 = cookies_1.next(); !cookies_1_1.done; cookies_1_1 = cookies_1.next()) {
var cookie = cookies_1_1.value;
var _b = __read(cookie.split('=', 2), 2), cookieKey = _b[0], cookieValue = _b[1];
if (cookieKey === key) {
value = decodeURIComponent(cookieValue);
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (cookies_1_1 && !cookies_1_1.done && (_a = cookies_1.return)) _a.call(cookies_1);
}
finally { if (e_1) throw e_1.error; }
}
if (!value) {
return;
}
// Parse cookie value depending on format
try {
// New format
if (newFormat) {
var decoding = atob(value);
return JSON.parse(decodeURIComponent(decoding));
}
// Old format
var values = value.split('.');
var userId = undefined;
if (values.length >= 2 && values[1]) {
userId = atob(values[1]);
}
return {
deviceId: values[0],
userId: userId,
};
}
catch (e) {
return;
}
};
var parseAmplitudeLocalStorage = function (apiKey) {
var key = generateKey(apiKey, true);
try {
var value = safeGlobal.localStorage.getItem(key);
if (!value)
return;
var state = JSON.parse(value);
if (typeof state !== 'object')
return;
return state;
}
catch (_a) {
return;
}
};
var parseAmplitudeSessionStorage = function (apiKey) {
var key = generateKey(apiKey, true);
try {
var value = safeGlobal.sessionStorage.getItem(key);
if (!value)
return;
var state = JSON.parse(value);
if (typeof state !== 'object')
return;
return state;
}
catch (_a) {
return;
}
};
var generateKey = function (apiKey, newFormat) {
if (newFormat) {
if ((apiKey === null || apiKey === void 0 ? void 0 : apiKey.length) < 10) {
return;
}
return "AMP_".concat(apiKey.substring(0, 10));
}
if ((apiKey === null || apiKey === void 0 ? void 0 : apiKey.length) < 6) {
return;
}
return "amp_".concat(apiKey.substring(0, 6));
};
/**
* Integration plugin for Amplitude Analytics. Uses the analytics connector to
* track events and get user identity.
*
* On initialization, this plugin attempts to read the user identity from all
* the storage locations and formats supported by the analytics SDK, then
* commits the identity to the connector. The order of locations checks are:
* - Cookie
* - Cookie (Legacy)
* - Local Storage
* - Session Storage
*
* Events are tracked only if the connector has an event receiver set, otherwise
* track returns false, and events are persisted and managed by the
* IntegrationManager.
*/
var AmplitudeIntegrationPlugin = /** @class */ (function () {
function AmplitudeIntegrationPlugin(apiKey, connector, timeoutMillis) {
this.type = 'integration';
this.apiKey = apiKey;
this.identityStore = connector.identityStore;
this.eventBridge = connector.eventBridge;
this.contextProvider = connector.applicationContextProvider;
this.timeoutMillis = timeoutMillis;
this.loadPersistedState();
if (timeoutMillis <= 0) {
this.setup = undefined;
}
}
AmplitudeIntegrationPlugin.prototype.setup = function (config, client) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
// Setup automatic fetch on amplitude identity change.
if (config === null || config === void 0 ? void 0 : config.automaticFetchOnAmplitudeIdentityChange) {
this.identityStore.addIdentityListener(function () {
client === null || client === void 0 ? void 0 : client.fetch();
});
}
return [2 /*return*/, this.waitForConnectorIdentity(this.timeoutMillis)];
});
});
};
AmplitudeIntegrationPlugin.prototype.getUser = function () {
var identity = this.identityStore.getIdentity();
return {
user_id: identity.userId,
device_id: identity.deviceId,
user_properties: identity.userProperties,
version: this.contextProvider.versionName,
};
};
AmplitudeIntegrationPlugin.prototype.track = function (event) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!this.eventBridge.receiver) {
return false;
}
this.eventBridge.logEvent({
eventType: event.eventType,
eventProperties: event.eventProperties,
});
return true;
};
AmplitudeIntegrationPlugin.prototype.loadPersistedState = function () {
// Avoid reading state if the api key is undefined or an experiment
// deployment.
if (!this.apiKey || this.apiKey.startsWith('client-')) {
return false;
}
// New cookie format
var user = parseAmplitudeCookie(this.apiKey, true);
if (user) {
this.commitIdentityToConnector(user);
return true;
}
// Old cookie format
user = parseAmplitudeCookie(this.apiKey, false);
if (user) {
this.commitIdentityToConnector(user);
return true;
}
// Local storage
user = parseAmplitudeLocalStorage(this.apiKey);
if (user) {
this.commitIdentityToConnector(user);
return true;
}
// Session storage
user = parseAmplitudeSessionStorage(this.apiKey);
if (user) {
this.commitIdentityToConnector(user);
return true;
}
return false;
};
AmplitudeIntegrationPlugin.prototype.commitIdentityToConnector = function (user) {
var editor = this.identityStore.editIdentity();
editor.setDeviceId(user.deviceId);
if (user.userId) {
editor.setUserId(user.userId);
}
editor.commit();
};
AmplitudeIntegrationPlugin.prototype.waitForConnectorIdentity = function (ms) {
return __awaiter(this, void 0, void 0, function () {
var identity;
var _this = this;
return __generator(this, function (_a) {
identity = this.identityStore.getIdentity();
if (!identity.userId && !identity.deviceId) {
return [2 /*return*/, Promise.race([
new Promise(function (resolve) {
var listener = function () {
resolve();
_this.identityStore.removeIdentityListener(listener);
};
_this.identityStore.addIdentityListener(listener);
}),
new Promise(function (_, reject) {
safeGlobal.setTimeout(reject, ms, 'Timed out waiting for Amplitude Analytics SDK to initialize.');
}),
])];
}
return [2 /*return*/];
});
});
};
return AmplitudeIntegrationPlugin;
}());
function unfetch(e,n){return n=n||{},new Promise(function(t,r){var s=new XMLHttpRequest,o=[],u=[],i={},a=function(){return {ok:2==(s.status/100|0),statusText:s.statusText,status:s.status,url:s.responseURL,text:function(){return Promise.resolve(s.responseText)},json:function(){return Promise.resolve(JSON.parse(s.responseText))},blob:function(){return Promise.resolve(new Blob([s.response]))},clone:a,headers:{keys:function(){return o},entries:function(){return u},get:function(e){return i[e.toLowerCase()]},has:function(e){return e.toLowerCase()in i}}}};for(var l in s.open(n.method||"get",e,!0),s.onload=function(){s.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,function(e,n,t){o.push(n=n.toLowerCase()),u.push([n,t]),i[n]=i[n]?i[n]+","+t:t;}),t(a());},s.onerror=r,s.withCredentials="include"==n.credentials,n.headers)s.setRequestHeader(l,n.headers[l]);s.send(n.body||null);})}
/**
* @packageDocumentation
* @internal
*/
var fetch = safeGlobal.fetch || unfetch;
/*
* Copied from:
* https://github.com/github/fetch/issues/175#issuecomment-284787564
*/
var timeout = function (promise, timeoutMillis) {
// Don't timeout if timeout is null or invalid
if (timeoutMillis == null || timeoutMillis <= 0) {
return promise;
}
return new Promise(function (resolve, reject) {
safeGlobal.setTimeout(function () {
reject(new TimeoutError('Request timeout after ' + timeoutMillis + ' milliseconds'));
}, timeoutMillis);
promise.then(resolve, reject);
});
};
var _request = function (requestUrl, method, headers, data, timeoutMillis) {
var call = function () { return __awaiter(void 0, void 0, void 0, function () {
var response, simpleResponse;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, fetch(requestUrl, {
method: method,
headers: headers,
body: data,
})];
case 1:
response = _b.sent();
_a = {
status: response.status
};
return [4 /*yield*/, response.text()];
case 2:
simpleResponse = (_a.body = _b.sent(),
_a);
return [2 /*return*/, simpleResponse];
}
});
}); };
return timeout(call(), timeoutMillis);
};
/**
* Wrap the exposed HttpClient in a CoreClient implementation to work with
* FlagsApi and EvaluationApi.
*/
var WrapperClient = /** @class */ (function () {
function WrapperClient(client) {
this.client = client;
}
WrapperClient.prototype.request = function (request) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.client.request(request.requestUrl, request.method, request.headers, null, request.timeoutMillis)];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
};
return WrapperClient;
}());
var FetchHttpClient = { request: _request };
/**
* Log level enumeration for controlling logging verbosity.
* @category Logging
*/
var LogLevel;
(function (LogLevel) {
/**
* Disable all logging
*/
LogLevel[LogLevel["Disable"] = 0] = "Disable";
/**
* Error level logging - only critical errors
*/
LogLevel[LogLevel["Error"] = 1] = "Error";
/**
* Warning level logging - errors and warnings
*/
LogLevel[LogLevel["Warn"] = 2] = "Warn";
/**
* Info level logging - errors, warnings, and informational messages
*/
LogLevel[LogLevel["Info"] = 3] = "Info";
/**
* Debug level logging - errors, warnings, info, and debug messages
*/
LogLevel[LogLevel["Debug"] = 4] = "Debug";
/**
* Verbose level logging - all messages including verbose details
*/
LogLevel[LogLevel["Verbose"] = 5] = "Verbose";
})(LogLevel || (LogLevel = {}));
/**
* Determines the primary source of variants before falling back.
*
* @category Source
*/
var Source;
(function (Source) {
/**
* The default way to source variants within your application. Before the
* assignments are fetched, `getVariant(s)` will fallback to local storage
* first, then `initialVariants` if local storage is empty. This option
* effectively falls back to an assignment fetched previously.
*/
Source["LocalStorage"] = "localStorage";
/**
* This bootstrap option is used primarily for servers-side rendering using an
* Experiment server SDK. This bootstrap option always prefers the config
* `initialVariants` over data in local storage, even if variants are fetched
* successfully and stored locally.
*/
Source["InitialVariants"] = "initialVariants";
})(Source || (Source = {}));
/**
* Indicates from which source the variant() function determines the variant
*
* @category Source
*/
var VariantSource;
(function (VariantSource) {
VariantSource["LocalStorage"] = "storage";
VariantSource["InitialVariants"] = "initial";
VariantSource["SecondaryLocalStorage"] = "secondary-storage";
VariantSource["SecondaryInitialVariants"] = "secondary-initial";
VariantSource["FallbackInline"] = "fallback-inline";
VariantSource["FallbackConfig"] = "fallback-config";
VariantSource["LocalEvaluation"] = "local-evaluation";
})(VariantSource || (VariantSource = {}));
/**
* Returns true if the VariantSource is one of the fallbacks (inline or config)
*
* @param source a {@link VariantSource}
* @returns true if source is {@link VariantSource.FallbackInline} or {@link VariantSource.FallbackConfig}
*/
var isFallback = function (source) {
return (!source ||
source === VariantSource.FallbackInline ||
source === VariantSource.FallbackConfig ||
source === VariantSource.SecondaryInitialVariants);
};
/**
Defaults for Experiment Config options
| **Option** | **Default** |
|------------------|-----------------------------------|
| **debug** | `false` |
| **logLevel** | `LogLevel.Error` |
| **logger** | `null` (ConsoleLogger will be used) |
| **instanceName** | `$default_instance` |
| **fallbackVariant** | `null` |
| **initialVariants** | `null` |
| **initialFlags** | `undefined` |
| **source** | `Source.LocalStorage` |
| **serverUrl** | `"https://api.lab.amplitude.com"` |
| **flagsServerUrl** | `"https://flag.lab.amplitude.com"` |
| **serverZone** | `"US"` |
| **assignmentTimeoutMillis** | `10000` |
| **retryFailedAssignment** | `true` |
| **automaticExposureTracking** | `true` |
| **pollOnStart** | `true` |
| **flagConfigPollingIntervalMillis** | `300000` |
| **fetchOnStart** | `true` |
| **automaticFetchOnAmplitudeIdentityChange** | `false` |
| **userProvider** | `null` |
| **analyticsProvider** | `null` |
| **exposureTrackingProvider** | `null` |
*
* @category Configuration
*/
var Defaults = {
debug: false,
logLevel: LogLevel.Error,
loggerProvider: null,
instanceName: '$default_instance',
fallbackVariant: {},
initialVariants: {},
initialFlags: undefined,
source: Source.LocalStorage,
serverUrl: 'https://api.lab.amplitude.com',
flagsServerUrl: 'https://flag.lab.amplitude.com',
serverZone: 'US',
fetchTimeoutMillis: 10000,
retryFetchOnFailure: true,
throwOnError: false,
automaticExposureTracking: true,
pollOnStart: true,
flagConfigPollingIntervalMillis: 300000,
fetchOnStart: true,
automaticFetchOnAmplitudeIdentityChange: false,
userProvider: null,
analyticsProvider: null,
exposureTrackingProvider: null,
httpClient: FetchHttpClient,
};
var version = "1.19.0";
var MAX_QUEUE_SIZE = 512;
/**
* Handles integration plugin management, event persistence and deduplication.
*/
var IntegrationManager = /** @class */ (function () {
function IntegrationManager(config, client) {
var _this = this;
var _a;
this.isReady = new Promise(function (resolve) {
_this.resolve = resolve;
});
this.config = config;
this.client = client;
var instanceName = (_a = config.instanceName) !== null && _a !== void 0 ? _a : Defaults.instanceName;
this.queue = new PersistentTrackingQueue(instanceName);
this.cache = new SessionDedupeCache(instanceName);
}
/**
* Returns a promise when the integration has completed setup. If no
* integration has been set, returns a resolved promise.
*/
IntegrationManager.prototype.ready = function () {
if (!this.integration) {
return Promise.resolve();
}
return this.isReady;
};
/**
* Set the integration to be managed. An existing integration is torndown,
* and the new integration is setup. This function resolves the promise
* returned by ready() if it has not already been resolved.
*
* @param integration the integration to manage.
*/
IntegrationManager.prototype.setIntegration = function (integration) {
var _this = this;
if (this.integration && this.integration.teardown) {
void this.integration.teardown();
}
this.integration = integration;
if (integration.setup) {
this.integration.setup(this.config, this.client).then(function () {
_this.queue.setTracker(_this.integration.track.bind(integration));
_this.resolve();
}, function () {
_this.queue.setTracker(_this.integration.track.bind(integration));
_this.resolve();
});
}
else {
this.queue.setTracker(this.integration.track.bind(integration));
this.resolve();
}
};
/**
* Get the user from the integration. If no integration is set, returns an
* empty object.
*/
IntegrationManager.prototype.getUser = function () {
if (!this.integration) {
return {};
}
return this.integration.getUser();
};
/**
* Deduplicates exposures using session storage, then tracks the event to the
* integration. If no integration is set, or if the integration returns false,
* the event is persisted in local storage.
*
* @param exposure
*/
IntegrationManager.prototype.track = function (exposure) {
if (this.cache.shouldTrack(exposure)) {
var event_1 = this.getExposureEvent(exposure);
this.queue.push(event_1);
}
};
IntegrationManager.prototype.getExposureEvent = function (exposure) {
var _a, _b, _c;
var event = {
eventType: '$exposure',
eventProperties: exposure,
};
if ((_a = exposure.metadata) === null || _a === void 0 ? void 0 : _a.exposureEvent) {
// Metadata specifically passes the exposure event definition
event = {
eventType: (_b = exposure.metadata) === null || _b === void 0 ? void 0 : _b.exposureEvent,
eventProperties: exposure,
};
}
else if (((_c = exposure.metadata) === null || _c === void 0 ? void 0 : _c.deliveryMethod) === 'web') {
// Web experiments track impression events by default
event = {
eventType: '$impression',
eventProperties: exposure,
};
}
return event;
};
return IntegrationManager;
}());
var SessionDedupeCache = /** @class */ (function () {
function SessionDedupeCache(instanceName) {
this.isSessionStorageAvailable = checkIsSessionStorageAvailable();
this.inMemoryCache = {};
this.storageKey = "EXP_sent_v2_".concat(instanceName);
// Remove previous version of storage if it exists.
if (this.isSessionStorageAvailable) {
safeGlobal.sessionStorage.removeItem("EXP_sent_".concat(instanceName));
}
}
SessionDedupeCache.prototype.shouldTrack = function (exposure) {
var _a;
// Always track web impressions.
if (((_a = exposure.metadata) === null || _a === void 0 ? void 0 : _a.deliveryMethod) === 'web') {
return true;
}
this.loadCache();
var cachedExposure = this.inMemoryCache[exposure.flag_key];
var shouldTrack = false;
if (!cachedExposure || cachedExposure.variant !== exposure.variant) {
shouldTrack = true;
this.inMemoryCache[exposure.flag_key] = exposure;
}
this.storeCache();
return shouldTrack;
};
SessionDedupeCache.prototype.loadCache = function () {
if (this.isSessionStorageAvailable) {
var storedCache = safeGlobal.sessionStorage.getItem(this.storageKey);
this.inMemoryCache = storedCache ? JSON.parse(storedCache) : {};
}
};
SessionDedupeCache.prototype.storeCache = function () {
if (this.isSessionStorageAvailable) {
safeGlobal.sessionStorage.setItem(this.storageKey, JSON.stringify(this.inMemoryCache));
}
};
return SessionDedupeCache;
}());
var PersistentTrackingQueue = /** @class */ (function () {
function PersistentTrackingQueue(instanceName, maxQueueSize) {
if (maxQueueSize === void 0) { maxQueueSize = MAX_QUEUE_SIZE; }
this.isLocalStorageAvailable = isLocalStorageAvailable();
this.inMemoryQueue = [];
this.storageKey = "EXP_unsent_".concat(instanceName);
this.maxQueueSize = maxQueueSize;
}
PersistentTrackingQueue.prototype.push = function (event) {
this.loadQueue();
this.inMemoryQueue.push(event);
this.flush();
this.storeQueue();
};
PersistentTrackingQueue.prototype.setTracker = function (tracker) {
var _this = this;
this.tracker = tracker;
this.poller = safeGlobal.setInterval(function () {
_this.loadFlushStore();
}, 1000);
this.loadFlushStore();
};
PersistentTrackingQueue.prototype.flush = function () {
var e_1, _a;
if (!this.tracker)
return;
if (this.inMemoryQueue.length === 0)
return;
try {
for (var _b = __values(this.inMemoryQueue), _c = _b.next(); !_c.done; _c = _b.next()) {
var event_2 = _c.value;
try {
if (!this.tracker(event_2)) {
return;
}
}
catch (e) {
return;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
this.inMemoryQueue = [];
if (this.poller) {
safeGlobal.clearInterval(this.poller);
this.poller = undefined;
}
};
PersistentTrackingQueue.prototype.loadQueue = function () {
if (this.isLocalStorageAvailable) {
var storedQueue = safeGlobal.localStorage.getItem(this.storageKey);
this.inMemoryQueue = storedQueue ? JSON.parse(storedQueue) : [];
}
};
PersistentTrackingQueue.prototype.storeQueue = function () {
if (this.isLocalStorageAvailable) {
// Trim the queue if it is too large.
if (this.inMemoryQueue.length > this.maxQueueSize) {
this.inMemoryQueue = this.inMemoryQueue.slice(this.inMemoryQueue.length - this.maxQueueSize);
}
safeGlobal.localStorage.setItem(this.storageKey, JSON.stringify(this.inMemoryQueue));
}
};
PersistentTrackingQueue.prototype.loadFlushStore = function () {
this.loadQueue();
this.flush();
this.storeQueue();
};
return PersistentTrackingQueue;
}());
var checkIsSessionStorageAvailable = function () {
var globalScope = getGlobalScope();
if (globalScope) {
try {
var testKey = 'EXP_test';
globalScope.sessionStorage.setItem(testKey, testKey);
globalScope.sessionStorage.removeItem(testKey);
return true;
}
catch (e) {
return false;
}
}
return false;
};
/**
* Internal logger class that wraps a Logger implementation and handles log level filtering.
* This class provides a centralized logging mechanism for the Experiment client.
* @category Logging
*/
var AmpLogger = /** @class */ (function () {
/**
* Creates a new AmpLogger instance
* @param logger The underlying logger implementation to use
* @param logLevel The minimum log level to output. Messages below this level will be ignored.
*/
function AmpLogger(logger, logLevel) {
if (logLevel === void 0) { logLevel = LogLevel.Error; }
this.logger = logger;
this.logLevel = logLevel;
}
/**
* Log an error message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
AmpLogger.prototype.error = function (message) {
var _a;
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
if (this.logLevel >= LogLevel.Error) {
(_a = this.logger).error.apply(_a, __spreadArray([message], __read(optionalParams), false));
}
};
/**
* Log a warning message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
AmpLogger.prototype.warn = function (message) {
var _a;
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
if (this.logLevel >= LogLevel.Warn) {
(_a = this.logger).warn.apply(_a, __spreadArray([message], __read(optionalParams), false));
}
};
/**
* Log an informational message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
AmpLogger.prototype.info = function (message) {
var _a;
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
if (this.logLevel >= LogLevel.Info) {
(_a = this.logger).info.apply(_a, __spreadArray([message], __read(optionalParams), false));
}
};
/**
* Log a debug message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
AmpLogger.prototype.debug = function (message) {
var _a;
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
if (this.logLevel >= LogLevel.Debug) {
(_a = this.logger).debug.apply(_a, __spreadArray([message], __read(optionalParams), false));
}
};
/**
* Log a verbose message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
AmpLogger.prototype.verbose = function (message) {
var _a;
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
if (this.logLevel >= LogLevel.Verbose) {
(_a = this.logger).verbose.apply(_a, __spreadArray([message], __read(optionalParams), false));
}
};
return AmpLogger;
}());
/**
* Default console-based logger implementation.
* This logger uses the browser's console API to output log messages.
* Log level filtering is handled by the AmpLogger wrapper class.
* @category Logging
*/
var ConsoleLogger = /** @class */ (function () {
function ConsoleLogger() {
}
/**
* Log an error message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
ConsoleLogger.prototype.error = function (message) {
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
console.error.apply(console, __spreadArray([message], __read(optionalParams), false));
};
/**
* Log a warning message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
ConsoleLogger.prototype.warn = function (message) {
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
console.warn.apply(console, __spreadArray([message], __read(optionalParams), false));
};
/**
* Log an informational message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
ConsoleLogger.prototype.info = function (message) {
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
console.info.apply(console, __spreadArray([message], __read(optionalParams), false));
};
/**
* Log a debug message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
ConsoleLogger.prototype.debug = function (message) {
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
console.debug.apply(console, __spreadArray([message], __read(optionalParams), false));
};
/**
* Log a verbose message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
ConsoleLogger.prototype.verbose = function (message) {
var optionalParams = [];
for (var _i = 1; _i < arguments.length; _i++) {
optionalParams[_i - 1] = arguments[_i];
}
console.debug.apply(console, __spreadArray([message], __read(optionalParams), false));
};
return ConsoleLogger;
}());
var LocalStorage = /** @class */ (function () {
function LocalStorage() {
this.globalScope = getGlobalScope();
}
LocalStorage.prototype.get = function (key) {
var _a;
return (_a = this.globalScope) === null || _a === void 0 ? void 0 : _a.localStorage.getItem(key);
};
LocalStorage.prototype.put = function (key, value) {
var _a;
(_a = this.globalScope) === null || _a === void 0 ? void 0 : _a.localStorage.setItem(key, value);
};
LocalStorage.prototype.delete = function (key) {
var _a;
(_a = this.globalScope) === null || _a === void 0 ? void 0 : _a.localStorage.removeItem(key);
};
return LocalStorage;
}());
var getVariantStorage = function (deploymentKey, instanceName, storage) {
var truncatedDeployment = deploymentKey.substring(deploymentKey.length - 6);
var namespace = "amp-exp-".concat(instanceName, "-").concat(truncatedDeployment);
return new LoadStoreCache(namespace, storage, transformVariantFromStorage);
};
var getFlagStorage = function (deploymentKey, instanceName, storage) {
if (storage === void 0) { storage = new LocalStorage(); }
var truncatedDeployment = deploymentKey.substring(deploymentKey.length - 6);
var namespace = "amp-exp-".concat(instanceName, "-").concat(truncatedDeployment, "-flags");
return new LoadStoreCache(namespace, storage);
};
var LoadStoreCache = /** @class */ (function () {
function LoadStoreCache(namespace, storage, transformer) {
this.cache = {};
this.namespace = namespace;
this.storage = storage;
this.transformer = transformer;
}
LoadStoreCache.prototype.get = function (key) {
return this.cache[key];
};
LoadStoreCache.prototype.getAll = function () {
return __assign({}, this.cache);
};
LoadStoreCache.prototype.put = function (key, value) {
this.cache[key] = value;
};
LoadStoreCache.prototype.putAll = function (values) {
var e_1, _a;
try {
for (var _b = __values(Object.keys(values)), _c = _b.next(); !_c.done; _c = _b.next()) {
var key = _c.value;
this.cache[key] = values[key];
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
};
LoadStoreCache.prototype.remove = function (key) {
delete this.cache[key];
};
LoadStoreCache.prototype.clear = function () {
this.cache = {};
};
LoadStoreCache.prototype.load = function () {
var e_2, _a;
var rawValues = this.storage.get(this.namespace);
var jsonValues;
try {
jsonValues = JSON.parse(rawValues) || {};
}
catch (_b) {
// Do nothing
return;
}
var values = {};
try {
for (var _c = __values(Object.keys(jsonValues)), _d = _c.next(); !_d.done; _d = _c.next()) {
var key = _d.value;
try {
var value = void 0;
if (this.transformer) {
value = this.transformer(jsonValues[key]);
}
else {
value = jsonValues[key];
}
if (value) {
values[key] = value;
}
}
catch (_e) {
// Do nothing
}
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
}
finally { if (e_2) throw e_2.error; }
}
this.clear();
this.putAll(values);
};
LoadStoreCache.prototype.store = function (values) {
if (values === void 0) { values = this.cache; }
this.storage.put(this.namespace, JSON.stringify(values));
};
return LoadStoreCache;
}());
var transformVariantFromStorage = function (storageValue) {
if (typeof storageValue === 'string') {
// From v0 string format
return {
key: storageValue,
value: storageValue,
};
}
else if (typeof storageValue === 'object') {
// From v1 or v2 object format
var key = storageValue['key'];
var value = storageValue['value'];
var payload = storageValue['payload'];
var metadata = storageValue['metadata'];
var experimentKey = storageValue['expKey'];
if (metadata && metadata.experimentKey) {
experimentKey = metadata.experimentKey;
}
else if (experimentKey) {
metadata = metadata || {};
metadata['experimentKey'] = experimentKey;
}
var variant = {};
if (key) {
variant.key = key;
}
else if (value) {
variant.key = value;
}
if (value)
variant.value = value;
if (metadata)
variant.metadata = metadata;
if (payload)
variant.payload = payload;
if (experimentKey)
variant.expKey = experimentKey;
return variant;
}
};
var SessionStorage = /** @class */ (function () {
function SessionStorage() {
this.globalScope = getGlobalScope();
}
SessionStorage.prototype.get = function (key) {
var _a;
return (_a = this.globalScope) === null || _a === void 0 ? void 0 : _a.sessionStorage.getItem(key);
};
SessionStorage.prototype.put = function (key, value) {
var _a;
(_a = this.globalScope) === null || _a === void 0 ? void 0 : _a.sessionStorage.setItem(key, value);
};
SessionStorage.prototype.delete = function (key) {
var _a;
(_a = this.globalScope) === null || _a === void 0 ? void 0 : _a.sessionStorage.removeItem(key);
};
return SessionStorage;
}());
/**
* Event for tracking a user's exposure to a variant. This event will not count
* towards your analytics event volume.
*
* @deprecated use ExposureTrackingProvider instead
*/
var exposureEvent = function (user, key, variant, source) {
var _a;
var name = '[Experiment] Exposure';
var value = variant === null || variant === void 0 ? void 0 : variant.value;
var userProperty = "[Experiment] ".concat(key);
return {
name: name,
user: user,
key: key,
variant: variant,
userProperty: userProperty,
properties: {
key: key,
variant: value,
source: source,
},
userProperties: (_a = {},
_a[userProperty] = value,
_a),
};
};
var isNullOrUndefined = function (value) {
return value === null || value === undefined;
};
var isNullUndefinedOrEmpty = function (value) {
if (isNullOrUndefined(value))
return true;
return value && Object.keys(value).length === 0;
};
/**
* Filters out null and undefined values from an object, returning a new object
* with only defined values. This is useful for config merging where you want
* defaults to take precedence over explicit null/undefined values.
*/
var filterNullUndefined = function (obj) {
var e_1, _a;
if (!obj || typeof obj !== 'object') {
return {};
}
var filtered = {};
try {
for (var _b = __values(Object.entries(obj)), _c = _b.next(); !_c.done; _c = _b.next()) {
var _d = __read(_c.value, 2), key = _d[0], value = _d[1];
if (!isNullOrUndefined(value)) {
filtered[key] = value;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
return filtered;
};
var isLocalEvaluationMode = function (flag) {
var _a;
return ((_a = flag === null || flag === void 0 ? void 0 : flag.metadata) === null || _a === void 0 ? void 0 : _a.evaluationMode) === 'local';
};
var Backoff = /** @class */ (function () {
function Backoff(attempts, min, max, scalar) {
this.started = false;
this.done = false;
this.attempts = attempts;
this.min = min;
this.max = max;
this.scalar = scalar;
}
Backoff.prototype.start = function (fn) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!this.started) {
this.started = true;
}
else {
throw Error('Backoff already started');
}
return [4 /*yield*/, this.backoff(fn, 0, this.min)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
};
Backoff.prototype.cancel = function () {
this.done = true;
clearTimeout(this.timeoutHandle);
};
Backoff.prototype.backoff = function (fn, attempt, delay) {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
if (this.done) {
return [2 /*return*/];
}
this.timeoutHandle = safeGlobal.setTimeout(function () { return __awaiter(_this, void 0, void 0, function () {
var nextAttempt, nextDelay;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: