@amplitude/experiment-js-client
Version:
Amplitude Experiment Javascript Client SDK
1,372 lines (1,348 loc) • 100 kB
JavaScript
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 };
/**
* 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` |
| **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,
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.17.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;
};
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:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, fn()];
case 1:
_a.sent();
return [3 /*break*/, 3];
case 2:
_a.sent();
nextAttempt = attempt + 1;
if (nextAttempt < this.attempts) {
nextDelay = Math.min(delay * this.scalar, this.max);
this.backoff(fn, nextAttempt, nextDelay);
}
return [3 /*break*/, 3];
case 3: return [2 /*return*/];
}
});
}); }, delay);
return [2 /*return*/];
});
});
};
return Backoff;
}());
var convertUserToContext = function (user) {
var e_1, _a;
var _b, _c;
if (!user) {
return {};
}
var context = { user: user };
// add page context
var globalScope = getGlobalScope();
if (globalScope) {
context.page = {
url: globalScope.location.href,
};
}
var groups = {};
if (!user.groups) {
return context;
}
try {
for (var _d = __values(Object.keys(user.groups)), _e = _d.next(); !_e.done; _e = _d.next()) {
var groupType = _e.value;
var groupNames = user.groups[groupType];
if (groupNames.length > 0 && groupNames[0]) {
var groupName = groupNames[0];
var groupNameMap = {
group_name: groupName,
};
// Check for group properties
var groupProperties = (_c = (_b = user.group_properties) === null || _b === void 0 ? void 0 : _b[groupType]) === null || _c === void 0 ? void 0 : _c[groupName];
if (groupProperties && Object.keys(groupProperties).length > 0) {
groupNameMap['group_properties'] = groupProperties;
}
groups[groupType] = groupNameMap;
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
}
finally { if (e_1) throw e_1.error; }
}
if (Object.keys(groups).length > 0) {
context['groups'] = groups;
}
delete context.user['groups'];
delete context.user['group_properties'];
return context;
};
var convertVariant = function (value) {
if (value === null || value === undefined) {
return {};
}
if (typeof value == 'string') {
return {
key: value,
value: value,
};
}
else {
return value;
}
};
var convertEvaluationVariantToVariant = function (evaluationVariant) {
if (!evaluationVariant) {
return {};
}
var experimentKey = undefined;
if (evaluationVariant.metadata) {
experimentKey = evaluationVariant.metadata['experimentKey'];
}
var variant = {};
if (evaluationVariant.key)
variant.key = evaluationVariant.key;
if (evaluationVariant.value)
variant.value = evaluationVariant.value;
if (evaluationVariant.payload)
variant.payload = evaluationVariant.payload;
if (experimentKey)
variant.expKey = experimentKey;
if (evaluationVariant.metadata)
variant.metadata = evaluationVariant.metadata;
return variant;
};
/**
* A wrapper for an analytics provider which only sends one exposure event per
* flag, per variant, per session. In other words, wrapping an analytics
* provider in this class will prevent the same exposure event to be sent twice
* in one session.
*/
var SessionAnalyticsProvider = /** @class */ (function () {
function SessionAnalyticsProvider(analyticsProvider) {
// In memory record of flagKey and variant value to in order to only set
// user properties and track an exposure event once per session unless the
// variant value changes
this.setProperties = {};
this.unsetProperties = {};
this.analyticsProvider = analyticsProvider;
}
SessionAnalyticsProvider.prototype.track = function (event) {
if (this.setProperties[event.key] == event.variant.value) {
return;
}
else {
this.setProperties[event.key] = event.variant.value;
delete this.unsetProperties[event.key];
}
this.analyticsProvider.track(event);
};
SessionAnalyticsProvider.prototype.setUserProperty = function (event) {
if (this.setProperties[event.key] == event.variant.value) {
return;
}
this.analyticsProvider.setUserProperty(event);
};
SessionAnalyticsProvider.prototype.unsetUserProperty = function (event) {
if (this.unsetProperties[event.key]) {
return;
}
else {
this.unsetProperties[event.key] = 'unset';
delete this.setProperties[event.key];
}
this.analyticsProvider.unsetUserProperty(event);
};
return SessionAnalyticsProvider;
}());
/**
* A wrapper for an exposure tracking provider which only sends one exposure event per
* flag, per variant, per user session. When the user identity (userId or deviceId) changes,
* the tracking cache is reset to ensure exposures are tracked for the new user session.
*/
var UserSessionExposureTracker = /** @class */ (function () {
function UserSessionExposureTracker(exposureTrackingProvider) {
this.tracked = {};
this.identity = {};
this.exposureTrackingProvider = exposureTrackingProvider;
}
UserSessionExposureTracker.prototype.track = function (exposure, user) {
var newIdentity = {
userId: user === null || user === void 0 ? void 0 : user.user_id,
deviceId: user === null || user === void 0 ? void 0 : user.device_id,
};
if (!this.identityEquals(this.identity, newIdentity)) {
this.tracked = {};
}
this.identity = newIdentity;
var hasTrackedFlag = exposure.flag_key in this.tracked;
var trackedVariant = this.tracked[exposure.flag_key];
if (hasTrackedFlag && trackedVariant === exposure.variant) {
return;
}
this.tracked[exposure.flag_key] = exposure.variant;
this.exposureTrackingProvider.track(exposure);
};
UserSessionExposureTracker.prototype.identityEquals = function (id1, id2) {
return id1.userId === id2.userId && id1.deviceId === id2.deviceId;
};
return UserSessionExposureTracker;
}());
/**
* @packageDocumentation
* @module experiment-js-client
*/
// Configs which have been removed from the public API.
// May be added back in the future.
var fetchBackoffTimeout = 10000;
var fetchBackoffAttempts = 8;
var fetchBackoffMinMillis = 500;
var fetchBackoffMaxMillis = 10000;
var fetchBackoffScalar = 1.5;
var minFlagPollerIntervalMillis = 6000