UNPKG

@absmartly/javascript-sdk

Version:

A/B Smartly Javascript SDK

697 lines (696 loc) 23.5 kB
"use strict"; require("core-js/modules/es.array.slice.js"); require("core-js/modules/es.symbol.description.js"); require("core-js/modules/es.promise.js"); require("core-js/modules/es.string.trim.js"); require("core-js/modules/es.object.entries.js"); require("core-js/modules/es.array.iterator.js"); require("core-js/modules/es.array.map.js"); require("core-js/modules/es.regexp.exec.js"); require("core-js/modules/es.string.split.js"); require("core-js/modules/es.set.js"); require("core-js/modules/es.array.from.js"); function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("./utils"); const assigner_1 = require("./assigner"); const matcher_1 = require("./matcher"); const algorithm_1 = require("./algorithm"); class Context { constructor(sdk, options, params, promise) { this._sdk = sdk; this._publisher = options.publisher || this._sdk.getContextPublisher(); this._dataProvider = options.dataProvider || this._sdk.getContextDataProvider(); this._eventLogger = options.eventLogger || this._sdk.getEventLogger(); this._opts = options; this._pending = 0; this._failed = false; this._finalized = false; this._attrs = []; this._goals = []; this._exposures = []; this._overrides = {}; this._cassignments = {}; this._units = {}; this._assigners = {}; this._audienceMatcher = new matcher_1.AudienceMatcher(); if (params.units) { this.units(params.units); } if ((0, utils_1.isPromise)(promise)) { this._promise = promise.then(data => { this._init(data); delete this._promise; this._logEvent("ready", data); if (this.pending() > 0) { this._setTimeout(); } }).catch(error => { this._init({}); this._failed = true; delete this._promise; this._logError(error); }); } else { promise = promise; this._init(promise); this._logEvent("ready", promise); } } isReady() { return this._promise === undefined; } isFinalizing() { return !this._finalized && this._finalizing != null; } isFinalized() { return this._finalized; } isFailed() { return this._failed; } ready() { if (this.isReady()) { return Promise.resolve(true); } return new Promise(resolve => { var _a; (_a = this._promise) === null || _a === void 0 ? void 0 : _a.then(() => resolve(true)).catch(e => resolve(e)); }); } pending() { return this._pending; } data() { this._checkReady(); return this._data; } eventLogger() { return this._eventLogger; } publisher() { return this._publisher; } provider() { return this._dataProvider; } publish(requestOptions) { this._checkReady(true); return new Promise((resolve, reject) => { this._flush(error => { if (error) { reject(error); } else { resolve(); } }, requestOptions); }); } refresh(requestOptions) { this._checkReady(true); return new Promise((resolve, reject) => { this._refresh(error => { if (error) { reject(error); } else { resolve(); } }, requestOptions); }); } getUnit(unitType) { return this._units[unitType]; } unit(unitType, uid) { this._checkNotFinalized(); switch (typeof uid) { case "string": uid = uid.trim(); if (uid.length === 0) throw new Error(`Unit '${unitType}' UID must not be blank.`); break; case "number": break; default: throw new Error(`Unit '${unitType}' must be a string or a number.`); } const previous = this._units[unitType]; if (previous !== undefined && previous !== uid) { throw new Error(`Unit '${unitType}' UID already set.`); } this._units[unitType] = uid; } getUnits() { return this._units; } units(units) { Object.entries(units).forEach(([unitType, uid]) => { this.unit(unitType, uid); }); } getAttribute(attrName) { let result; this._attrs.forEach(attr => { if (attr.name === attrName) result = attr.value; }); return result; } attribute(attrName, value) { this._checkNotFinalized(); this._attrs.push({ name: attrName, value: value, setAt: Date.now() }); } getAttributes() { const attributes = {}; this._attrs.map(a => [a.name, a.value]).forEach(([key, value]) => { attributes[key] = value; }); return attributes; } attributes(attrs) { Object.entries(attrs).forEach(([attrName, value]) => { this.attribute(attrName, value); }); } peek(experimentName) { this._checkReady(true); return this._peek(experimentName).variant; } treatment(experimentName) { this._checkReady(true); return this._treatment(experimentName).variant; } track(goalName, properties) { this._checkNotFinalized(); return this._track(goalName, properties); } finalize(requestOptions) { return this._finalize(requestOptions); } experiments() { var _a; this._checkReady(); return (_a = this._data.experiments) === null || _a === void 0 ? void 0 : _a.map(x => x.name); } variableValue(key, defaultValue) { this._checkReady(true); return this._variableValue(key, defaultValue); } peekVariableValue(key, defaultValue) { this._checkReady(true); return this._peekVariable(key, defaultValue); } variableKeys() { this._checkReady(true); const variableExperiments = {}; Object.entries(this._indexVariables).forEach(([key, values]) => { values.forEach(value => { if (variableExperiments[key]) variableExperiments[key].push(value.data.name);else variableExperiments[key] = [value.data.name]; }); }); return variableExperiments; } override(experimentName, variant) { this._overrides = Object.assign(this._overrides, { [experimentName]: variant }); } overrides(experimentVariants) { Object.entries(experimentVariants).forEach(([experimentName, variant]) => { this.override(experimentName, variant); }); } customAssignment(experimentName, variant) { this._checkNotFinalized(); this._cassignments[experimentName] = variant; } customAssignments(experimentVariants) { Object.entries(experimentVariants).forEach(([experimentName, variant]) => { this.customAssignment(experimentName, variant); }); } _checkNotFinalized() { if (this.isFinalized()) { throw new Error("ABSmartly Context is finalized."); } else if (this.isFinalizing()) { throw new Error("ABSmartly Context is finalizing."); } } _checkReady(expectNotFinalized) { if (!this.isReady()) { throw new Error("ABSmartly Context is not yet ready."); } if (expectNotFinalized) { this._checkNotFinalized(); } } _assign(experimentName) { const experimentMatches = (experiment, assignment) => { return experiment.id === assignment.id && experiment.unitType === assignment.unitType && experiment.iteration === assignment.iteration && experiment.fullOnVariant === assignment.fullOnVariant && (0, utils_1.arrayEqualsShallow)(experiment.trafficSplit, assignment.trafficSplit); }; const hasCustom = (experimentName in this._cassignments); const hasOverride = (experimentName in this._overrides); const experiment = experimentName in this._index ? this._index[experimentName] : null; if (experimentName in this._assignments) { const assignment = this._assignments[experimentName]; if (hasOverride) { if (assignment.overridden && assignment.variant === this._overrides[experimentName]) { return assignment; } } else if (experiment == null) { if (!assignment.assigned) { return assignment; } } else if (!hasCustom || this._cassignments[experimentName] === assignment.variant) { if (experimentMatches(experiment.data, assignment)) { return assignment; } } } const assignment = { id: 0, iteration: 0, fullOnVariant: 0, unitType: null, variant: 0, overridden: false, assigned: false, exposed: false, eligible: true, fullOn: false, custom: false, audienceMismatch: false }; this._assignments[experimentName] = assignment; if (hasOverride) { if (experiment != null) { assignment.id = experiment.data.id; assignment.unitType = experiment.data.unitType; } assignment.overridden = true; assignment.variant = this._overrides[experimentName]; } else { if (experiment != null) { const unitType = experiment.data.unitType; if (experiment.data.audience && experiment.data.audience.length > 0) { const attrs = {}; this._attrs.forEach(attr => { attrs[attr.name] = attr.value; }); const result = this._audienceMatcher.evaluate(experiment.data.audience, attrs); if (typeof result === "boolean") { assignment.audienceMismatch = !result; } } if (experiment.data.audienceStrict && assignment.audienceMismatch) { assignment.variant = 0; } else if (experiment.data.fullOnVariant === 0) { if (unitType !== null) { if (unitType in this._units) { const unit = this._unitHash(unitType); if (unit !== null) { const assigner = unitType in this._assigners ? this._assigners[unitType] : this._assigners[unitType] = new assigner_1.VariantAssigner(unit); const eligible = assigner.assign(experiment.data.trafficSplit, experiment.data.trafficSeedHi, experiment.data.trafficSeedLo) === 1; assignment.assigned = true; assignment.eligible = eligible; if (eligible) { if (hasCustom) { assignment.variant = this._cassignments[experimentName]; assignment.custom = true; } else { assignment.variant = assigner.assign(experiment.data.split, experiment.data.seedHi, experiment.data.seedLo); } } else { assignment.variant = 0; } } } } } else { assignment.assigned = true; assignment.eligible = true; assignment.variant = experiment.data.fullOnVariant; assignment.fullOn = true; } assignment.unitType = unitType; assignment.id = experiment.data.id; assignment.iteration = experiment.data.iteration; assignment.trafficSplit = experiment.data.trafficSplit; assignment.fullOnVariant = experiment.data.fullOnVariant; } } if (experiment != null && assignment.variant < experiment.data.variants.length) { assignment.variables = experiment.variables[assignment.variant]; } return assignment; } _peek(experimentName) { return this._assign(experimentName); } _treatment(experimentName) { const assignment = this._assign(experimentName); if (!assignment.exposed) { assignment.exposed = true; this._queueExposure(experimentName, assignment); } return assignment; } _queueExposure(experimentName, assignment) { const exposureEvent = { id: assignment.id, name: experimentName, exposedAt: Date.now(), unit: assignment.unitType, variant: assignment.variant, assigned: assignment.assigned, eligible: assignment.eligible, overridden: assignment.overridden, fullOn: assignment.fullOn, custom: assignment.custom, audienceMismatch: assignment.audienceMismatch }; this._logEvent("exposure", exposureEvent); this._exposures.push(exposureEvent); this._pending++; this._setTimeout(); } _customFieldKeys() { const keys = new Set(); if (!this._data.experiments) return []; var _iterator = _createForOfIteratorHelper(this._data.experiments), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { const experiment = _step.value; if (experiment.customFieldValues != null) { var _iterator2 = _createForOfIteratorHelper(experiment.customFieldValues), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { const customFieldValues = _step2.value; keys.add(customFieldValues.name); } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return Array.from(keys); } customFieldKeys() { this._checkReady(true); return this._customFieldKeys(); } _customFieldValue(experimentName, key) { var _a; const experiment = this._index[experimentName]; if (experiment != null) { const field = (_a = experiment.data.customFieldValues) === null || _a === void 0 ? void 0 : _a.find(x => x.name === key); if (field != null) { switch (field.type) { case "text": case "string": return field.value; case "number": return Number(field.value); case "json": try { if (field.value === "null") return null; if (field.value === "") return ""; return JSON.parse(field.value); } catch (e) { console.error(`Failed to parse JSON custom field value '${key}' for experiment '${experimentName}'`); return null; } case "boolean": return field.value === "true"; default: console.error(`Unknown custom field type '${field.type}' for experiment '${experimentName}' and key '${key}' - you may need to upgrade to the latest SDK version`); return null; } } } return null; } customFieldValue(experimentName, key) { this._checkReady(true); return this._customFieldValue(experimentName, key); } _customFieldValueType(experimentName, key) { var _a; const experiment = this._index[experimentName]; if (experiment != null) { const field = (_a = experiment.data.customFieldValues) === null || _a === void 0 ? void 0 : _a.find(x => x.name === key); if (field != null) { return field.type; } } return null; } customFieldValueType(experimentName, key) { this._checkReady(true); return this._customFieldValueType(experimentName, key); } _variableValue(key, defaultValue) { for (const i in this._indexVariables[key]) { const experimentName = this._indexVariables[key][i].data.name; const assignment = this._assign(experimentName); if (assignment.variables !== undefined) { if (!assignment.exposed) { assignment.exposed = true; this._queueExposure(experimentName, assignment); } if (key in assignment.variables && (assignment.assigned || assignment.overridden)) { return assignment.variables[key]; } } } return defaultValue; } _peekVariable(key, defaultValue) { for (const i in this._indexVariables[key]) { const experimentName = this._indexVariables[key][i].data.name; const assignment = this._assign(experimentName); if (assignment.variables !== undefined) { if (key in assignment.variables && (assignment.assigned || assignment.overridden)) { return assignment.variables[key]; } } } return defaultValue; } _validateGoal(goalName, properties) { if (properties !== null && properties !== undefined) { if (!(0, utils_1.isObject)(properties)) { throw new Error(`Goal '${goalName}' properties must be of type object.`); } return Object.assign({}, properties); } return null; } _track(goalName, properties) { const props = this._validateGoal(goalName, properties); const goalEvent = { name: goalName, properties: props, achievedAt: Date.now() }; this._logEvent("goal", goalEvent); this._goals.push(goalEvent); this._pending++; this._setTimeout(); } _setTimeout() { if (this.isReady()) { if (this._publishTimeout === undefined && this._opts.publishDelay >= 0) { this._publishTimeout = setTimeout(() => { this._flush(); }, this._opts.publishDelay); } } } _flush(callback, requestOptions) { if (this._publishTimeout !== undefined) { clearTimeout(this._publishTimeout); delete this._publishTimeout; } if (this._pending === 0) { if (typeof callback === "function") { callback(); } } else { if (!this._failed) { const request = { publishedAt: Date.now(), units: Object.entries(this._units).map(entry => ({ type: entry[0], uid: this._unitHash(entry[0]) })), hashed: true }; if (this._goals.length > 0) { request.goals = this._goals.map(x => ({ name: x.name, achievedAt: x.achievedAt, properties: x.properties })); } if (this._exposures.length > 0) { request.exposures = this._exposures.map(x => ({ id: x.id, name: x.name, unit: x.unit, exposedAt: x.exposedAt, variant: x.variant, assigned: x.assigned, eligible: x.eligible, overridden: x.overridden, fullOn: x.fullOn, custom: x.custom, audienceMismatch: x.audienceMismatch })); } if (this._attrs.length > 0) { request.attributes = this._attrs.map(x => ({ name: x.name, value: x.value, setAt: x.setAt })); } this._publisher.publish(request, this._sdk, this, requestOptions).then(() => { this._logEvent("publish", request); if (typeof callback === "function") { callback(); } }).catch(e => { this._logError(e); if (typeof callback === "function") { callback(e); } }); } else { if (typeof callback === "function") { callback(); } } this._pending = 0; this._exposures = []; this._goals = []; } } _refresh(callback, requestOptions) { if (!this._failed) { this._dataProvider.getContextData(this._sdk, requestOptions).then(data => { this._init(data, this._assignments); this._logEvent("refresh", data); if (typeof callback === "function") { callback(); } }).catch(e => { this._logError(e); if (typeof callback === "function") { callback(e); } }); } else { if (typeof callback === "function") { callback(); } } } _logEvent(eventName, data) { if (this._eventLogger) { this._eventLogger(this, eventName, data); } } _logError(error) { if (this._eventLogger) { this._eventLogger(this, "error", error); } } _unitHash(unitType) { if (!this._hashes) { this._hashes = {}; } if (!(unitType in this._hashes)) { const hash = unitType in this._units ? (0, utils_1.hashUnit)(this._units[unitType]) : null; this._hashes[unitType] = hash; return hash; } return this._hashes[unitType]; } _init(data, assignments = {}) { this._data = data; const index = {}; const indexVariables = {}; (data.experiments || []).forEach(experiment => { const variables = []; const entry = { data: experiment, variables }; index[experiment.name] = entry; experiment.variants.forEach((variant, i) => { const config = variant.config; const parsed = config != null && config.length > 0 ? JSON.parse(config) : {}; Object.keys(parsed).forEach(key => { const value = entry; if (indexVariables[key]) { (0, algorithm_1.insertUniqueSorted)(indexVariables[key], value, (a, b) => a.data.id < b.data.id); } else indexVariables[key] = [value]; }); variables[i] = parsed; }); }); this._index = index; this._indexVariables = indexVariables; this._assignments = assignments; if (!this._failed && this._opts.refreshPeriod > 0 && !this._refreshInterval) { this._refreshInterval = setInterval(() => this._refresh(), this._opts.refreshPeriod); } } _finalize(requestOptions) { if (!this._finalized) { if (!this._finalizing) { if (this._refreshInterval !== undefined) { clearInterval(this._refreshInterval); delete this._refreshInterval; } if (this.pending() > 0) { this._finalizing = new Promise((resolve, reject) => { this._flush(error => { this._finalizing = null; if (error) { reject(error); } else { this._finalized = true; this._logEvent("finalize"); resolve(); } }, requestOptions); }); return this._finalizing; } this._finalized = true; this._logEvent("finalize"); return Promise.resolve(); } return this._finalizing; } return Promise.resolve(); } } exports.default = Context;