@instantdomain/bandit
Advanced tools
Comparing version 1.0.9 to 1.0.10
@@ -1,16 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Default = void 0; | ||
var tslib_1 = require("tslib"); | ||
var react_1 = tslib_1.__importDefault(require("react")); | ||
var constants = tslib_1.__importStar(require("../lib/constants")); | ||
var hooks_1 = require("../lib/hooks"); | ||
/** | ||
* A `Default` renders its children when the default variant is being presented | ||
*/ | ||
function Default(props) { | ||
var variant = (0, hooks_1.useInstantBandit)().variant; | ||
var matchesDefaultVariant = (variant.name === constants.DEFAULT_VARIANT_NAME); | ||
return (react_1.default.createElement(react_1.default.Fragment, null, matchesDefaultVariant && props.children)); | ||
} | ||
exports.Default = Default; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Default=void 0;var a=require("tslib"),b=a.__importDefault(require("react")),c=a.__importStar(require("../lib/constants")),d=require("../lib/hooks");exports.Default=function(a){var e=(0,d.useInstantBandit)().variant.name===c.DEFAULT_VARIANT_NAME;return b.default.createElement(b.default.Fragment,null,e&&a.children)} |
@@ -1,185 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InstantBandit = exports.InstantBanditComponent = void 0; | ||
var tslib_1 = require("tslib"); | ||
var react_1 = tslib_1.__importStar(require("react")); | ||
var constants_1 = require("../lib/constants"); | ||
var types_1 = require("../lib/types"); | ||
var contexts_1 = require("../lib/contexts"); | ||
var utils_1 = require("../lib/utils"); | ||
var defaults_1 = require("../lib/defaults"); | ||
/** | ||
* Enables Instant Bandit in your apps and websites. | ||
*/ | ||
var InstantBanditComponent = function (props) { | ||
var _a = (0, react_1.useState)(false), ready = _a[0], setReady = _a[1]; | ||
var _b = (0, react_1.useState)({ | ||
renders: 0, | ||
recordedExposure: false, | ||
state: types_1.LoadState.PRELOAD, | ||
loadTimeStart: new Date().getTime(), | ||
loadTimeEnd: 0, | ||
loadDuration: 0, | ||
loadTimedOut: false, | ||
loadTimeoutTimer: null, | ||
}), loadState = _b[0], setLoadState = _b[1]; | ||
var _c = (0, react_1.useState)(function () { | ||
var _a = props || {}, config = _a.options, siteName = _a.siteName; | ||
var ctx = (0, contexts_1.createBanditContext)(config); | ||
var loader = ctx.loader; | ||
// Hook to switch variants on the fly for debugging | ||
ctx.select = function (variant) { return tslib_1.__awaiter(void 0, void 0, void 0, function () { | ||
var site, err_1; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
_a.trys.push([0, 2, , 3]); | ||
return [4 /*yield*/, loader.load(ctx, siteName, typeof variant === "string" ? variant : variant === null || variant === void 0 ? void 0 : variant.name)]; | ||
case 1: | ||
site = _a.sent(); | ||
setBanditState(tslib_1.__assign({}, ctx)); | ||
return [2 /*return*/, site]; | ||
case 2: | ||
err_1 = _a.sent(); | ||
handleError(err_1, ctx); | ||
console.warn("[IB] Error loading site: ".concat(err_1)); | ||
return [2 /*return*/, defaults_1.DEFAULT_SITE]; | ||
case 3: return [2 /*return*/]; | ||
} | ||
}); | ||
}); }; | ||
return ctx; | ||
}), ctx = _c[0], setBanditState = _c[1]; | ||
var defer = props.defer, select = props.select, siteProp = props.site, siteName = props.siteName, propsTimeout = props.timeout, onError = props.onError, onReady = props.onReady; | ||
var loader = ctx.loader, metrics = ctx.metrics, session = ctx.session; | ||
var timeout = (0, utils_1.exists)(propsTimeout) ? propsTimeout : constants_1.DEFAULT_TIMEOUT; | ||
// Rather than setting state as soon as we are ready, we defer it to the layout effect. | ||
var readyCallback = (0, react_1.useCallback)(function () { | ||
if (loadState.state === types_1.LoadState.READY) { | ||
return; | ||
} | ||
loadState.state = types_1.LoadState.READY; | ||
// Give the layoutEffect the go ahead, that's where we'll set our main state | ||
// and kick off any layout changes, render, and we'll repaint more effectively. | ||
setReady(true); | ||
if (onReady) { | ||
try { | ||
onReady(ctx); | ||
} | ||
catch (err) { | ||
console.warn("[IB] An error occurred while handling a ready event: ".concat(err)); | ||
} | ||
} | ||
}, [loadState, onReady, ctx]); | ||
// Flush any queued metrics | ||
var flush = (0, react_1.useCallback)(function () { | ||
try { | ||
window.removeEventListener("beforeunload", flush); | ||
document.removeEventListener("onvisibilitychange", flush); | ||
} | ||
finally { | ||
metrics.flush(ctx, true).catch(function () { return void 0; }); | ||
} | ||
}, [ctx, metrics]); | ||
function broadcastReadyState() { | ||
readyCallback(); | ||
} | ||
function markVariantPresented(ctx) { | ||
if (!utils_1.isBrowserEnvironment || loadState.recordedExposure === true) { | ||
return; | ||
} | ||
var experiment = ctx.experiment, variant = ctx.variant; | ||
try { | ||
session.persistVariant(ctx, experiment.id, variant.name); | ||
} | ||
catch (err) { | ||
console.warn("[IB] Session not saved"); | ||
} | ||
// Track the exposure | ||
metrics.sinkEvent(ctx, constants_1.DefaultMetrics.EXPOSURES); | ||
loadState.recordedExposure = true; | ||
} | ||
function handleError(err, ib) { | ||
if (err === void 0) { err = null; } | ||
if (!err) { | ||
return; | ||
} | ||
console.warn("[IB] Component received error: ".concat(err)); | ||
if (onError) { | ||
try { | ||
onError(err, ib); | ||
} | ||
catch (err) { | ||
console.warn("[IB] Additional error received invoking error handler: ".concat(err)); | ||
} | ||
} | ||
} | ||
var timeoutCallback = (0, react_1.useCallback)(function () { | ||
if (loadState.state === types_1.LoadState.READY) { | ||
return; | ||
} | ||
loadState.loadTimedOut = true; | ||
loadState.loadTimeEnd = new Date().getTime(); | ||
loadState.loadDuration = loadState.loadTimeEnd - loadState.loadTimeStart; | ||
return function () { return clearTimeout(loadState.loadTimeoutTimer); }; | ||
}, [loadState]); | ||
// Kick off site loading ASAP | ||
if (loader && loadState.state === types_1.LoadState.PRELOAD) { | ||
// Note: Not calling setState for internal state. Prevents unwanted renders aka flicker | ||
loadState.state = types_1.LoadState.WAIT; | ||
// Note: In order to load in SSR without flicker, we must initialize *synchronously*. | ||
if (siteProp) { | ||
loader.init(ctx, siteProp, select); | ||
setLoadState(tslib_1.__assign(tslib_1.__assign({}, loadState), { state: types_1.LoadState.READY })); | ||
markVariantPresented(ctx); | ||
broadcastReadyState(); | ||
if (loader.error) { | ||
handleError(loader.error, ctx); | ||
} | ||
} | ||
else { | ||
if (!(0, utils_1.exists)(loadState.loadTimeoutTimer) && (0, utils_1.exists)(timeout)) { | ||
loadState.loadTimeoutTimer = setTimeout(timeoutCallback, timeout); | ||
} | ||
loader.load(ctx, siteName, select) | ||
.then(function () { | ||
if (loadState.loadTimedOut) { | ||
ctx.init(defaults_1.DEFAULT_SITE); | ||
markVariantPresented(ctx); | ||
setLoadState(tslib_1.__assign({}, loadState)); | ||
broadcastReadyState(); | ||
throw new Error("[IB] Timed out waiting for site @ ".concat(loadState.loadDuration, " ms.")); | ||
} | ||
}) | ||
// This will invoke a layout effect, which will do our primary state update in | ||
.then(function () { return markVariantPresented(ctx); }) | ||
.then(function () { return setLoadState(tslib_1.__assign(tslib_1.__assign({}, loadState), { state: types_1.LoadState.READY })); }) | ||
.then(broadcastReadyState) | ||
.then(function () { return loader.error ? handleError(loader.error, ctx) : void 0; }) | ||
.catch(function (err) { return handleError(err, ctx); }); | ||
} | ||
} | ||
(0, react_1.useEffect)(function () { | ||
window.addEventListener("beforeunload", flush); | ||
document.addEventListener("onvisibilitychange", flush); | ||
return flush; | ||
}, [flush]); | ||
// This state change happens synchronously before the next paint. | ||
// Arranging our state changes in order to do our biggest one here reduces flicker immensely. | ||
(0, utils_1.useIsomorphicLayoutEffect)(function () { | ||
if (ready) { | ||
setBanditState(ctx); | ||
} | ||
}, []); | ||
// Skip the hydration render. | ||
// This decouples browser selection from full SSR selection and de-risks against rehydration | ||
// errors in general. | ||
if (defer === true && loadState.renders === 0) { | ||
++loadState.renders; | ||
setTimeout(function () { return setLoadState(tslib_1.__assign({}, loadState)); }); | ||
return (react_1.default.createElement(react_1.default.Fragment, null)); | ||
} | ||
return (react_1.default.createElement(contexts_1.InstantBanditContext.Provider, { value: ctx }, ready && props.children)); | ||
}; | ||
exports.InstantBanditComponent = InstantBanditComponent; | ||
exports.InstantBandit = InstantBanditComponent; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.InstantBandit=exports.InstantBanditComponent=void 0;var b=require("tslib"),c=b.__importStar(require("react")),d=require("../lib/constants"),e=require("../lib/types"),f=require("../lib/contexts"),g=require("../lib/utils"),h=require("../lib/defaults"),a=function(j){var l=function(){H()},t=function(a){if(g.isBrowserEnvironment&& !0!==i.recordedExposure){var b=a.experiment,c=a.variant;try{G.persistVariant(a,b.id,c.name)}catch(e){console.warn("[IB] Session not saved")}A.sinkEvent(a,d.DefaultMetrics.EXPOSURES),i.recordedExposure=!0}},u=function(a,b){if(void 0===a&&(a=null),a&&(console.warn("[IB] Component received error: ".concat(a)),F))try{F(a,b)}catch(c){console.warn("[IB] Additional error received invoking error handler: ".concat(c))}},m=(0,c.useState)(!1),v=m[0],D=m[1],n=(0,c.useState)({renders:0,recordedExposure:!1,state:e.LoadState.PRELOAD,loadTimeStart:new Date().getTime(),loadTimeEnd:0,loadDuration:0,loadTimedOut:!1,loadTimeoutTimer:null}),i=n[0],w=n[1],o=(0,c.useState)(function(){var c=j||{},d=c.options,e=c.siteName,a=(0,f.createBanditContext)(d),g=a.loader;return a.select=function(c){return b.__awaiter(void 0,void 0,void 0,function(){var d,f;return b.__generator(this,function(i){switch(i.label){case 0:return i.trys.push([0,2,,3]),[4,g.load(a,e,"string"==typeof c?c:null==c?void 0:c.name)];case 1:return d=i.sent(),E(b.__assign({},a)),[2,d];case 2:return u(f=i.sent(),a),console.warn("[IB] Error loading site: ".concat(f)),[2,h.DEFAULT_SITE];case 3:return[2]}})})},a}),a=o[0],E=o[1],x=j.defer,p=j.select,q=j.site,y=j.siteName,r=j.timeout,F=j.onError,z=j.onReady,k=a.loader,A=a.metrics,G=a.session,s=(0,g.exists)(r)?r:d.DEFAULT_TIMEOUT,H=(0,c.useCallback)(function(){if(i.state!==e.LoadState.READY&&(i.state=e.LoadState.READY,D(!0),z))try{z(a)}catch(b){console.warn("[IB] An error occurred while handling a ready event: ".concat(b))}},[i,z,a]),B=(0,c.useCallback)(function(){try{window.removeEventListener("beforeunload",B),document.removeEventListener("onvisibilitychange",B)}finally{A.flush(a,!0).catch(function(){})}},[a,A]),C=(0,c.useCallback)(function(){if(i.state!==e.LoadState.READY)return i.loadTimedOut=!0,i.loadTimeEnd=new Date().getTime(),i.loadDuration=i.loadTimeEnd-i.loadTimeStart,function(){return clearTimeout(i.loadTimeoutTimer)}},[i]);return(k&&i.state===e.LoadState.PRELOAD&&(i.state=e.LoadState.WAIT,q?(k.init(a,q,p),w(b.__assign(b.__assign({},i),{state:e.LoadState.READY})),t(a),l(),k.error&&u(k.error,a)):(!(0,g.exists)(i.loadTimeoutTimer)&&(0,g.exists)(s)&&(i.loadTimeoutTimer=setTimeout(C,s)),k.load(a,y,p).then(function(){if(i.loadTimedOut)throw a.init(h.DEFAULT_SITE),t(a),w(b.__assign({},i)),l(),Error("[IB] Timed out waiting for site @ ".concat(i.loadDuration," ms."))}).then(function(){return t(a)}).then(function(){return w(b.__assign(b.__assign({},i),{state:e.LoadState.READY}))}).then(l).then(function(){return k.error?u(k.error,a):void 0}).catch(function(b){return u(b,a)}))),(0,c.useEffect)(function(){return window.addEventListener("beforeunload",B),document.addEventListener("onvisibilitychange",B),B},[B]),(0,g.useIsomorphicLayoutEffect)(function(){v&&E(a)},[]),!0===x&&0===i.renders)?(++i.renders,setTimeout(function(){return w(b.__assign({},i))}),c.default.createElement(c.default.Fragment,null)):c.default.createElement(f.InstantBanditContext.Provider,{value:a},v&&j.children)};exports.InstantBanditComponent=a,exports.InstantBandit=a |
@@ -1,16 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Placeholder = void 0; | ||
var tslib_1 = require("tslib"); | ||
var react_1 = tslib_1.__importDefault(require("react")); | ||
var types_1 = require("../lib/types"); | ||
var hooks_1 = require("../lib/hooks"); | ||
/** | ||
* A Placeholder renders its children whenever Instant Bandit is in a loading state. | ||
*/ | ||
function Placeholder(props) { | ||
var loader = (0, hooks_1.useInstantBandit)().loader; | ||
var siteReady = loader.state === types_1.LoadState.READY; | ||
return (react_1.default.createElement(react_1.default.Fragment, null, !siteReady && props.children)); | ||
} | ||
exports.Placeholder = Placeholder; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Placeholder=void 0;var a=require("tslib").__importDefault(require("react")),b=require("../lib/types"),c=require("../lib/hooks");exports.Placeholder=function(d){var e=(0,c.useInstantBandit)().loader.state===b.LoadState.READY;return a.default.createElement(a.default.Fragment,null,!e&&d.children)} |
@@ -1,6 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var tslib_1 = require("tslib"); | ||
tslib_1.__exportStar(require("./Default"), exports); | ||
tslib_1.__exportStar(require("./Placeholder"), exports); | ||
tslib_1.__exportStar(require("./Variant"), exports); | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var a=require("tslib");a.__exportStar(require("./Default"),exports),a.__exportStar(require("./Placeholder"),exports),a.__exportStar(require("./Variant"),exports) |
@@ -1,19 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Variant = void 0; | ||
var tslib_1 = require("tslib"); | ||
var react_1 = tslib_1.__importDefault(require("react")); | ||
var types_1 = require("../lib/types"); | ||
var hooks_1 = require("../lib/hooks"); | ||
/** | ||
* A Variant is rendered when its `name` prop matches the current variant | ||
*/ | ||
var Variant = function (props) { | ||
var name = props.name; | ||
var _a = (0, hooks_1.useInstantBandit)(), loader = _a.loader, variant = _a.variant; | ||
var matchesVariant = variant && variant.name === name; | ||
var siteIsReady = loader.state === types_1.LoadState.READY; | ||
var isPresent = matchesVariant && siteIsReady; | ||
return (react_1.default.createElement(react_1.default.Fragment, null, isPresent && props.children)); | ||
}; | ||
exports.Variant = Variant; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Variant=void 0;var a=require("tslib").__importDefault(require("react")),b=require("../lib/types"),c=require("../lib/hooks");exports.Variant=function(d){var g=d.name,e=(0,c.useInstantBandit)(),h=e.loader,f=e.variant,i=f&&f.name===g,j=h.state===b.LoadState.READY;return a.default.createElement(a.default.Fragment,null,i&&j&&d.children)} |
@@ -1,13 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Default = exports.Variant = exports.InstantBandit = void 0; | ||
var tslib_1 = require("tslib"); | ||
var InstantBanditComponent_1 = require("./components/InstantBanditComponent"); | ||
Object.defineProperty(exports, "InstantBandit", { enumerable: true, get: function () { return InstantBanditComponent_1.InstantBandit; } }); | ||
var primitives_1 = require("./components/primitives"); | ||
Object.defineProperty(exports, "Variant", { enumerable: true, get: function () { return primitives_1.Variant; } }); | ||
Object.defineProperty(exports, "Default", { enumerable: true, get: function () { return primitives_1.Default; } }); | ||
tslib_1.__exportStar(require("./lib/defaults"), exports); | ||
tslib_1.__exportStar(require("./lib/hooks"), exports); | ||
tslib_1.__exportStar(require("./lib/models"), exports); | ||
tslib_1.__exportStar(require("./lib/types"), exports); | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Default=exports.Variant=exports.InstantBandit=void 0;var a=require("tslib"),b=require("./components/InstantBanditComponent");Object.defineProperty(exports,"InstantBandit",{enumerable:!0,get:function(){return b.InstantBandit}});var c=require("./components/primitives");Object.defineProperty(exports,"Variant",{enumerable:!0,get:function(){return c.Variant}}),Object.defineProperty(exports,"Default",{enumerable:!0,get:function(){return c.Default}}),a.__exportStar(require("./lib/defaults"),exports),a.__exportStar(require("./lib/hooks"),exports),a.__exportStar(require("./lib/models"),exports),a.__exportStar(require("./lib/types"),exports) |
@@ -1,41 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.conversionRates = exports.maxKey = exports.otherProbabilities = exports.bandit = void 0; | ||
var tslib_1 = require("tslib"); | ||
/** | ||
* This is an epsilon-greedy bandit algorithm. | ||
* @see https://en.wikipedia.org/wiki/Multi-armed_bandit#Semi-uniform_strategies | ||
* IDEA: equal probs until one clear winner | ||
*/ | ||
function bandit(exposures, conversions, epsilon // taken from common values in literature | ||
) { | ||
var _a; | ||
if (epsilon === void 0) { epsilon = 0.2; } | ||
var rates = conversionRates(exposures, conversions); | ||
var winningVariant = maxKey(rates); | ||
return tslib_1.__assign((_a = {}, _a[winningVariant] = 1 - epsilon, _a), otherProbabilities(Object.keys(exposures), winningVariant, epsilon)); | ||
} | ||
exports.bandit = bandit; | ||
function otherProbabilities(variants, winningVariant, epsilon) { | ||
var otherVariants = variants.filter(function (v) { return v !== winningVariant; }); | ||
return Object.fromEntries(otherVariants.map(function (v) { return [v, epsilon / otherVariants.length]; })); | ||
} | ||
exports.otherProbabilities = otherProbabilities; | ||
function maxKey(rates) { | ||
return Object.entries(rates).reduce(function (_a, _b) { | ||
var v1 = _a[0], rate1 = _a[1]; | ||
var v2 = _b[0], rate2 = _b[1]; | ||
return rate1 > rate2 ? [v1, rate1] : [v2, rate2]; | ||
})[0]; | ||
} | ||
exports.maxKey = maxKey; | ||
function conversionRates(exposures, conversions) { | ||
return Object.fromEntries(Object.entries(exposures).map(function (_a) { | ||
var v = _a[0], count = _a[1]; | ||
return [ | ||
v, | ||
(conversions[v] || 0) / count, | ||
]; | ||
})); | ||
} | ||
exports.conversionRates = conversionRates; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.conversionRates=exports.maxKey=exports.otherProbabilities=exports.bandit=void 0;var d=require("tslib");function a(a,c,d){var b=a.filter(function(a){return a!==c});return Object.fromEntries(b.map(function(a){return[a,d/b.length]}))}function b(a){return Object.entries(a).reduce(function(a,b){var e=a[0],c=a[1],f=b[0],d=b[1];return c>d?[e,c]:[f,d]})[0]}function c(a,b){return Object.fromEntries(Object.entries(a).map(function(a){var c=a[0],d=a[1];return[c,(b[c]||0)/d,]}))}exports.bandit=function(f,i,e){void 0===e&&(e=.2);var g,j=c(f,i),h=b(j);return d.__assign(((g={})[h]=1-e,g),a(Object.keys(f),h,e))},exports.otherProbabilities=a,exports.maxKey=b,exports.conversionRates=c |
@@ -26,2 +26,10 @@ export declare const DEFAULT_NAME = "default"; | ||
export declare const MAX_STORAGE_VALUE_LENGTH = 1024; | ||
export declare const METRICS_MAX_LENGTH: number; | ||
export declare const METRICS_MAX_ITEM_LENGTH = 1024; | ||
export declare const METRICS_PAYLOAD_IGNORED: { | ||
readonly error: "ignored"; | ||
}; | ||
export declare const METRICS_PAYLOAD_SIZE_ERR: { | ||
readonly error: "size"; | ||
}; | ||
/** | ||
@@ -28,0 +36,0 @@ * Metrics tracked by Instant Bandit by default |
@@ -1,38 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.DefaultMetrics = exports.MAX_STORAGE_VALUE_LENGTH = exports.MAX_STORAGE_KEY_LENGTH = exports.UUID_LENGTH = exports.PROBABILITY_PRECISION = exports.DEFAULT_TIMEOUT = exports.HEADER_SESSION_ID = exports.PARAM_SELECT = exports.PARAM_TIMESTAMP = exports.VARNAME_METRICS_PATH = exports.VARNAME_SITE_PATH = exports.VARNAME_BASE_URL_PUBLIC = exports.VARNAME_BASE_URL = exports.NEXTJS_PUBLIC_PREFIX = exports.DEFAULT_METRICS_PATH = exports.DEFAULT_SITE_PATH = exports.DEFAULT_COOKIE_SETTINGS = exports.DEFAULT_BASE_URL = exports.DEFAULT_ORIGIN = exports.DEFAULT_VARIANT_NAME = exports.DEFAULT_VARIANT_ID = exports.DEFAULT_EXPERIMENT_NAME = exports.DEFAULT_EXPERIMENT_ID = exports.DEFAULT_SITE_NAME = exports.DEFAULT_SITE_ID = exports.DEFAULT_NAME = void 0; | ||
exports.DEFAULT_NAME = "default"; | ||
exports.DEFAULT_SITE_ID = exports.DEFAULT_NAME; | ||
exports.DEFAULT_SITE_NAME = exports.DEFAULT_NAME; | ||
exports.DEFAULT_EXPERIMENT_ID = exports.DEFAULT_NAME; | ||
exports.DEFAULT_EXPERIMENT_NAME = exports.DEFAULT_NAME; | ||
exports.DEFAULT_VARIANT_ID = exports.DEFAULT_NAME; | ||
exports.DEFAULT_VARIANT_NAME = exports.DEFAULT_NAME; | ||
exports.DEFAULT_ORIGIN = "localhost"; | ||
exports.DEFAULT_BASE_URL = "http://localhost:3000"; | ||
exports.DEFAULT_COOKIE_SETTINGS = "Path=/; SameSite=Strict; Max-Age=2147483647; HttpOnly"; | ||
exports.DEFAULT_SITE_PATH = "api/sites"; | ||
exports.DEFAULT_METRICS_PATH = "api/metrics"; | ||
// Any env vars prefixed with this are exposed to the browser | ||
// See: https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser | ||
exports.NEXTJS_PUBLIC_PREFIX = "NEXT_PUBLIC_"; | ||
exports.VARNAME_BASE_URL = "IB_BASE_API_URL"; | ||
exports.VARNAME_BASE_URL_PUBLIC = exports.NEXTJS_PUBLIC_PREFIX + exports.VARNAME_BASE_URL; | ||
exports.VARNAME_SITE_PATH = "DEFAULT_SITE_PATH"; | ||
exports.VARNAME_METRICS_PATH = "DEFAULT_METRICS_PATH"; | ||
exports.PARAM_TIMESTAMP = "ts"; | ||
exports.PARAM_SELECT = "select"; | ||
exports.HEADER_SESSION_ID = "ibsession"; | ||
exports.DEFAULT_TIMEOUT = 1000; | ||
exports.PROBABILITY_PRECISION = 4; | ||
exports.UUID_LENGTH = 36; | ||
exports.MAX_STORAGE_KEY_LENGTH = 256; | ||
exports.MAX_STORAGE_VALUE_LENGTH = 1024; | ||
/** | ||
* Metrics tracked by Instant Bandit by default | ||
*/ | ||
var DefaultMetrics; | ||
(function (DefaultMetrics) { | ||
DefaultMetrics["EXPOSURES"] = "exposures"; | ||
DefaultMetrics["CONVERSIONS"] = "conversions"; | ||
})(DefaultMetrics = exports.DefaultMetrics || (exports.DefaultMetrics = {})); | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.DefaultMetrics=exports.METRICS_PAYLOAD_SIZE_ERR=exports.METRICS_PAYLOAD_IGNORED=exports.METRICS_MAX_ITEM_LENGTH=exports.METRICS_MAX_LENGTH=exports.MAX_STORAGE_VALUE_LENGTH=exports.MAX_STORAGE_KEY_LENGTH=exports.UUID_LENGTH=exports.PROBABILITY_PRECISION=exports.DEFAULT_TIMEOUT=exports.HEADER_SESSION_ID=exports.PARAM_SELECT=exports.PARAM_TIMESTAMP=exports.VARNAME_METRICS_PATH=exports.VARNAME_SITE_PATH=exports.VARNAME_BASE_URL_PUBLIC=exports.VARNAME_BASE_URL=exports.NEXTJS_PUBLIC_PREFIX=exports.DEFAULT_METRICS_PATH=exports.DEFAULT_SITE_PATH=exports.DEFAULT_COOKIE_SETTINGS=exports.DEFAULT_BASE_URL=exports.DEFAULT_ORIGIN=exports.DEFAULT_VARIANT_NAME=exports.DEFAULT_VARIANT_ID=exports.DEFAULT_EXPERIMENT_NAME=exports.DEFAULT_EXPERIMENT_ID=exports.DEFAULT_SITE_NAME=exports.DEFAULT_SITE_ID=exports.DEFAULT_NAME=void 0,exports.DEFAULT_NAME="default",exports.DEFAULT_SITE_ID=exports.DEFAULT_NAME,exports.DEFAULT_SITE_NAME=exports.DEFAULT_NAME,exports.DEFAULT_EXPERIMENT_ID=exports.DEFAULT_NAME,exports.DEFAULT_EXPERIMENT_NAME=exports.DEFAULT_NAME,exports.DEFAULT_VARIANT_ID=exports.DEFAULT_NAME,exports.DEFAULT_VARIANT_NAME=exports.DEFAULT_NAME,exports.DEFAULT_ORIGIN="localhost",exports.DEFAULT_BASE_URL="http://localhost:3000",exports.DEFAULT_COOKIE_SETTINGS="Path=/; SameSite=Strict; Max-Age=2147483647; HttpOnly",exports.DEFAULT_SITE_PATH="api/sites",exports.DEFAULT_METRICS_PATH="api/metrics",exports.NEXTJS_PUBLIC_PREFIX="NEXT_PUBLIC_",exports.VARNAME_BASE_URL="IB_BASE_API_URL",exports.VARNAME_BASE_URL_PUBLIC=exports.NEXTJS_PUBLIC_PREFIX+exports.VARNAME_BASE_URL,exports.VARNAME_SITE_PATH="DEFAULT_SITE_PATH",exports.VARNAME_METRICS_PATH="DEFAULT_METRICS_PATH",exports.PARAM_TIMESTAMP="ts",exports.PARAM_SELECT="select",exports.HEADER_SESSION_ID="ibsession",exports.DEFAULT_TIMEOUT=1e3,exports.PROBABILITY_PRECISION=4,exports.UUID_LENGTH=36,exports.MAX_STORAGE_KEY_LENGTH=256,exports.MAX_STORAGE_VALUE_LENGTH=1024,exports.METRICS_MAX_LENGTH=524288,exports.METRICS_MAX_ITEM_LENGTH=1024,exports.METRICS_PAYLOAD_IGNORED={error:"ignored"},exports.METRICS_PAYLOAD_SIZE_ERR={error:"size"},function(a){a.EXPOSURES="exposures",a.CONVERSIONS="conversions"}(exports.DefaultMetrics||(exports.DefaultMetrics={})) |
import React from "react"; | ||
import { InstantBanditOptions, MetricsProvider, SessionProvider, SiteProvider } from "./types"; | ||
import * as constants from "./constants"; | ||
import { InstantBanditOptions, Metric, MetricsProvider, SessionProvider, SiteProvider } from "./types"; | ||
import { Experiment, Site, Variant } from "./models"; | ||
@@ -13,5 +14,7 @@ export interface InstantBanditContext { | ||
session: SessionProvider; | ||
init: (site: Site) => Promise<Site>; | ||
load: (siteName?: string, variant?: string) => Promise<Site>; | ||
select: (variant?: Variant | string) => Promise<Site>; | ||
init(site: Site): Promise<Site>; | ||
load(siteName?: string, variant?: string): Promise<Site>; | ||
select(variant?: Variant | string): Promise<Site>; | ||
incrementMetric(metric: constants.DefaultMetrics | string): any; | ||
recordEvent(name: string, payload: Metric): any; | ||
} | ||
@@ -18,0 +21,0 @@ export declare function createBanditContext(options?: Partial<InstantBanditOptions>): InstantBanditContext; |
@@ -1,68 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.InstantBanditContext = exports.DEFAULT_BANDIT_OPTIONS = exports.mergeBanditOptions = exports.createBanditContext = void 0; | ||
var tslib_1 = require("tslib"); | ||
var react_1 = tslib_1.__importDefault(require("react")); | ||
var constants = tslib_1.__importStar(require("./constants")); | ||
var session_1 = require("./providers/session"); | ||
var site_1 = require("./providers/site"); | ||
var site_2 = require("./providers/site"); | ||
var metrics_1 = require("./providers/metrics"); | ||
function createBanditContext(options) { | ||
var _this = this; | ||
var appliedOptions = mergeBanditOptions(exports.DEFAULT_BANDIT_OPTIONS, options !== null && options !== void 0 ? options : {}); | ||
var providers = appliedOptions.providers; | ||
var loader = providers.loader(appliedOptions); | ||
var metrics = providers.metrics(appliedOptions); | ||
var session = providers.session(appliedOptions); | ||
var ctx = { | ||
origin: typeof location !== "undefined" ? location.origin : constants.DEFAULT_ORIGIN, | ||
config: appliedOptions, | ||
loader: loader, | ||
metrics: metrics, | ||
session: session, | ||
get site() { return loader.model; }, | ||
get experiment() { return loader.experiment; }, | ||
get variant() { return loader.variant; }, | ||
load: function (variant) { return tslib_1.__awaiter(_this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, loader.load(ctx, variant)]; | ||
case 1: return [2 /*return*/, _a.sent()]; | ||
} | ||
}); | ||
}); }, | ||
init: function (site, select) { return tslib_1.__awaiter(_this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: return [4 /*yield*/, loader.init(ctx, site, select)]; | ||
case 1: return [2 /*return*/, _a.sent()]; | ||
} | ||
}); | ||
}); }, | ||
select: function (variant) { return tslib_1.__awaiter(_this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
loader.select(ctx, variant); | ||
session.persistVariant(ctx, loader.experiment.id, loader.variant.name); | ||
return [2 /*return*/, loader.model]; | ||
}); | ||
}); }, | ||
}; | ||
return ctx; | ||
} | ||
exports.createBanditContext = createBanditContext; | ||
function mergeBanditOptions(a, b) { | ||
var providersA = a.providers; | ||
var providersB = b.providers; | ||
var providers = Object.assign({}, providersA, providersB); | ||
var merged = Object.assign({}, a, b, { providers: providers }); | ||
return Object.freeze(merged); | ||
} | ||
exports.mergeBanditOptions = mergeBanditOptions; | ||
exports.DEFAULT_BANDIT_OPTIONS = tslib_1.__assign(tslib_1.__assign(tslib_1.__assign({}, site_2.DEFAULT_SITE_PROVIDER_OPTIONS), metrics_1.DEFAULT_METRICS_SINK_OPTIONS), { providers: { | ||
loader: function (options) { return (0, site_1.getSiteProvider)(options); }, | ||
session: function (options) { return (0, session_1.getLocalStorageSessionProvider)(options); }, | ||
metrics: function (options) { return (0, metrics_1.getHttpMetricsSink)(options); }, | ||
} }); | ||
Object.freeze(exports.DEFAULT_BANDIT_OPTIONS); | ||
exports.InstantBanditContext = react_1.default.createContext(createBanditContext()); | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.InstantBanditContext=exports.DEFAULT_BANDIT_OPTIONS=exports.mergeBanditOptions=exports.createBanditContext=void 0;var a=require("tslib"),c=a.__importDefault(require("react")),g=a.__importStar(require("./constants")),h=require("./providers/session"),i=require("./providers/site"),d=require("./providers/site"),e=require("./providers/metrics");function b(e){var b=f(exports.DEFAULT_BANDIT_OPTIONS,null!=e?e:{}),d=b.providers,c=d.loader(b),h=d.metrics(b),i=d.session(b),j={origin:"undefined"!=typeof location?location.origin:g.DEFAULT_ORIGIN,config:b,loader:c,metrics:h,session:i,get site(){return c.model},get experiment(){return c.experiment},get variant(){return c.variant},load:function(b){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){switch(a.label){case 0:return[4,c.load(j,b)];case 1:return[2,a.sent()]}})})},init:function(b,d){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){switch(a.label){case 0:return[4,c.init(j,b,d)];case 1:return[2,a.sent()]}})})},select:function(b){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return c.select(j,b),i.persistVariant(j,c.experiment.id,c.variant.name),[2,c.model]})})},incrementMetric:function(a){var b={ts:new Date().getTime(),name:a};h.sink(j,b)},recordEvent:function(a,b){var c={ts:new Date().getTime(),name:"evt.component.".concat(a),payload:b};h.sink(j,c)}};return j}function f(a,b){var e,c=Object.assign({},a.providers,b.providers),d=Object.assign({},a,b,{providers:c});return Object.freeze(d)}exports.createBanditContext=b,exports.mergeBanditOptions=f,exports.DEFAULT_BANDIT_OPTIONS=a.__assign(a.__assign(a.__assign({},d.DEFAULT_SITE_PROVIDER_OPTIONS),e.DEFAULT_METRICS_SINK_OPTIONS),{providers:{loader:function(a){return(0,i.getSiteProvider)(a)},session:function(a){return(0,h.getLocalStorageSessionProvider)(a)},metrics:function(a){return(0,e.getHttpMetricsSink)(a)}}}),Object.freeze(exports.DEFAULT_BANDIT_OPTIONS),exports.InstantBanditContext=c.default.createContext(b()) |
@@ -1,38 +0,1 @@ | ||
"use strict"; | ||
var _a; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.DEFAULT_METRICS = exports.DEFAULT_VARIANT = exports.DEFAULT_EXPERIMENT = exports.DEFAULT_SITE = exports.DEFAULT_OPTIONS = void 0; | ||
var tslib_1 = require("tslib"); | ||
var constants = tslib_1.__importStar(require("./constants")); | ||
var utils_1 = require("./utils"); | ||
exports.DEFAULT_OPTIONS = { | ||
baseUrl: (_a = (0, utils_1.env)(constants.VARNAME_BASE_URL)) !== null && _a !== void 0 ? _a : constants.DEFAULT_BASE_URL, | ||
}; | ||
/** | ||
* | ||
* This file defines the built-in default model and is used as a fallback, | ||
* as well as the template for new sites. | ||
* | ||
* It represents the invariant, baseline version of a new or existing site. | ||
* | ||
*/ | ||
var DEFAULT_ORIGIN = constants.DEFAULT_ORIGIN, DEFAULT_SITE_NAME = constants.DEFAULT_SITE_NAME, DEFAULT_EXPERIMENT_ID = constants.DEFAULT_EXPERIMENT_ID, DEFAULT_EXPERIMENT_NAME = constants.DEFAULT_EXPERIMENT_NAME, DEFAULT_VARIANT_NAME = constants.DEFAULT_VARIANT_NAME; | ||
exports.DEFAULT_SITE = (0, utils_1.deepFreeze)({ | ||
name: DEFAULT_SITE_NAME, | ||
origin: DEFAULT_ORIGIN, | ||
experiments: [{ | ||
id: DEFAULT_EXPERIMENT_ID, | ||
name: DEFAULT_EXPERIMENT_NAME, | ||
pValue: 1, | ||
metrics: {}, | ||
variants: [{ | ||
name: DEFAULT_VARIANT_NAME, | ||
prob: 1, | ||
metrics: {}, | ||
props: {}, | ||
}], | ||
}], | ||
}); | ||
exports.DEFAULT_EXPERIMENT = exports.DEFAULT_SITE.experiments[0]; | ||
exports.DEFAULT_VARIANT = exports.DEFAULT_EXPERIMENT.variants[0]; | ||
exports.DEFAULT_METRICS = exports.DEFAULT_VARIANT.metrics; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.DEFAULT_METRICS=exports.DEFAULT_VARIANT=exports.DEFAULT_EXPERIMENT=exports.DEFAULT_SITE=exports.DEFAULT_OPTIONS=void 0;var b,a=require("tslib").__importStar(require("./constants")),c=require("./utils");exports.DEFAULT_OPTIONS={baseUrl:null!==(b=(0,c.env)(a.VARNAME_BASE_URL))&& void 0!==b?b:a.DEFAULT_BASE_URL};var d=a.DEFAULT_ORIGIN,e=a.DEFAULT_SITE_NAME,f=a.DEFAULT_EXPERIMENT_ID,g=a.DEFAULT_EXPERIMENT_NAME,h=a.DEFAULT_VARIANT_NAME;exports.DEFAULT_SITE=(0,c.deepFreeze)({name:e,origin:d,experiments:[{id:f,name:g,pValue:1,metrics:{},variants:[{name:h,prob:1,metrics:{},props:{}}]}]}),exports.DEFAULT_EXPERIMENT=exports.DEFAULT_SITE.experiments[0],exports.DEFAULT_VARIANT=exports.DEFAULT_EXPERIMENT.variants[0],exports.DEFAULT_METRICS=exports.DEFAULT_VARIANT.metrics |
@@ -1,12 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.useInstantBandit = void 0; | ||
var react_1 = require("react"); | ||
var contexts_1 = require("./contexts"); | ||
/** | ||
* Gets access to the Instant Bandit API and the current site/experiment/variant | ||
*/ | ||
function useInstantBandit() { | ||
return (0, react_1.useContext)(contexts_1.InstantBanditContext); | ||
} | ||
exports.useInstantBandit = useInstantBandit; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.useInstantBandit=void 0;var a=require("react"),b=require("./contexts");exports.useInstantBandit=function(){return(0,a.useContext)(b.InstantBanditContext)} |
@@ -1,9 +0,1 @@ | ||
"use strict"; | ||
/** | ||
* Domain Models | ||
* | ||
* These models formalize a language that Instant Bandit clients and servers can speak. | ||
* Types suffixed in `Meta` represent additional server-side info. | ||
* | ||
*/ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}) |
@@ -1,154 +0,1 @@ | ||
"use strict"; | ||
var _a; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.sendBatchViaFetch = exports.sendBatchViaBeacon = exports.getHttpMetricsSink = exports.DEFAULT_METRICS_SINK_OPTIONS = void 0; | ||
var tslib_1 = require("tslib"); | ||
var constants = tslib_1.__importStar(require("../constants")); | ||
var defaults_1 = require("../defaults"); | ||
var utils_1 = require("../utils"); | ||
exports.DEFAULT_METRICS_SINK_OPTIONS = tslib_1.__assign(tslib_1.__assign({}, defaults_1.DEFAULT_OPTIONS), { metricsPath: (_a = (0, utils_1.env)(constants.VARNAME_METRICS_PATH)) !== null && _a !== void 0 ? _a : constants.DEFAULT_METRICS_PATH, batchSize: 10, flushInterval: 100 }); | ||
function getHttpMetricsSink(initOptions) { | ||
var options = Object.assign({}, exports.DEFAULT_METRICS_SINK_OPTIONS, initOptions); | ||
var items = []; | ||
var flushTimer = null; | ||
var provider = { | ||
get pending() { return items.length; }, | ||
sink: function (ctx, sample, forceFlush) { | ||
if (forceFlush === void 0) { forceFlush = false; } | ||
items.push(sample); | ||
if (forceFlush) { | ||
return provider.flush(ctx); | ||
} | ||
else { | ||
provider.scheduleFlush(ctx); | ||
} | ||
}, | ||
sinkEvent: function (ctx, eventName, payload, forceFlush) { | ||
if (forceFlush === void 0) { forceFlush = false; } | ||
try { | ||
var evt = { | ||
ts: new Date().getTime(), | ||
name: eventName, | ||
payload: payload, | ||
}; | ||
provider.sink(ctx, evt, forceFlush); | ||
} | ||
catch (err) { | ||
console.warn("[IB] Error sinking event: ".concat(err)); | ||
} | ||
}, | ||
scheduleFlush: function (ctx) { | ||
if (flushTimer) { | ||
return; | ||
} | ||
flushTimer = setTimeout(function () { | ||
return provider | ||
.flush(ctx) | ||
.catch(function (err) { return console.warn(err); }); | ||
}, options.flushInterval); | ||
}, | ||
flush: function (ctx, flushAll) { | ||
var _a, _b; | ||
if (flushAll === void 0) { flushAll = false; } | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var baseUrl, batchSize, metricsPath, session, site, experiment, variant, count, entries, sessionId, batch, url, session_1, err_1; | ||
return tslib_1.__generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
baseUrl = options.baseUrl, batchSize = options.batchSize, metricsPath = options.metricsPath; | ||
session = ctx.session, site = ctx.site, experiment = ctx.experiment, variant = ctx.variant; | ||
// No where to send to? Discard. | ||
if (!(0, utils_1.exists)(metricsPath) || metricsPath.trim() === "") { | ||
console.debug("[IB] No metrics path configured"); | ||
items.splice(0, items.length); | ||
return [2 /*return*/]; | ||
} | ||
count = flushAll | ||
? items.length | ||
: Math.min(items.length, batchSize); | ||
entries = items.slice(0, count); | ||
sessionId = (_b = (_a = session.id) !== null && _a !== void 0 ? _a : (0, utils_1.getCookie)(constants.HEADER_SESSION_ID)) !== null && _b !== void 0 ? _b : ""; | ||
batch = { | ||
site: site.name, | ||
experiment: experiment.id, | ||
variant: variant.name, | ||
entries: entries, | ||
}; | ||
url = new URL(metricsPath, baseUrl); | ||
_c.label = 1; | ||
case 1: | ||
_c.trys.push([1, 5, 6, 7]); | ||
if (!(flushAll && | ||
typeof navigator !== "undefined" && | ||
typeof navigator.sendBeacon !== "undefined")) return [3 /*break*/, 2]; | ||
sendBatchViaBeacon(url, batch); | ||
return [3 /*break*/, 4]; | ||
case 2: return [4 /*yield*/, sendBatchViaFetch(url, sessionId, batch)]; | ||
case 3: | ||
session_1 = _c.sent(); | ||
if ((0, utils_1.exists)(session_1.sid)) { | ||
ctx.session.save(ctx, session_1); | ||
} | ||
_c.label = 4; | ||
case 4: return [3 /*break*/, 7]; | ||
case 5: | ||
err_1 = _c.sent(); | ||
console.warn("Error occurred while flushing metrics: ".concat(err_1)); | ||
return [3 /*break*/, 7]; | ||
case 6: | ||
items.splice(0, count); | ||
if (flushTimer) { | ||
clearTimeout(flushTimer); | ||
flushTimer = null; | ||
} | ||
if (items.length > 0) { | ||
provider.scheduleFlush(ctx); | ||
} | ||
return [7 /*endfinally*/]; | ||
case 7: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
}; | ||
return provider; | ||
} | ||
exports.getHttpMetricsSink = getHttpMetricsSink; | ||
// Note: sendBeacon is synchronous (fire and forget), and will return `true` even in the case server error | ||
function sendBatchViaBeacon(url, batch) { | ||
var blob = new Blob([JSON.stringify(batch)], { | ||
type: "application/json; charset=UTF-8", | ||
}); | ||
return navigator.sendBeacon(url + "", blob); | ||
} | ||
exports.sendBatchViaBeacon = sendBatchViaBeacon; | ||
function sendBatchViaFetch(url, sessionId, batch) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var resp, status, statusText, session; | ||
var _a; | ||
return tslib_1.__generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: return [4 /*yield*/, fetch(url.toString(), { | ||
method: "POST", | ||
headers: (_a = { | ||
"Accept": "application/json", | ||
"Content-Type": "application/json" | ||
}, | ||
_a[constants.HEADER_SESSION_ID] = sessionId, | ||
_a), | ||
body: JSON.stringify(batch), | ||
})]; | ||
case 1: | ||
resp = _b.sent(); | ||
status = resp.status, statusText = resp.statusText; | ||
if (!(status === 200)) return [3 /*break*/, 3]; | ||
return [4 /*yield*/, resp.json()]; | ||
case 2: | ||
session = _b.sent(); | ||
return [2 /*return*/, session]; | ||
case 3: return [2 /*return*/, { status: status, statusText: statusText }]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.sendBatchViaFetch = sendBatchViaFetch; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.sendBatchViaFetch=exports.sendBatchViaBeacon=exports.getHttpMetricsSink=exports.DEFAULT_METRICS_SINK_OPTIONS=void 0;var a,b=require("tslib"),c=b.__importStar(require("../constants")),d=require("../defaults"),e=require("../utils");function f(a,b){var c=new Blob([(0,e.encodeMetricsBatch)(b)],{type:"application/text; charset=UTF-8"});return navigator.sendBeacon(a+"",c)}function g(a,d,f){return b.__awaiter(this,void 0,void 0,function(){var g,h,i,j,k;return b.__generator(this,function(b){switch(b.label){case 0:return[4,fetch(a.toString(),{method:"POST",headers:((k={Accept:"application/json","Content-Type":"text/plain; charset=UTF-8"})[c.HEADER_SESSION_ID]=d,k),body:(0,e.encodeMetricsBatch)(f)})];case 1:if(h=(g=b.sent()).status,i=g.statusText,200!==h)return[3,3];return[4,g.json()];case 2:return[2,j=b.sent()];case 3:return[2,{status:h,statusText:i}]}})})}exports.DEFAULT_METRICS_SINK_OPTIONS=b.__assign(b.__assign({},d.DEFAULT_OPTIONS),{metricsPath:null!==(a=(0,e.env)(c.VARNAME_METRICS_PATH))&& void 0!==a?a:c.DEFAULT_METRICS_PATH,batchSize:10,flushInterval:100}),exports.getHttpMetricsSink=function(a){var i=Object.assign({},exports.DEFAULT_METRICS_SINK_OPTIONS,a),d=[],j=null,h={get pending(){return d.length},sink:function(b,c,a){if(void 0===a&&(a=!1),d.push(c),a)return h.flush(b);h.scheduleFlush(b)},sinkEvent:function(b,c,d,a){void 0===a&&(a=!1);try{var e={ts:new Date().getTime(),name:c,payload:d};h.sink(b,e,a)}catch(f){console.warn("[IB] Error sinking event: ".concat(f))}},scheduleFlush:function(a){!j&&(j=setTimeout(function(){return h.flush(a).catch(function(a){return console.warn(a)})},i.flushInterval))},flush:function(k,a){var l,m;return void 0===a&&(a=!1),b.__awaiter(this,void 0,void 0,function(){var n,o,p,q,r,s,t,u,v,w,x,y,z,A;return b.__generator(this,function(b){switch(b.label){case 0:if(n=i.baseUrl,o=i.batchSize,p=i.metricsPath,q=k.session,r=k.site,s=k.experiment,t=k.variant,!(0,e.exists)(p)||""===p.trim())return console.debug("[IB] No metrics path configured"),d.splice(0,d.length),[2];u=a?d.length:Math.min(d.length,o),v=d.slice(0,u),w=null!==(m=null!==(l=q.id)&& void 0!==l?l:(0,e.getCookie)(c.HEADER_SESSION_ID))&& void 0!==m?m:"",x={site:r.name,experiment:s.id,variant:t.name,entries:v},y=new URL(p,n),b.label=1;case 1:if(b.trys.push([1,5,6,7]),!(a&&"undefined"!=typeof navigator&& void 0!==navigator.sendBeacon))return[3,2];return f(y,x),[3,4];case 2:return[4,g(y,w,x)];case 3:z=b.sent(),(0,e.exists)(z.sid)&&k.session.save(k,z),b.label=4;case 4:return[3,7];case 5:return A=b.sent(),console.warn("Error occurred while flushing metrics: ".concat(A)),[3,7];case 6:return d.splice(0,u),j&&(clearTimeout(j),j=null),d.length>0&&h.scheduleFlush(k),[7];case 7:return[2]}})})}};return h},exports.sendBatchViaBeacon=f,exports.sendBatchViaFetch=g |
@@ -1,148 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getLocalStorageSessionProvider = exports.getLocalStorageKey = void 0; | ||
var utils_1 = require("../utils"); | ||
var defaults_1 = require("../defaults"); | ||
var constants_1 = require("../constants"); | ||
function getLocalStorageKey() { | ||
return constants_1.HEADER_SESSION_ID; | ||
} | ||
exports.getLocalStorageKey = getLocalStorageKey; | ||
function getLocalStorageSessionProvider(options) { | ||
options; | ||
var id = null; | ||
return { | ||
get id() { return id; }, | ||
/** | ||
* Gets an existing session for the given site, creating one with default | ||
* properties if it does not exist. | ||
*/ | ||
getOrCreateSession: function (ctx, props) { | ||
var session = getOrCreateSession(ctx, props); | ||
if ((0, utils_1.exists)(session.sid)) { | ||
id = session.sid; | ||
} | ||
return session; | ||
}, | ||
/** | ||
* Records a variant exposure in the session in order to show the same one next time | ||
*/ | ||
persistVariant: function (ctx, experiment, variant) { | ||
return persistVariant(ctx, experiment, variant); | ||
}, | ||
/** | ||
* Checks the session to see if a particular site/experiment/variant combo has been | ||
* presented before | ||
*/ | ||
hasSeen: function (ctx, experiment, variant) { | ||
return hasSeen(ctx, experiment, variant); | ||
}, | ||
/** | ||
* Persists a new version of the session from the server | ||
* @param ctx | ||
* @param session | ||
*/ | ||
save: function (ctx, session) { | ||
id = session.sid; | ||
var storageKey = getLocalStorageKey(); | ||
try { | ||
localStorage.setItem(storageKey, JSON.stringify(session)); | ||
} | ||
catch (err) { | ||
handlePossibleQuotaError(err); | ||
} | ||
return session; | ||
} | ||
}; | ||
} | ||
exports.getLocalStorageSessionProvider = getLocalStorageSessionProvider; | ||
function getOrCreateSession(ctx, props) { | ||
if (!utils_1.isBrowserEnvironment) { | ||
return Object.assign((0, utils_1.makeNewSession)(), props); | ||
} | ||
var site = ctx.site; | ||
if (!(0, utils_1.exists)(site)) { | ||
site = defaults_1.DEFAULT_SITE; | ||
} | ||
var storageKey = getLocalStorageKey(); | ||
var sessionJson = localStorage.getItem(storageKey); | ||
var session; | ||
if ((0, utils_1.exists)(sessionJson)) { | ||
session = JSON.parse(sessionJson); | ||
} | ||
else { | ||
session = (0, utils_1.makeNewSession)(); | ||
} | ||
if (props) { | ||
Object.assign(session, props); | ||
} | ||
try { | ||
localStorage.setItem(storageKey, JSON.stringify(session)); | ||
} | ||
catch (err) { | ||
handlePossibleQuotaError(err); | ||
} | ||
return session; | ||
} | ||
function persistVariant(ctx, experiment, variant) { | ||
if (!utils_1.isBrowserEnvironment) { | ||
return; | ||
} | ||
// Defaults are implicit, no need to have them persisted | ||
if (experiment === defaults_1.DEFAULT_EXPERIMENT.id && variant === defaults_1.DEFAULT_VARIANT.name) { | ||
return; | ||
} | ||
var site = ctx.site; | ||
var storageKey = getLocalStorageKey(); | ||
var session = getOrCreateSession(ctx); | ||
(0, utils_1.markVariantInSession)(session, site.name, experiment, variant); | ||
try { | ||
localStorage.setItem(storageKey, JSON.stringify(session)); | ||
} | ||
catch (err) { | ||
handlePossibleQuotaError(err); | ||
} | ||
} | ||
function handlePossibleQuotaError(err) { | ||
if (!isQuotaError(err)) { | ||
console.warn("[IB] Error updating session: ".concat(err)); | ||
} | ||
else { | ||
// NOTE: This is almost certainly a quota issue, and the shape of which is not | ||
// consistent across browsers. Safe to suppress here. | ||
console.debug("[IB] Storage quota error: ".concat(err)); | ||
} | ||
} | ||
/** | ||
* Examines an error to see if it's a quota error from local/session storage | ||
*/ | ||
function isQuotaError(err) { | ||
if (!err) { | ||
return false; | ||
} | ||
if (!(0, utils_1.exists)(err.code)) { | ||
return false; | ||
} | ||
else { | ||
switch (err.code) { | ||
// Proper DOM code in most modern browsers | ||
case 22: | ||
return true; | ||
// Firefox | ||
case 1014: | ||
return err.name === "NS_ERROR_DOM_QUOTA_REACHED"; | ||
default: | ||
return false; | ||
} | ||
} | ||
} | ||
function hasSeen(ctx, experiment, variant) { | ||
var _a, _b; | ||
if (!utils_1.isBrowserEnvironment) { | ||
return false; | ||
} | ||
var site = ctx.site; | ||
var session = getOrCreateSession(ctx); | ||
var seenVariants = (_b = (_a = session.selections) === null || _a === void 0 ? void 0 : _a[site.name]) === null || _b === void 0 ? void 0 : _b[experiment]; | ||
return (0, utils_1.exists)(seenVariants) && (0, utils_1.exists)(seenVariants.find(function (v) { return v === variant; })); | ||
} | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getLocalStorageSessionProvider=exports.getLocalStorageKey=void 0;var b=require("../utils"),c=require("../defaults"),d=require("../constants");function a(){return d.HEADER_SESSION_ID}function e(j,e){if(!b.isBrowserEnvironment)return Object.assign((0,b.makeNewSession)(),e);var d,g=j.site;(0,b.exists)(g)||(g=c.DEFAULT_SITE);var h=a(),i=localStorage.getItem(h);d=(0,b.exists)(i)?JSON.parse(i):(0,b.makeNewSession)(),e&&Object.assign(d,e);try{localStorage.setItem(h,JSON.stringify(d))}catch(k){f(k)}return d}exports.getLocalStorageKey=a;function f(a){g(a)?console.debug("[IB] Storage quota error: ".concat(a)):console.warn("[IB] Error updating session: ".concat(a))}function g(a){if(!a||!(0,b.exists)(a.code))return!1;switch(a.code){case 22:return!0;case 1014:return"NS_ERROR_DOM_QUOTA_REACHED"===a.name;default:return!1}}exports.getLocalStorageSessionProvider=function(g){var d=null;return{get id(){return d},getOrCreateSession:function(c,f){var a=e(c,f);return(0,b.exists)(a.sid)&&(d=a.sid),a},persistVariant:function(d,g,h){return function m(d,g,h){if(b.isBrowserEnvironment&&(g!==c.DEFAULT_EXPERIMENT.id||h!==c.DEFAULT_VARIANT.name)){var j=d.site,k=a(),i=e(d);(0,b.markVariantInSession)(i,j.name,g,h);try{localStorage.setItem(k,JSON.stringify(i))}catch(l){f(l)}}}(d,g,h)},hasSeen:function(a,c,d){return function j(d,g,k){if(!b.isBrowserEnvironment)return!1;var a,c,h=d.site,i=e(d),f=null===(c=null===(a=i.selections)|| void 0===a?void 0:a[h.name])|| void 0===c?void 0:c[g];return(0,b.exists)(f)&&(0,b.exists)(f.find(function(a){return a===k}))}(a,c,d)},save:function(g,b){d=b.sid;var c=a();try{localStorage.setItem(c,JSON.stringify(b))}catch(e){f(e)}return b}}} |
@@ -1,273 +0,1 @@ | ||
"use strict"; | ||
var _a; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getDefaultExperiment = exports.getActiveExperiment = exports.balanceProbabilities = exports.selectWithProbabilities = exports.getSiteProvider = exports.DEFAULT_SITE_PROVIDER_OPTIONS = void 0; | ||
var tslib_1 = require("tslib"); | ||
var constants = tslib_1.__importStar(require("../constants")); | ||
var types_1 = require("../types"); | ||
var utils_1 = require("../utils"); | ||
var defaults_1 = require("../defaults"); | ||
exports.DEFAULT_SITE_PROVIDER_OPTIONS = tslib_1.__assign(tslib_1.__assign({}, defaults_1.DEFAULT_OPTIONS), { sitePath: (_a = (0, utils_1.env)(constants.VARNAME_SITE_PATH)) !== null && _a !== void 0 ? _a : constants.DEFAULT_SITE_PATH, appendTimestamp: false }); | ||
Object.freeze(exports.DEFAULT_SITE_PROVIDER_OPTIONS); | ||
function getSiteProvider(initOptions) { | ||
if (initOptions === void 0) { initOptions = {}; } | ||
var options = Object.assign({}, exports.DEFAULT_SITE_PROVIDER_OPTIONS, initOptions); | ||
Object.freeze(options); | ||
var state = types_1.LoadState.PRELOAD; | ||
var error = null; | ||
var site = defaults_1.DEFAULT_SITE; | ||
var experiment = defaults_1.DEFAULT_EXPERIMENT; | ||
var variant = defaults_1.DEFAULT_VARIANT; | ||
var provider = { | ||
get error() { return error; }, | ||
get origin() { | ||
if (utils_1.isBrowserEnvironment) { | ||
return location.origin; | ||
} | ||
else { | ||
return constants.DEFAULT_ORIGIN; | ||
} | ||
}, | ||
get model() { return site; }, | ||
get experiment() { return experiment; }, | ||
get variant() { return variant; }, | ||
get state() { return state; }, | ||
load: function (ctx, siteName, variant) { | ||
if (siteName === void 0) { siteName = defaults_1.DEFAULT_SITE.name; } | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var siteUrl, baseUrl, sitePath, url, resp, siteJson, err_1; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
siteUrl = ""; | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 5, 7, 8]); | ||
baseUrl = options.baseUrl, sitePath = options.sitePath; | ||
url = new URL([sitePath, siteName].join("/"), baseUrl); | ||
siteUrl = url.toString(); | ||
if (options.appendTimestamp === true) { | ||
url.searchParams.append(constants.PARAM_TIMESTAMP, new Date().getTime() + ""); | ||
} | ||
siteUrl = url.toString(); | ||
state = types_1.LoadState.WAIT; | ||
return [4 /*yield*/, fetch(siteUrl)]; | ||
case 2: | ||
resp = _a.sent(); | ||
return [4 /*yield*/, resp.json()]; | ||
case 3: | ||
siteJson = _a.sent(); | ||
return [4 /*yield*/, provider.init(ctx, siteJson, variant)]; | ||
case 4: | ||
site = _a.sent(); | ||
return [3 /*break*/, 8]; | ||
case 5: | ||
err_1 = _a.sent(); | ||
error = err_1; | ||
console.warn("[IB] An error occurred while loading from '".concat(siteUrl, "': ").concat(err_1, ". Default site will be used.")); | ||
// Re-init w/ builtins | ||
return [4 /*yield*/, provider.init(ctx, defaults_1.DEFAULT_SITE, variant)]; | ||
case 6: | ||
// Re-init w/ builtins | ||
_a.sent(); | ||
return [3 /*break*/, 8]; | ||
case 7: | ||
state = types_1.LoadState.READY; | ||
return [7 /*endfinally*/]; | ||
case 8: return [2 /*return*/, site]; | ||
} | ||
}); | ||
}); | ||
}, | ||
/** | ||
* Initializes from a site object provided locally | ||
* @param ctx | ||
* @param site | ||
* @param select | ||
* @returns | ||
*/ | ||
init: function (ctx, siteArg, select) { | ||
try { | ||
if (!siteArg || typeof siteArg !== "object") { | ||
throw new Error("Invalid site configuration"); | ||
} | ||
error = null; | ||
state = types_1.LoadState.SELECTING; | ||
site = Object.assign({}, siteArg); | ||
var _a = provider.select(ctx, select), selectedExperiment = _a.experiment, selectedVariant = _a.variant; | ||
experiment = Object.assign({}, selectedExperiment); | ||
variant = Object.assign({}, selectedVariant); | ||
state = types_1.LoadState.READY; | ||
return site; | ||
} | ||
catch (err) { | ||
error = err; | ||
site = defaults_1.DEFAULT_SITE; | ||
experiment = defaults_1.DEFAULT_EXPERIMENT; | ||
variant = defaults_1.DEFAULT_VARIANT; | ||
console.warn("[IB] Error initializing. Default site will be used. Error was: ".concat(err)); | ||
} | ||
finally { | ||
state = types_1.LoadState.READY; | ||
} | ||
// Just to be safe | ||
if (!site) { | ||
site = defaults_1.DEFAULT_SITE; | ||
experiment = defaults_1.DEFAULT_EXPERIMENT; | ||
variant = defaults_1.DEFAULT_VARIANT; | ||
} | ||
return site; | ||
}, | ||
select: function (ctx, selectVariant) { | ||
var _a, _b, _c, _d, _e; | ||
try { | ||
// Selection precedence: | ||
// 1. Explicit (i.e. specified on props) | ||
// 2. Specified in site object (via the "select" field) | ||
// 3. Session (display previously presented variant) | ||
// 4. Probabilistic (use probabilities computed on the server) | ||
// 5. Fallback to default variant in configured experiment (should one exist) | ||
// 6. Fallback to default variant in builtin experiment | ||
var selection = (_a = selectVariant !== null && selectVariant !== void 0 ? selectVariant : site.select) !== null && _a !== void 0 ? _a : undefined; | ||
var experiment_1 = (_c = (_b = getActiveExperiment(site, selection)) !== null && _b !== void 0 ? _b : getDefaultExperiment(site)) !== null && _c !== void 0 ? _c : defaults_1.DEFAULT_EXPERIMENT; | ||
var variant_1 = null; | ||
var session = ctx.session; | ||
if ((0, utils_1.exists)(selection) && selection) { | ||
var result = provider.selectSpecific(experiment_1, selection); | ||
experiment_1 = result.experiment; | ||
variant_1 = result.variant; | ||
} | ||
else if (session) { | ||
var userSession = session.getOrCreateSession(ctx); | ||
var selections = userSession.selections; | ||
var selectedSite = selections[site.name]; | ||
var mostRecentSeenVariant_1 = (_d = selectedSite === null || selectedSite === void 0 ? void 0 : selectedSite[experiment_1.id]) === null || _d === void 0 ? void 0 : _d.slice().reverse()[0]; | ||
variant_1 = (_e = experiment_1.variants.find(function (v) { return v.name === mostRecentSeenVariant_1; })) !== null && _e !== void 0 ? _e : null; | ||
} | ||
if (!variant_1) { | ||
variant_1 = selectWithProbabilities(experiment_1); | ||
} | ||
if (!variant_1) { | ||
variant_1 = defaults_1.DEFAULT_VARIANT; | ||
} | ||
return { experiment: experiment_1, variant: variant_1 }; | ||
} | ||
catch (err) { | ||
error = err; | ||
site = defaults_1.DEFAULT_SITE; | ||
experiment = defaults_1.DEFAULT_EXPERIMENT; | ||
variant = defaults_1.DEFAULT_VARIANT; | ||
console.warn("[IB] Error encountered while selecting variant '".concat(selectVariant, "': ").concat(err)); | ||
return { experiment: defaults_1.DEFAULT_EXPERIMENT, variant: defaults_1.DEFAULT_VARIANT }; | ||
} | ||
}, | ||
/** | ||
* Selects a specific variant from the given experiment. | ||
* If it does not exist, the variant will be searched for in a configured default experiment. | ||
* If no variant match is found, the default variant in the default experiment is returned. | ||
* @param experiment | ||
* @param variant | ||
* @returns | ||
*/ | ||
selectSpecific: function (experiment, variant) { | ||
var configuredDefaultExperiment; | ||
// Check in the specified experiment | ||
var selected = experiment.variants.find(function (v) { return v.name === variant; }); | ||
if (selected) { | ||
return { experiment: experiment, variant: selected }; | ||
} | ||
else { | ||
// Check in an explicitly configured default experiment, should one exist | ||
configuredDefaultExperiment = site.experiments.find(function (e) { return e.id === defaults_1.DEFAULT_EXPERIMENT.id; }); | ||
selected = configuredDefaultExperiment === null || configuredDefaultExperiment === void 0 ? void 0 : configuredDefaultExperiment.variants.find(function (v) { return v.name === defaults_1.DEFAULT_VARIANT.name; }); | ||
} | ||
// If the variant was found in the configured default, use the default variant, configured default experiment | ||
if (configuredDefaultExperiment && !selected) { | ||
return { experiment: configuredDefaultExperiment, variant: defaults_1.DEFAULT_VARIANT }; | ||
} | ||
else if (selected) { | ||
return { experiment: experiment, variant: selected }; | ||
} | ||
else { | ||
// Otherwise, fall back to the inbuilt default experiment + variant | ||
return { experiment: defaults_1.DEFAULT_EXPERIMENT, variant: defaults_1.DEFAULT_VARIANT }; | ||
} | ||
}, | ||
}; | ||
return provider; | ||
} | ||
exports.getSiteProvider = getSiteProvider; | ||
/** | ||
* Selects a specific variant based on the probabilities expressed by the variants | ||
* in the experiment | ||
* @param experiment | ||
*/ | ||
function selectWithProbabilities(experiment) { | ||
var _a; | ||
var variants = experiment.variants; | ||
var probs = balanceProbabilities(variants); | ||
var winner = null; | ||
var rand = Math.random(); | ||
var cumulativeProb = 0.0; | ||
var sorted = Object.entries(probs).sort(function (a, b) { return a[1] - b[1]; }); | ||
var _loop_1 = function (pair) { | ||
var name_1 = pair[0], prob = pair[1]; | ||
cumulativeProb += prob; | ||
if (rand <= cumulativeProb) { | ||
winner = (_a = variants.find(function (v) { return v.name === name_1; })) !== null && _a !== void 0 ? _a : null; | ||
return "break"; | ||
} | ||
}; | ||
for (var _i = 0, sorted_1 = sorted; _i < sorted_1.length; _i++) { | ||
var pair = sorted_1[_i]; | ||
var state_1 = _loop_1(pair); | ||
if (state_1 === "break") | ||
break; | ||
} | ||
return winner; | ||
} | ||
exports.selectWithProbabilities = selectWithProbabilities; | ||
/** | ||
* Balances variant probabilities, ensuring that don't exceed 1.0. | ||
* If all probabilities are 0, gives them equal weight. | ||
* If the sum is < 1, balances them so that the sum is 1. | ||
* @param variants | ||
* @returns | ||
*/ | ||
function balanceProbabilities(variants) { | ||
var sum = variants.reduce(function (p, v) { var _a; return p += (_a = v.prob) !== null && _a !== void 0 ? _a : 0; }, 0); | ||
var results = {}; | ||
for (var _i = 0, variants_1 = variants; _i < variants_1.length; _i++) { | ||
var v = variants_1[_i]; | ||
if (!(0, utils_1.exists)(v.prob) || isNaN(v.prob) || v.prob === 0) { | ||
results[v.name] = 0; | ||
} | ||
if (sum === 0) { | ||
results[v.name] = parseFloat((1 / variants.length).toPrecision(constants.PROBABILITY_PRECISION)); | ||
} | ||
else { | ||
var ratio = 1 / sum; | ||
results[v.name] = !(0, utils_1.exists)(v.prob) ? 0 : parseFloat((v.prob * ratio).toPrecision(constants.PROBABILITY_PRECISION)); | ||
} | ||
} | ||
return results; | ||
} | ||
exports.balanceProbabilities = balanceProbabilities; | ||
function getActiveExperiment(site, variant) { | ||
var _a, _b; | ||
var experiment = ((_a = site.experiments) !== null && _a !== void 0 ? _a : []) | ||
.filter(function (exp) { return !(0, utils_1.exists)(variant) ? true : exp.variants.some(function (v) { return v.name === variant; }); }) | ||
.filter(function (exp) { return exp.inactive !== true; })[0]; | ||
// No active experiment? Look for an explicit default, falling back to the implicit default | ||
// The implicit default is always considered active if we have no other alternative | ||
if (!experiment) { | ||
experiment = (_b = site.experiments.filter(function (exp) { return exp.id === constants.DEFAULT_EXPERIMENT_ID; })) === null || _b === void 0 ? void 0 : _b[0]; | ||
} | ||
return experiment !== null && experiment !== void 0 ? experiment : null; | ||
} | ||
exports.getActiveExperiment = getActiveExperiment; | ||
function getDefaultExperiment(site) { | ||
var _a, _b; | ||
return (_b = (_a = site.experiments.filter(function (e) { return e.id === defaults_1.DEFAULT_EXPERIMENT.id; })) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : defaults_1.DEFAULT_EXPERIMENT; | ||
} | ||
exports.getDefaultExperiment = getDefaultExperiment; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getDefaultExperiment=exports.getActiveExperiment=exports.balanceProbabilities=exports.selectWithProbabilities=exports.getSiteProvider=exports.DEFAULT_SITE_PROVIDER_OPTIONS=void 0;var a,b=require("tslib"),c=b.__importStar(require("../constants")),j=require("../types"),d=require("../utils"),e=require("../defaults");function f(c){for(var i,d=c.variants,e=g(d),f=null,j=Math.random(),k=0,h=Object.entries(e).sort(function(a,b){return a[1]-b[1]}),a=0,b=h;a<b.length&&"break"!==function(a){var b=a[0];if(j<=(k+=a[1]))return f=null!==(i=d.find(function(a){return a.name===b}))&& void 0!==i?i:null,"break"}(b[a]);a++);return f}function g(e){for(var g=e.reduce(function(b,c){var a;return b+(null!==(a=c.prob)&& void 0!==a?a:0)},0),b={},f=0,h=e;f<h.length;f++){var a=h[f];if((!(0,d.exists)(a.prob)||isNaN(a.prob)||0===a.prob)&&(b[a.name]=0),0===g)b[a.name]=parseFloat((1/e.length).toPrecision(c.PROBABILITY_PRECISION));else{var i=1/g;b[a.name]=(0,d.exists)(a.prob)?parseFloat((a.prob*i).toPrecision(c.PROBABILITY_PRECISION)):0}}return b}function h(f,g){var b,e,a=(null!==(b=f.experiments)&& void 0!==b?b:[]).filter(function(a){return!(0,d.exists)(g)||a.variants.some(function(a){return a.name===g})}).filter(function(a){return!0!==a.inactive})[0];return a||(a=null===(e=f.experiments.filter(function(a){return a.id===c.DEFAULT_EXPERIMENT_ID}))|| void 0===e?void 0:e[0]),null!=a?a:null}function i(c){var a,b;return null!==(b=null===(a=c.experiments.filter(function(a){return a.id===e.DEFAULT_EXPERIMENT.id}))|| void 0===a?void 0:a[0])&& void 0!==b?b:e.DEFAULT_EXPERIMENT}exports.DEFAULT_SITE_PROVIDER_OPTIONS=b.__assign(b.__assign({},e.DEFAULT_OPTIONS),{sitePath:null!==(a=(0,d.env)(c.VARNAME_SITE_PATH))&& void 0!==a?a:c.DEFAULT_SITE_PATH,appendTimestamp:!1}),Object.freeze(exports.DEFAULT_SITE_PROVIDER_OPTIONS),exports.getSiteProvider=function(a){void 0===a&&(a={});var g=Object.assign({},exports.DEFAULT_SITE_PROVIDER_OPTIONS,a);Object.freeze(g);var k=j.LoadState.PRELOAD,l=null,m=e.DEFAULT_SITE,n=e.DEFAULT_EXPERIMENT,o=e.DEFAULT_VARIANT,p={get error(){return l},get origin(){if(d.isBrowserEnvironment)return location.origin;return c.DEFAULT_ORIGIN},get model(){return m},get experiment(){return n},get variant(){return o},get state(){return k},load:function(d,a,f){return void 0===a&&(a=e.DEFAULT_SITE.name),b.__awaiter(this,void 0,void 0,function(){var h,i,n,o,q,r,s;return b.__generator(this,function(b){switch(b.label){case 0:h="",b.label=1;case 1:return b.trys.push([1,5,7,8]),i=g.baseUrl,n=g.sitePath,h=(o=new URL([n,a].join("/"),i)).toString(),!0===g.appendTimestamp&&o.searchParams.append(c.PARAM_TIMESTAMP,new Date().getTime()+""),h=o.toString(),k=j.LoadState.WAIT,[4,fetch(h)];case 2:return[4,(q=b.sent()).json()];case 3:return r=b.sent(),[4,p.init(d,r,f)];case 4:return m=b.sent(),[3,8];case 5:return l=s=b.sent(),console.warn("[IB] An error occurred while loading from '".concat(h,"': ").concat(s,". Default site will be used.")),[4,p.init(d,e.DEFAULT_SITE,f)];case 6:return b.sent(),[3,8];case 7:return k=j.LoadState.READY,[7];case 8:return[2,m]}})})},init:function(d,a,f){try{if(!a||"object"!=typeof a)throw Error("Invalid site configuration");l=null,k=j.LoadState.SELECTING,m=Object.assign({},a);var b=p.select(d,f),g=b.experiment,h=b.variant;return n=Object.assign({},g),o=Object.assign({},h),k=j.LoadState.READY,m}catch(c){l=c,m=e.DEFAULT_SITE,n=e.DEFAULT_EXPERIMENT,o=e.DEFAULT_VARIANT,console.warn("[IB] Error initializing. Default site will be used. Error was: ".concat(c))}finally{k=j.LoadState.READY}return m||(m=e.DEFAULT_SITE,n=e.DEFAULT_EXPERIMENT,o=e.DEFAULT_VARIANT),m},select:function(t,g){var j,k,q,r,s;try{var c=null!==(j=null!=g?g:m.select)&& void 0!==j?j:void 0,b=null!==(q=null!==(k=h(m,c))&& void 0!==k?k:i(m))&& void 0!==q?q:e.DEFAULT_EXPERIMENT,a=null,u=t.session;if((0,d.exists)(c)&&c){var v=p.selectSpecific(b,c);b=v.experiment,a=v.variant}else if(u){var w=u.getOrCreateSession(t).selections[m.name],y=null===(r=null==w?void 0:w[b.id])|| void 0===r?void 0:r.slice().reverse()[0];a=null!==(s=b.variants.find(function(a){return a.name===y}))&& void 0!==s?s:null}return a||(a=f(b)),a||(a=e.DEFAULT_VARIANT),{experiment:b,variant:a}}catch(x){return l=x,m=e.DEFAULT_SITE,n=e.DEFAULT_EXPERIMENT,o=e.DEFAULT_VARIANT,console.warn("[IB] Error encountered while selecting variant '".concat(g,"': ").concat(x)),{experiment:e.DEFAULT_EXPERIMENT,variant:e.DEFAULT_VARIANT}}},selectSpecific:function(c,d){var b,a=c.variants.find(function(a){return a.name===d});return a?{experiment:c,variant:a}:(a=null==(b=m.experiments.find(function(a){return a.id===e.DEFAULT_EXPERIMENT.id}))?void 0:b.variants.find(function(a){return a.name===e.DEFAULT_VARIANT.name}),b&&!a)?{experiment:b,variant:e.DEFAULT_VARIANT}:a?{experiment:c,variant:a}:{experiment:e.DEFAULT_EXPERIMENT,variant:e.DEFAULT_VARIANT}}};return p},exports.selectWithProbabilities=f,exports.balanceProbabilities=g,exports.getActiveExperiment=h,exports.getDefaultExperiment=i |
@@ -1,142 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.DEFAULT_STATIC_SITE_BACKEND_OPTIONS = exports.scanSites = exports.getJsonSiteBackend = void 0; | ||
var tslib_1 = require("tslib"); | ||
var fs_1 = tslib_1.__importDefault(require("fs")); | ||
var path_1 = tslib_1.__importDefault(require("path")); | ||
var process_1 = require("process"); | ||
var environment_1 = tslib_1.__importDefault(require("../environment")); | ||
var defaults_1 = require("../../defaults"); | ||
var asyncfs = fs_1.default.promises; | ||
/** | ||
* Serves sites from named JSON definitions on the server | ||
*/ | ||
function getJsonSiteBackend(initOptions) { | ||
if (initOptions === void 0) { initOptions = {}; } | ||
var options = Object.assign({}, exports.DEFAULT_STATIC_SITE_BACKEND_OPTIONS, initOptions); | ||
var sites = {}; | ||
var lastScan = 0; | ||
function logError(err) { | ||
console.warn("[IB] Error scanning sites: ".concat(err)); | ||
} | ||
return { | ||
connect: function () { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var err_1; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
_a.trys.push([0, 2, , 3]); | ||
return [4 /*yield*/, scanSites(environment_1.default.IB_STATIC_SITES_PATH, sites, lastScan)]; | ||
case 1: | ||
sites = _a.sent(); | ||
return [3 /*break*/, 3]; | ||
case 2: | ||
err_1 = _a.sent(); | ||
logError(err_1); | ||
return [3 /*break*/, 3]; | ||
case 3: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}, | ||
getSiteConfig: function (req) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var siteName, refreshInterval, now, useRefreshInterval, intervalElapsed, siteNameOrDefault, site; | ||
return tslib_1.__generator(this, function (_a) { | ||
siteName = req.siteName; | ||
refreshInterval = options.refreshInterval; | ||
now = new Date().getTime(); | ||
useRefreshInterval = refreshInterval > -1; | ||
intervalElapsed = (now - lastScan > refreshInterval); | ||
if (useRefreshInterval && intervalElapsed) { | ||
// Don't hold up the show. | ||
scanSites(environment_1.default.IB_STATIC_SITES_PATH, sites, lastScan) | ||
.then(function (scanned) { return sites = scanned; }) | ||
.then(function () { return lastScan = now; }) | ||
.catch(logError); | ||
} | ||
siteNameOrDefault = siteName !== null && siteName !== void 0 ? siteName : defaults_1.DEFAULT_SITE.name; | ||
site = sites[siteNameOrDefault]; | ||
if (!site) { | ||
console.warn("[IB] Could not find site '".concat(siteNameOrDefault, "'")); | ||
return [2 /*return*/, defaults_1.DEFAULT_SITE]; | ||
} | ||
if (site.name !== siteNameOrDefault) { | ||
console.warn("[IB] Site '".concat(siteNameOrDefault, "'.json doesn't match name '").concat(site.name, "' in contents.")); | ||
site.name = siteNameOrDefault; | ||
} | ||
return [2 /*return*/, site]; | ||
}); | ||
}); | ||
} | ||
}; | ||
} | ||
exports.getJsonSiteBackend = getJsonSiteBackend; | ||
/** | ||
* Scans the public sites folder, reloading any modified sites. | ||
* Observes a configurable interval so as to not cause unnecessary IO. | ||
*/ | ||
function scanSites(folder, sites, lastScan) { | ||
if (sites === void 0) { sites = {}; } | ||
if (lastScan === void 0) { lastScan = 0; } | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var err_2, files, jsonFiles, root, _i, jsonFiles_1, jsonFile, siteName, fullPath, stat, mtimeMs, jsonBuffer, jsonStr, json, err_3; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
_a.trys.push([0, 2, , 3]); | ||
return [4 /*yield*/, asyncfs.stat(folder)]; | ||
case 1: | ||
_a.sent(); | ||
return [3 /*break*/, 3]; | ||
case 2: | ||
err_2 = _a.sent(); | ||
console.warn("[IB] Public sites folder does not exist"); | ||
return [2 /*return*/, sites]; | ||
case 3: return [4 /*yield*/, asyncfs.readdir(folder)]; | ||
case 4: | ||
files = _a.sent(); | ||
jsonFiles = files.filter(function (name) { return name.endsWith(".json"); }); | ||
root = path_1.default.join((0, process_1.cwd)(), folder); | ||
_i = 0, jsonFiles_1 = jsonFiles; | ||
_a.label = 5; | ||
case 5: | ||
if (!(_i < jsonFiles_1.length)) return [3 /*break*/, 11]; | ||
jsonFile = jsonFiles_1[_i]; | ||
siteName = jsonFile.replace(".json", ""); | ||
fullPath = path_1.default.join(root, jsonFile); | ||
return [4 /*yield*/, asyncfs.stat(fullPath)]; | ||
case 6: | ||
stat = _a.sent(); | ||
mtimeMs = stat.mtimeMs; | ||
if (mtimeMs < lastScan) { | ||
return [3 /*break*/, 10]; | ||
} | ||
_a.label = 7; | ||
case 7: | ||
_a.trys.push([7, 9, , 10]); | ||
return [4 /*yield*/, asyncfs.readFile(fullPath)]; | ||
case 8: | ||
jsonBuffer = _a.sent(); | ||
jsonStr = jsonBuffer.toString("utf-8"); | ||
json = JSON.parse(jsonStr); | ||
sites[siteName] = json; | ||
return [3 /*break*/, 10]; | ||
case 9: | ||
err_3 = _a.sent(); | ||
console.warn("[IB] Error reading '".concat(folder, "/").concat(jsonFile, "': ").concat(err_3)); | ||
return [3 /*break*/, 10]; | ||
case 10: | ||
_i++; | ||
return [3 /*break*/, 5]; | ||
case 11: return [2 /*return*/, sites]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.scanSites = scanSites; | ||
exports.DEFAULT_STATIC_SITE_BACKEND_OPTIONS = { | ||
basePath: environment_1.default.IB_STATIC_SITES_PATH, | ||
refreshInterval: 10 * 1000, | ||
}; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.DEFAULT_STATIC_SITE_BACKEND_OPTIONS=exports.scanSites=exports.getJsonSiteBackend=void 0;var a=require("tslib"),b=a.__importDefault(require("fs")),e=a.__importDefault(require("path")),f=require("process"),c=a.__importDefault(require("../environment")),g=require("../../defaults"),h=b.default.promises;function d(d,b,c){return void 0===b&&(b={}),void 0===c&&(c=0),a.__awaiter(this,void 0,void 0,function(){var g,i,j,k,l,m,n,o,p,q,r,s,t,u,v;return a.__generator(this,function(a){switch(a.label){case 0:return a.trys.push([0,2,,3]),[4,h.stat(d)];case 1:return a.sent(),[3,3];case 2:return g=a.sent(),console.warn("[IB] Public sites folder does not exist"),[2,b];case 3:return[4,h.readdir(d)];case 4:j=(i=a.sent()).filter(function(a){return a.endsWith(".json")}),k=e.default.join((0,f.cwd)(),d),l=0,m=j,a.label=5;case 5:if(!(l<m.length))return[3,11];return o=(n=m[l]).replace(".json",""),p=e.default.join(k,n),[4,h.stat(p)];case 6:if((r=(q=a.sent()).mtimeMs)<c)return[3,10];a.label=7;case 7:return a.trys.push([7,9,,10]),[4,h.readFile(p)];case 8:return u=JSON.parse(t=(s=a.sent()).toString("utf-8")),b[o]=u,[3,10];case 9:return v=a.sent(),console.warn("[IB] Error reading '".concat(d,"/").concat(n,"': ").concat(v)),[3,10];case 10:return l++,[3,5];case 11:return[2,b]}})})}exports.getJsonSiteBackend=function(b){var e=function(a){console.warn("[IB] Error scanning sites: ".concat(a))};void 0===b&&(b={});var f=Object.assign({},exports.DEFAULT_STATIC_SITE_BACKEND_OPTIONS,b),h={},i=0;return{connect:function(){return a.__awaiter(this,void 0,void 0,function(){var b;return a.__generator(this,function(a){switch(a.label){case 0:return a.trys.push([0,2,,3]),[4,d(c.default.IB_STATIC_SITES_PATH,h,i)];case 1:return h=a.sent(),[3,3];case 2:return e(b=a.sent()),[3,3];case 3:return[2]}})})},getSiteConfig:function(b){return a.__awaiter(this,void 0,void 0,function(){var j,k,l,m,n,o,p;return a.__generator(this,function(a){return(j=b.siteName,k=f.refreshInterval,l=new Date().getTime(),m=k> -1,n=l-i>k,m&&n&&d(c.default.IB_STATIC_SITES_PATH,h,i).then(function(a){return h=a}).then(function(){return i=l}).catch(e),o=null!=j?j:g.DEFAULT_SITE.name,p=h[o])?(p.name!==o&&(console.warn("[IB] Site '".concat(o,"'.json doesn't match name '").concat(p.name,"' in contents.")),p.name=o),[2,p]):(console.warn("[IB] Could not find site '".concat(o,"'")),[2,g.DEFAULT_SITE])})})}}},exports.scanSites=d,exports.DEFAULT_STATIC_SITE_BACKEND_OPTIONS={basePath:c.default.IB_STATIC_SITES_PATH,refreshInterval:1e4} |
@@ -1,31 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getStubMetricsBackend = void 0; | ||
var tslib_1 = require("tslib"); | ||
function getStubMetricsBackend() { | ||
return { | ||
getMetricsForSite: function () { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
return [2 /*return*/, new Map()]; | ||
}); | ||
}); | ||
}, | ||
getMetricsBucket: function () { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
return [2 /*return*/, {}]; | ||
}); | ||
}); | ||
}, | ||
ingestBatch: function (req, batch) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
console.log("Metrics stub ingests batch", batch); | ||
return [2 /*return*/]; | ||
}); | ||
}); | ||
}, | ||
}; | ||
} | ||
exports.getStubMetricsBackend = getStubMetricsBackend; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getStubMetricsBackend=void 0;var a=require("tslib");exports.getStubMetricsBackend=function(){return{getMetricsForSite:function(){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return[2,new Map]})})},getMetricsBucket:function(){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return[2,{}]})})},ingestBatch:function(b,c){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return console.log("Metrics stub ingests batch",c),[2]})})}}} |
@@ -1,384 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getMetricsBucket = exports.incrementMetricInPipeline = exports.isValidMetricsSample = exports.ingestBatch = exports.markVariantSeen = exports.getOrCreateSession = exports.getMetricsForSite = exports.getRedisBackend = exports.DEFAULT_REDIS_OPTIONS = void 0; | ||
var tslib_1 = require("tslib"); | ||
var crypto_1 = require("crypto"); | ||
var ioredis_1 = tslib_1.__importDefault(require("ioredis")); | ||
var environment_1 = tslib_1.__importDefault(require("../environment")); | ||
var server_utils_1 = require("../server-utils"); | ||
var utils_1 = require("../../utils"); | ||
var constants_1 = require("../../constants"); | ||
exports.DEFAULT_REDIS_OPTIONS = { | ||
lazyConnect: true, | ||
disconnectWaitDuration: 50, | ||
retryStrategy: function (count) { | ||
var maxAttempts = parseInt(environment_1.default.IB_REDIS_RETRY_COUNT + ""); | ||
var interval = parseInt(environment_1.default.IB_REDIS_RETRY_INTERVAL + ""); | ||
console.info("[IB] Connecting to Redis attempt # ".concat(count, " out of ").concat(maxAttempts)); | ||
if (count >= maxAttempts) { | ||
return null; | ||
} | ||
else { | ||
return interval; | ||
} | ||
}, | ||
}; | ||
function getRedisBackend(initOptions) { | ||
if (initOptions === void 0) { initOptions = {}; } | ||
var options = Object.freeze(Object.assign({}, exports.DEFAULT_REDIS_OPTIONS, initOptions)); | ||
var redis = new ioredis_1.default(options); | ||
var connected = false; | ||
return { | ||
get client() { return redis; }, | ||
get connected() { return connected; }, | ||
connect: function () { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
return [2 /*return*/, new Promise(function (resolve) { | ||
if (connected) { | ||
resolve(); | ||
return; | ||
} | ||
switch (redis.status) { | ||
case "connecting": | ||
case "connect": | ||
case "ready": | ||
resolve(); | ||
return; | ||
default: | ||
console.info("[IB] Connecting to redis..."); | ||
redis | ||
.on("error", function (err) { | ||
console.error("[IB] Error connecting to Redis: ".concat(err)); | ||
connected = false; | ||
return; | ||
}) | ||
.on("close", function () { | ||
connected = false; | ||
console.log("[IB] Redis connection closed"); | ||
return; | ||
}) | ||
.on("connect", function () { | ||
connected = true; | ||
console.log("[IB] Redis connection opened"); | ||
return; | ||
}) | ||
.connect() | ||
// Despite the error handler above, ioredis will still throw. | ||
// Suppress, because it's handled above. | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
.catch(function (err) { return void 0; }); | ||
resolve(); | ||
return; | ||
} | ||
})]; | ||
}); | ||
}); | ||
}, | ||
disconnect: function () { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
_a.trys.push([0, , 3, 6]); | ||
console.debug("[IB] Disconnecting from redis..."); | ||
if (!(redis.status === "ready")) return [3 /*break*/, 2]; | ||
return [4 /*yield*/, redis.quit()]; | ||
case 1: | ||
_a.sent(); | ||
_a.label = 2; | ||
case 2: | ||
connected = false; | ||
redis.disconnect(); | ||
return [3 /*break*/, 6]; | ||
case 3: | ||
if (!(redis.status === "ready")) return [3 /*break*/, 5]; | ||
return [4 /*yield*/, new Promise(function (r) { return setTimeout(r, options.disconnectWaitDuration); })]; | ||
case 4: | ||
_a.sent(); | ||
return [3 /*break*/, 3]; | ||
case 5: return [7 /*endfinally*/]; | ||
case 6: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}, | ||
getMetricsForSite: function (site, experiments) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
if (!connected) { | ||
return [2 /*return*/, new Map()]; | ||
} | ||
return [2 /*return*/, getMetricsForSite(redis, site, experiments)]; | ||
}); | ||
}); | ||
}, | ||
getMetricsBucket: function (siteId, experiment, variant) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
if (!connected) { | ||
return [2 /*return*/, {}]; | ||
} | ||
return [2 /*return*/, getMetricsBucket(redis, siteId, experiment, variant)]; | ||
}); | ||
}); | ||
}, | ||
ingestBatch: function (req, batch) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
if (!connected) { | ||
return [2 /*return*/]; | ||
} | ||
return [2 /*return*/, ingestBatch(redis, req, batch)]; | ||
}); | ||
}); | ||
}, | ||
getOrCreateSession: function (req) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
if (!connected) { | ||
throw new Error("Not connected to Redis session store"); | ||
} | ||
return [2 /*return*/, getOrCreateSession(redis, req)]; | ||
}); | ||
}); | ||
}, | ||
markVariantSeen: function (session, site, experimentId, variantName) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
if (!connected) { | ||
(0, utils_1.markVariantInSession)(session, site, experimentId, variantName); | ||
return [2 /*return*/, session]; | ||
} | ||
return [2 /*return*/, markVariantSeen(redis, session, site, experimentId, variantName)]; | ||
}); | ||
}); | ||
}, | ||
}; | ||
} | ||
exports.getRedisBackend = getRedisBackend; | ||
function getMetricsForSite(redis, site, experiments) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var variantBuckets, _i, experiments_1, exp, _a, _b, variant, bucket; | ||
return tslib_1.__generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
variantBuckets = new Map(); | ||
_i = 0, experiments_1 = experiments; | ||
_c.label = 1; | ||
case 1: | ||
if (!(_i < experiments_1.length)) return [3 /*break*/, 6]; | ||
exp = experiments_1[_i]; | ||
_a = 0, _b = exp.variants; | ||
_c.label = 2; | ||
case 2: | ||
if (!(_a < _b.length)) return [3 /*break*/, 5]; | ||
variant = _b[_a]; | ||
return [4 /*yield*/, getMetricsBucket(redis, site.name, exp.id, variant.name)]; | ||
case 3: | ||
bucket = _c.sent(); | ||
variantBuckets.set(variant, bucket); | ||
_c.label = 4; | ||
case 4: | ||
_a++; | ||
return [3 /*break*/, 2]; | ||
case 5: | ||
_i++; | ||
return [3 /*break*/, 1]; | ||
case 6: return [2 /*return*/, variantBuckets]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.getMetricsForSite = getMetricsForSite; | ||
function getOrCreateSession(redis, req) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var siteName, sid, sessionsSetKey, session, sessionKey, sessionRaw, serializedSession, pipe, sessionKey, err_1; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
siteName = req.siteName; | ||
sid = req.sid; | ||
sessionsSetKey = (0, server_utils_1.makeKey)(["sessions"]); | ||
session = null; | ||
if (!((0, utils_1.exists)(sid) && sid.length === constants_1.UUID_LENGTH)) return [3 /*break*/, 2]; | ||
sessionKey = (0, server_utils_1.makeKey)(["session", sid]); | ||
return [4 /*yield*/, redis.get(sessionKey)]; | ||
case 1: | ||
sessionRaw = _a.sent(); | ||
if (!(0, utils_1.exists)(sessionRaw)) { | ||
console.warn("[IB] Invalid or unknown session '".concat(sessionKey, "'")); | ||
} | ||
else { | ||
session = JSON.parse(sessionRaw); | ||
return [2 /*return*/, session]; | ||
} | ||
_a.label = 2; | ||
case 2: | ||
if (!!(0, utils_1.exists)(session)) return [3 /*break*/, 6]; | ||
if (!(0, utils_1.exists)(siteName)) { | ||
throw new Error("Invalid session scope"); | ||
} | ||
sid = (0, crypto_1.randomUUID)(); | ||
session = { | ||
sid: sid, | ||
selections: {}, | ||
}; | ||
serializedSession = JSON.stringify(session); | ||
pipe = redis.multi(); | ||
_a.label = 3; | ||
case 3: | ||
_a.trys.push([3, 5, , 6]); | ||
sessionKey = (0, server_utils_1.makeKey)(["session", session.sid]); | ||
pipe.sadd(sessionsSetKey, session.sid); | ||
pipe.set(sessionKey, serializedSession); | ||
return [4 /*yield*/, pipe.exec()]; | ||
case 4: | ||
_a.sent(); | ||
return [3 /*break*/, 6]; | ||
case 5: | ||
err_1 = _a.sent(); | ||
console.warn("[IB] Error saving session '".concat(sid, "': ").concat(err_1)); | ||
return [3 /*break*/, 6]; | ||
case 6: return [2 /*return*/, session]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.getOrCreateSession = getOrCreateSession; | ||
function markVariantSeen(redis, session, site, experimentId, variantName) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var serializedSession, sessionKey, err_2; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
(0, utils_1.markVariantInSession)(session, site, experimentId, variantName); | ||
serializedSession = JSON.stringify(session); | ||
sessionKey = (0, server_utils_1.makeKey)(["session", session.sid]); | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, redis.set(sessionKey, serializedSession)]; | ||
case 2: | ||
_a.sent(); | ||
return [3 /*break*/, 4]; | ||
case 3: | ||
err_2 = _a.sent(); | ||
console.warn("[IB] Error saving session '".concat(session.sid, "': ").concat(err_2)); | ||
return [3 /*break*/, 4]; | ||
case 4: return [2 /*return*/, session]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.markVariantSeen = markVariantSeen; | ||
/** | ||
* Ingests a batch of metrics, pipelining it into Redis | ||
* @param redis | ||
*/ | ||
function ingestBatch(redis, req, batch) { | ||
var _a; | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var sid, site, experiment, variant, samples, session, _b, pipe, prevTs, err_3; | ||
return tslib_1.__generator(this, function (_c) { | ||
switch (_c.label) { | ||
case 0: | ||
sid = req.sid; | ||
if (!(0, utils_1.exists)(sid)) { | ||
throw new Error("Missing session"); | ||
} | ||
site = batch.site, experiment = batch.experiment, variant = batch.variant, samples = batch.entries; | ||
if (!((_a = req.session) !== null && _a !== void 0)) return [3 /*break*/, 1]; | ||
_b = _a; | ||
return [3 /*break*/, 3]; | ||
case 1: return [4 /*yield*/, getOrCreateSession(redis, req)]; | ||
case 2: | ||
_b = _c.sent(); | ||
_c.label = 3; | ||
case 3: | ||
session = _b; | ||
return [4 /*yield*/, markVariantSeen(redis, session, site, experiment, variant)]; | ||
case 4: | ||
_c.sent(); | ||
pipe = redis.pipeline(); | ||
prevTs = 0; | ||
samples | ||
.filter(isValidMetricsSample) | ||
.forEach(function (sample) { | ||
var ts = sample.ts, name = sample.name; | ||
// Ignore out of order timestamps | ||
if (ts < prevTs) { | ||
return; | ||
} | ||
incrementMetricInPipeline(pipe, site, experiment, variant, name, 1); | ||
prevTs = ts; | ||
}); | ||
_c.label = 5; | ||
case 5: | ||
_c.trys.push([5, 7, , 8]); | ||
return [4 /*yield*/, pipe.exec()]; | ||
case 6: | ||
_c.sent(); | ||
return [3 /*break*/, 8]; | ||
case 7: | ||
err_3 = _c.sent(); | ||
console.error("[IB] Error while pipelining ".concat(samples.length, " for '").concat(sid, "'")); | ||
return [3 /*break*/, 8]; | ||
case 8: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.ingestBatch = ingestBatch; | ||
function isValidMetricsSample(sample) { | ||
var ts = sample.ts, payload = sample.payload; | ||
if (typeof ts !== "number") { | ||
return false; | ||
} | ||
else if (typeof payload === "string" && | ||
payload.length > parseInt(environment_1.default.IB_MAX_METRICS_PAYLOAD_LEN + "")) { | ||
return false; | ||
} | ||
else { | ||
return true; | ||
} | ||
} | ||
exports.isValidMetricsSample = isValidMetricsSample; | ||
function incrementMetricInPipeline(pipe, site, eid, variant, metric, incBy) { | ||
if (incBy === void 0) { incBy = 1; } | ||
if (site === null || site === void 0) { | ||
throw new Error("Missing site ID in incrementExposure"); | ||
} | ||
var key = (0, server_utils_1.makeKey)([site, eid, variant, "metrics"]); | ||
// TODO: Note the loss of bits, TEST | ||
// NOTE: Conversion from an integer to a float can happen here. | ||
if (incBy % 1 === 0) { | ||
pipe.hincrby(key, metric, incBy); | ||
} | ||
else { | ||
pipe.hincrbyfloat(key, metric, incBy); | ||
} | ||
return pipe; | ||
} | ||
exports.incrementMetricInPipeline = incrementMetricInPipeline; | ||
function getMetricsBucket(redis, siteId, eid, variant) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var key, hmap, bucket; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
key = (0, server_utils_1.makeKey)([siteId, eid, variant, "metrics"]); | ||
return [4 /*yield*/, redis.hgetall(key)]; | ||
case 1: | ||
hmap = _a.sent(); | ||
if (!(0, utils_1.exists)(hmap)) { | ||
return [2 /*return*/, {}]; | ||
} | ||
bucket = {}; | ||
Object.keys(hmap).forEach(function (k) { return bucket[k] = (0, server_utils_1.toNumber)(hmap[k]); }); | ||
return [2 /*return*/, bucket]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.getMetricsBucket = getMetricsBucket; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getMetricsBucket=exports.incrementMetricInPipeline=exports.isValidMetricsSample=exports.ingestBatch=exports.markVariantSeen=exports.getOrCreateSession=exports.getMetricsForSite=exports.getRedisBackend=exports.DEFAULT_REDIS_OPTIONS=void 0;var a=require("tslib"),i=require("crypto"),j=a.__importDefault(require("ioredis")),k=a.__importDefault(require("../environment")),l=require("../server-utils"),m=require("../../utils"),n=require("../../constants");function b(b,c,d){return a.__awaiter(this,void 0,void 0,function(){var e,f,g,i,j,k,l,m;return a.__generator(this,function(a){switch(a.label){case 0:e=new Map,f=0,g=d,a.label=1;case 1:if(!(f<g.length))return[3,6];j=0,k=(i=g[f]).variants,a.label=2;case 2:if(!(j<k.length))return[3,5];return l=k[j],[4,h(b,c.name,i.id,l.name)];case 3:m=a.sent(),e.set(l,m),a.label=4;case 4:return j++,[3,2];case 5:return f++,[3,1];case 6:return[2,e]}})})}function c(b,c){return a.__awaiter(this,void 0,void 0,function(){var d,e,f,g,h,j,k,o,p;return a.__generator(this,function(a){switch(a.label){case 0:if(d=c.siteName,e=c.sid,f=(0,l.makeKey)(["sessions"]),g=null,!((0,m.exists)(e)&&e.length===n.UUID_LENGTH))return[3,2];return o=(0,l.makeKey)(["session",e]),[4,b.get(o)];case 1:if(h=a.sent(),(0,m.exists)(h))return[2,g=JSON.parse(h)];console.warn("[IB] Invalid or unknown session '".concat(o,"'")),a.label=2;case 2:if((0,m.exists)(g))return[3,6];if(!(0,m.exists)(d))throw Error("Invalid session scope");j=JSON.stringify(g={sid:e=(0,i.randomUUID)(),selections:{}}),k=b.multi(),a.label=3;case 3:return a.trys.push([3,5,,6]),o=(0,l.makeKey)(["session",g.sid]),k.sadd(f,g.sid),k.set(o,j),[4,k.exec()];case 4:return a.sent(),[3,6];case 5:return p=a.sent(),console.warn("[IB] Error saving session '".concat(e,"': ").concat(p)),[3,6];case 6:return[2,g]}})})}function d(b,c,d,e,f){return a.__awaiter(this,void 0,void 0,function(){var g,h,i;return a.__generator(this,function(a){switch(a.label){case 0:(0,m.markVariantInSession)(c,d,e,f),g=JSON.stringify(c),h=(0,l.makeKey)(["session",c.sid]),a.label=1;case 1:return a.trys.push([1,3,,4]),[4,b.set(h,g)];case 2:return a.sent(),[3,4];case 3:return i=a.sent(),console.warn("[IB] Error saving session '".concat(c.sid,"': ").concat(i)),[3,4];case 4:return[2,c]}})})}function e(b,e,h){var i;return a.__awaiter(this,void 0,void 0,function(){var j,k,l,n,o,p,q,r,s,t;return a.__generator(this,function(a){switch(a.label){case 0:if(j=e.sid,!(0,m.exists)(j))throw Error("Missing session");if(k=h.site,l=h.experiment,n=h.variant,o=h.entries,!(null!==(i=e.session)&& void 0!==i))return[3,1];return q=i,[3,3];case 1:return[4,c(b,e)];case 2:q=a.sent(),a.label=3;case 3:return[4,d(b,p=q,k,l,n)];case 4:a.sent(),r=b.pipeline(),s=0,o.filter(f).forEach(function(a){var b=a.ts,c=a.name;!(b<s)&&(g(r,k,l,n,c,1),s=b)}),a.label=5;case 5:return a.trys.push([5,7,,8]),[4,r.exec()];case 6:return a.sent(),[3,8];case 7:return t=a.sent(),console.error("[IB] Error while pipelining ".concat(o.length," for '").concat(j,"'")),[3,8];case 8:return[2]}})})}function f(a){var c=a.ts,b=a.payload;return"number"==typeof c&&!("string"==typeof b&&b.length>parseInt(k.default.IB_MAX_METRICS_PAYLOAD_LEN+""))}function g(b,c,f,g,d,a){if(void 0===a&&(a=1),null==c)throw Error("Missing site ID in incrementExposure");var e=(0,l.makeKey)([c,f,g,"metrics"]);return a%1==0?b.hincrby(e,d,a):b.hincrbyfloat(e,d,a),b}function h(b,c,d,e){return a.__awaiter(this,void 0,void 0,function(){var f,g,h;return a.__generator(this,function(a){switch(a.label){case 0:return f=(0,l.makeKey)([c,d,e,"metrics"]),[4,b.hgetall(f)];case 1:if(g=a.sent(),!(0,m.exists)(g))return[2,{}];return h={},Object.keys(g).forEach(function(a){return h[a]=(0,l.toNumber)(g[a])}),[2,h]}})})}exports.DEFAULT_REDIS_OPTIONS={lazyConnect:!0,disconnectWaitDuration:50,retryStrategy:function(a){var b=parseInt(k.default.IB_REDIS_RETRY_COUNT+""),c=parseInt(k.default.IB_REDIS_RETRY_INTERVAL+"");return(console.info("[IB] Connecting to Redis attempt # ".concat(a," out of ").concat(b)),a>=b)?null:c}},exports.getRedisBackend=function(f){void 0===f&&(f={});var g=Object.freeze(Object.assign({},exports.DEFAULT_REDIS_OPTIONS,f)),i=new j.default(g),k=!1;return{get client(){return i},get connected(){return k},connect:function(){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return[2,new Promise(function(a){if(k){a();return}switch(i.status){case"connecting":case"connect":case"ready":a();return;default:console.info("[IB] Connecting to redis..."),i.on("error",function(a){console.error("[IB] Error connecting to Redis: ".concat(a)),k=!1}).on("close",function(){k=!1,console.log("[IB] Redis connection closed")}).on("connect",function(){k=!0,console.log("[IB] Redis connection opened")}).connect().catch(function(a){}),a();return}})]})})},disconnect:function(){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){switch(a.label){case 0:if(a.trys.push([0,,3,6]),console.debug("[IB] Disconnecting from redis..."),"ready"!==i.status)return[3,2];return[4,i.quit()];case 1:a.sent(),a.label=2;case 2:return k=!1,i.disconnect(),[3,6];case 3:if("ready"!==i.status)return[3,5];return[4,new Promise(function(a){return setTimeout(a,g.disconnectWaitDuration)})];case 4:return a.sent(),[3,3];case 5:return[7];case 6:return[2]}})})},getMetricsForSite:function(c,d){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return k?[2,b(i,c,d)]:[2,new Map]})})},getMetricsBucket:function(b,c,d){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return k?[2,h(i,b,c,d)]:[2,{}]})})},ingestBatch:function(b,c){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return k?[2,e(i,b,c)]:[2]})})},getOrCreateSession:function(b){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){if(!k)throw Error("Not connected to Redis session store");return[2,c(i,b)]})})},markVariantSeen:function(b,c,e,f){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return k?[2,d(i,b,c,e,f)]:((0,m.markVariantInSession)(b,c,e,f),[2,b])})})}}},exports.getMetricsForSite=b,exports.getOrCreateSession=c,exports.markVariantSeen=d,exports.ingestBatch=e,exports.isValidMetricsSample=f,exports.incrementMetricInPipeline=g,exports.getMetricsBucket=h |
@@ -1,44 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getStubSessionsBackend = void 0; | ||
var tslib_1 = require("tslib"); | ||
var crypto_1 = require("crypto"); | ||
var utils_1 = require("../../utils"); | ||
function getStubSessionsBackend() { | ||
var sessions = {}; | ||
return { | ||
getOrCreateSession: function (req) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var siteName, sid, session; | ||
return tslib_1.__generator(this, function (_a) { | ||
siteName = req.siteName; | ||
sid = req.sid; | ||
if (!(0, utils_1.exists)(siteName)) { | ||
throw new Error("Invalid session scope"); | ||
} | ||
session = null; | ||
if ((0, utils_1.exists)(sid) && sid.trim() !== "") { | ||
session = sessions[sid]; | ||
if (!(0, utils_1.exists)(session)) { | ||
console.warn("[IB] Missing or expired session '".concat(sid, "'")); | ||
session = null; | ||
} | ||
} | ||
if (!session) { | ||
return [2 /*return*/, (0, utils_1.makeNewSession)((0, crypto_1.randomUUID)())]; | ||
} | ||
return [2 /*return*/, session]; | ||
}); | ||
}); | ||
}, | ||
markVariantSeen: function (session, site, experiment, variant) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
(0, utils_1.markVariantInSession)(session, site, experiment, variant); | ||
return [2 /*return*/, session]; | ||
}); | ||
}); | ||
} | ||
}; | ||
} | ||
exports.getStubSessionsBackend = getStubSessionsBackend; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getStubSessionsBackend=void 0;var a=require("tslib"),b=require("crypto"),c=require("../../utils");exports.getStubSessionsBackend=function(){var d={};return{getOrCreateSession:function(e){return a.__awaiter(this,void 0,void 0,function(){var f,g,h;return a.__generator(this,function(a){if(f=e.siteName,g=e.sid,!(0,c.exists)(f))throw Error("Invalid session scope");return(h=null,(0,c.exists)(g)&&""!==g.trim()&&(h=d[g],(0,c.exists)(h)||(console.warn("[IB] Missing or expired session '".concat(g,"'")),h=null)),h)?[2,h]:[2,(0,c.makeNewSession)((0,b.randomUUID)())]})})},markVariantSeen:function(b,d,e,f){return a.__awaiter(this,void 0,void 0,function(){return a.__generator(this,function(a){return(0,c.markVariantInSession)(b,d,e,f),[2,b]})})}}} |
@@ -5,2 +5,3 @@ declare const defaults: { | ||
IB_COOKIE_SETTINGS: string; | ||
IB_ENFORCE_ORIGIN_CHECK: string; | ||
IB_ORIGINS_ALLOWLIST: string; | ||
@@ -7,0 +8,0 @@ IB_REDIS_RETRY_INTERVAL: number; |
@@ -1,26 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var constants_1 = require("../constants"); | ||
// These are overlaid with process variables, if set | ||
var defaults = { | ||
IB_MODE: process.env.NODE_ENV, | ||
IB_BASE_API_URL: constants_1.DEFAULT_BASE_URL, | ||
IB_COOKIE_SETTINGS: constants_1.DEFAULT_COOKIE_SETTINGS, | ||
IB_ORIGINS_ALLOWLIST: process.env.NODE_ENV === "production" ? "" : constants_1.DEFAULT_BASE_URL, | ||
IB_REDIS_RETRY_INTERVAL: 1000, | ||
IB_REDIS_RETRY_COUNT: 10, | ||
IB_STATIC_SITES_PATH: "./public/sites", | ||
IB_MAX_METRICS_PAYLOAD_LEN: 1024, | ||
}; | ||
var funcs = { | ||
isDev: function () { return env.IB_MODE === "development"; }, | ||
isTest: function () { return env.IB_MODE === "test"; }, | ||
isProduction: function () { return env.IB_MODE === "production"; }, | ||
}; | ||
var env = Object.assign({}, defaults, funcs); | ||
// Pull from actual environment vars, or use the defaults above | ||
Object | ||
.keys(defaults) | ||
.filter(function (key) { return typeof process.env[key] !== "undefined"; }) | ||
.forEach(function (key) { return env[key] = process.env[key]; }); | ||
exports.default = env; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var a=require("../constants"),b={IB_MODE:process.env.NODE_ENV,IB_BASE_API_URL:a.DEFAULT_BASE_URL,IB_COOKIE_SETTINGS:a.DEFAULT_COOKIE_SETTINGS,IB_ENFORCE_ORIGIN_CHECK:"false",IB_ORIGINS_ALLOWLIST:"production"===process.env.NODE_ENV?"":a.DEFAULT_BASE_URL,IB_REDIS_RETRY_INTERVAL:1e3,IB_REDIS_RETRY_COUNT:10,IB_STATIC_SITES_PATH:"./public/sites",IB_MAX_METRICS_PAYLOAD_LEN:1024},c=Object.assign({},b,{isDev:function(){return"development"===c.IB_MODE},isTest:function(){return"test"===c.IB_MODE},isProduction:function(){return"production"===c.IB_MODE}});Object.keys(b).filter(function(a){return void 0!==process.env[a]}).forEach(function(a){return c[a]=process.env[a]}),exports.default=c |
import { InstantBanditServerOptions, InstantBanditServer, ValidatedRequest, MetricsBackend } from "./server-types"; | ||
import { Site, SiteMeta } from "../models"; | ||
export declare const DEFAULT_SERVER_OPTIONS: Partial<InstantBanditServerOptions>; | ||
export declare const DEFAULT_SERVER_OPTIONS: InstantBanditServerOptions; | ||
/** | ||
@@ -5,0 +5,0 @@ * Provides framework-agnostic helper methods that expose configuration and handle |
@@ -1,217 +0,1 @@ | ||
"use strict"; | ||
var _a; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getBanditServer = exports.log = exports.TAG = exports.embedProbabilities = exports.buildInstantBanditServer = exports.DEFAULT_SERVER_OPTIONS = void 0; | ||
var tslib_1 = require("tslib"); | ||
var environment_1 = tslib_1.__importDefault(require("./environment")); | ||
var utils_1 = require("../utils"); | ||
var json_sites_1 = require("./backends/json-sites"); | ||
var server_utils_1 = require("./server-utils"); | ||
var bandit_1 = require("../bandit"); | ||
var redis_1 = require("./backends/redis"); | ||
exports.DEFAULT_SERVER_OPTIONS = { | ||
clientOrigins: ((_a = environment_1.default.IB_ORIGINS_ALLOWLIST) !== null && _a !== void 0 ? _a : ""), | ||
}; | ||
/** | ||
* Provides framework-agnostic helper methods that expose configuration and handle | ||
* initialization and shutdown of backend services for metrics, models, and sessions. | ||
* | ||
* Note: Implementers should call `getBanditServer` with their own config instead. | ||
* | ||
* @private | ||
*/ | ||
function buildInstantBanditServer(initOptions) { | ||
var _a; | ||
var options = Object.assign({}, exports.DEFAULT_SERVER_OPTIONS, initOptions); | ||
// Only instantiate the backends if needed | ||
var defaultRedisBackend = null; | ||
if (!options.models) { | ||
options.models = (0, json_sites_1.getJsonSiteBackend)(); | ||
} | ||
if (!options.metrics) { | ||
options.metrics = defaultRedisBackend = (0, redis_1.getRedisBackend)(); | ||
} | ||
if (!options.sessions) { | ||
options.sessions = (0, utils_1.exists)(defaultRedisBackend) ? defaultRedisBackend | ||
: (defaultRedisBackend = (0, redis_1.getRedisBackend)()); | ||
} | ||
Object.freeze(options); | ||
var metrics = options.metrics, models = options.models, sessions = options.sessions; | ||
var devOrigins = environment_1.default.isDev() ? [(0, utils_1.getBaseUrl)()] : []; | ||
var allowedOrigins = (0, server_utils_1.normalizeOrigins)((_a = options.clientOrigins) !== null && _a !== void 0 ? _a : [], devOrigins); | ||
var backends = [metrics, models, sessions]; | ||
var initPromise; | ||
var shutdownPromise; | ||
return { | ||
get metrics() { return metrics; }, | ||
get models() { return models; }, | ||
get sessions() { return sessions; }, | ||
get origins() { return allowedOrigins; }, | ||
isBackendConnected: function (backend) { | ||
// Backends that don't implement connection logic are considered "connected" | ||
if (!(0, utils_1.exists)(backend.connected)) { | ||
return true; | ||
} | ||
else { | ||
return backend.connected; | ||
} | ||
}, | ||
init: function () { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (initPromise) { | ||
return [2 /*return*/, initPromise]; | ||
} | ||
initPromise = Promise.all(backends.filter(utils_1.exists).map(function (be) { var _a; return (_a = be.connect) === null || _a === void 0 ? void 0 : _a.call(be); })) | ||
.catch(function (err) { return console.warn("[IB]: Error initializing: ".concat(err)); }) | ||
.then(function () { return void 0; }); | ||
(0, exports.log)("Server initializing...."); | ||
return [4 /*yield*/, initPromise]; | ||
case 1: | ||
_a.sent(); | ||
(0, exports.log)("Server initialized"); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}, | ||
shutdown: function () { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (shutdownPromise) { | ||
return [2 /*return*/, shutdownPromise]; | ||
} | ||
shutdownPromise = Promise.all(backends.filter(utils_1.exists).map(function (be) { var _a; return (_a = be.disconnect) === null || _a === void 0 ? void 0 : _a.call(be); })) | ||
.catch(function (err) { return console.warn("[IB]: Error shutting down: ".concat(err)); }) | ||
.then(function () { return void 0; }); | ||
(0, exports.log)("Server shutting down...."); | ||
return [4 /*yield*/, shutdownPromise]; | ||
case 1: | ||
_a.sent(); | ||
(0, exports.log)("Server shut down"); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}, | ||
/** | ||
* Produces a site object bearing probabilities and ready for consumer selection | ||
*/ | ||
getSite: function (req) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var getSiteConfig, siteConfig, siteWithProbs; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
getSiteConfig = models.getSiteConfig; | ||
return [4 /*yield*/, getSiteConfig(req)]; | ||
case 1: | ||
siteConfig = _a.sent(); | ||
return [4 /*yield*/, embedProbabilities(req, siteConfig, metrics)]; | ||
case 2: | ||
siteWithProbs = _a.sent(); | ||
return [2 /*return*/, { | ||
responseHeaders: {}, | ||
site: siteWithProbs, | ||
}]; | ||
} | ||
}); | ||
}); | ||
} | ||
}; | ||
} | ||
exports.buildInstantBanditServer = buildInstantBanditServer; | ||
/** | ||
* Computes probabilities required for variant selection and inlines them into a site configuration | ||
*/ | ||
function embedProbabilities(req, origSite, metrics) { | ||
var _a, _b; | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var site, experiments, variantMetrics, _i, _c, experiment, variants, exposures, conversions, _d, variants_1, variant, bucket, probs, _loop_1, key; | ||
return tslib_1.__generator(this, function (_e) { | ||
switch (_e.label) { | ||
case 0: | ||
site = JSON.parse(JSON.stringify(origSite)); | ||
experiments = site.experiments; | ||
return [4 /*yield*/, metrics.getMetricsForSite(site, experiments)]; | ||
case 1: | ||
variantMetrics = _e.sent(); | ||
for (_i = 0, _c = experiments; _i < _c.length; _i++) { | ||
experiment = _c[_i]; | ||
variants = experiment.variants; | ||
exposures = {}; | ||
conversions = {}; | ||
for (_d = 0, variants_1 = variants; _d < variants_1.length; _d++) { | ||
variant = variants_1[_d]; | ||
if (!variantMetrics.has(variant)) { | ||
continue; | ||
} | ||
bucket = variantMetrics.get(variant); | ||
// Show raw metrics in dev mode | ||
if (environment_1.default.isDev()) { | ||
variant.metrics = bucket; | ||
} | ||
exposures[variant.name] = (_a = bucket.exposures) !== null && _a !== void 0 ? _a : 0; | ||
conversions[variant.name] = (_b = bucket.conversions) !== null && _b !== void 0 ? _b : 0; | ||
} | ||
probs = void 0; | ||
if (Object.keys(exposures).length > 0 && Object.keys(conversions).length > 0) { | ||
probs = (0, bandit_1.bandit)(exposures, conversions || {}); | ||
} | ||
else { | ||
probs = {}; | ||
} | ||
experiment.metrics = {}; | ||
_loop_1 = function (key) { | ||
var variant = variants.find(function (v) { return v.name === key; }); | ||
if (variant) { | ||
variant.prob = probs[key]; | ||
} | ||
}; | ||
for (key in probs) { | ||
_loop_1(key); | ||
} | ||
} | ||
return [2 /*return*/, site]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.embedProbabilities = embedProbabilities; | ||
exports.TAG = "[IB][server]"; | ||
var log = function () { | ||
var items = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
items[_i] = arguments[_i]; | ||
} | ||
console.info.apply(console, tslib_1.__spreadArray([exports.TAG], items, true)); | ||
}; | ||
exports.log = log; | ||
/** | ||
* Creates an `InstantBanditServer` you can use in your backend app. | ||
*/ | ||
var banditServer; | ||
function getBanditServer(options) { | ||
// NOTE: Next.js will re-import modules during HMR. | ||
// This ends up creating multiple spurious instances of module functions, state, | ||
// network resources, database connections, etc. | ||
// A workaround is to attach an object to the global scope during dev. | ||
if (environment_1.default.isDev()) { | ||
if (global["defaultBanditServer"]) { | ||
banditServer = global["defaultBanditServer"]; | ||
} | ||
} | ||
if (!banditServer) { | ||
console.debug("[IB] Creating default InstantBanditServer..."); | ||
banditServer = buildInstantBanditServer(options); | ||
} | ||
if (environment_1.default.isDev()) { | ||
global["defaultBanditServer"] = banditServer; | ||
} | ||
return banditServer; | ||
} | ||
exports.getBanditServer = getBanditServer; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getBanditServer=exports.log=exports.TAG=exports.embedProbabilities=exports.buildInstantBanditServer=exports.DEFAULT_SERVER_OPTIONS=void 0;var a,j,b=require("tslib"),c=b.__importStar(require("../constants")),d=b.__importDefault(require("./environment")),k=require("./backends/redis"),l=require("../bandit"),m=require("../utils"),e=require("./backends/json-sites"),f=require("./backends/metrics"),g=require("./backends/sessions"),n=require("./server-utils");function h(l){var f,q,r,a=Object.assign({},exports.DEFAULT_SERVER_OPTIONS,l),c=null;a.models||(a.models=(0,e.getJsonSiteBackend)()),a.metrics||(a.metrics=c=(0,k.getRedisBackend)()),a.sessions||(a.sessions=(0,m.exists)(c)?c:c=(0,k.getRedisBackend)()),Object.freeze(a);var g=a.metrics,h=a.models,j=a.sessions,o=d.default.isDev()?[(0,m.getBaseUrl)()]:[],p=(0,n.normalizeOrigins)(null!==(f=a.clientOrigins)&& void 0!==f?f:[],o),s=[g,h,j];return{get options(){return a},get metrics(){return g},get models(){return h},get sessions(){return j},get origins(){return p},isBackendConnected:function(a){return!(0,m.exists)(a.connected)||a.connected},init:function(){return b.__awaiter(this,void 0,void 0,function(){return b.__generator(this,function(a){switch(a.label){case 0:if(q)return[2,q];return q=Promise.all(s.filter(m.exists).map(function(b){var a;return null===(a=b.connect)|| void 0===a?void 0:a.call(b)})).catch(function(a){return console.warn("[IB]: Error initializing: ".concat(a))}).then(function(){}),(0,exports.log)("Server initializing...."),[4,q];case 1:return a.sent(),(0,exports.log)("Server initialized"),[2]}})})},shutdown:function(){return b.__awaiter(this,void 0,void 0,function(){return b.__generator(this,function(a){switch(a.label){case 0:if(r)return[2,r];return r=Promise.all(s.filter(m.exists).map(function(b){var a;return null===(a=b.disconnect)|| void 0===a?void 0:a.call(b)})).catch(function(a){return console.warn("[IB]: Error shutting down: ".concat(a))}).then(function(){}),(0,exports.log)("Server shutting down...."),[4,r];case 1:return a.sent(),(0,exports.log)("Server shut down"),[2]}})})},getSite:function(a){return b.__awaiter(this,void 0,void 0,function(){var c,d,e;return b.__generator(this,function(b){switch(b.label){case 0:return[4,(c=h.getSiteConfig)(a)];case 1:return[4,i(a,d=b.sent(),g)];case 2:return[2,{responseHeaders:{},site:e=b.sent()}]}})})}}}function i(a,c,e){var f,g;return b.__awaiter(this,void 0,void 0,function(){var a,h,i,j,k,m,n,o,p,q,r,s,t,u,v,w;return b.__generator(this,function(b){switch(b.label){case 0:return h=(a=JSON.parse(JSON.stringify(c))).experiments,[4,e.getMetricsForSite(a,h)];case 1:for(j=0,i=b.sent(),k=h;j<k.length;j++){for(q=0,n=(m=k[j]).variants,o={},p={},r=n;q<r.length;q++)s=r[q],i.has(s)&&(t=i.get(s),d.default.isDev()&&(s.metrics=t),o[s.name]=null!==(f=t.exposures)&& void 0!==f?f:0,p[s.name]=null!==(g=t.conversions)&& void 0!==g?g:0);for(w in u=void 0,u=Object.keys(o).length>0&&Object.keys(p).length>0?(0,l.bandit)(o,p||{}):{},m.metrics={},v=function(b){var a=n.find(function(a){return a.name===b});a&&(a.prob=u[b])},u)v(w)}return[2,a]}})})}exports.DEFAULT_SERVER_OPTIONS={clientOrigins:null!==(a=d.default.IB_ORIGINS_ALLOWLIST)&& void 0!==a?a:"",allowMetricsPayloads:!1,maxBatchItemLength:c.METRICS_MAX_ITEM_LENGTH,maxBatchLength:c.METRICS_MAX_LENGTH,metrics:(0,f.getStubMetricsBackend)(),sessions:(0,g.getStubSessionsBackend)(),models:(0,e.getJsonSiteBackend)()},Object.freeze(exports.DEFAULT_SERVER_OPTIONS),exports.buildInstantBanditServer=h,exports.embedProbabilities=i,exports.TAG="[IB][server]",exports.log=function(){for(var c=[],a=0;a<arguments.length;a++)c[a]=arguments[a];console.info.apply(console,b.__spreadArray([exports.TAG],c,!0))},exports.getBanditServer=function(a){return d.default.isDev()&&global.defaultBanditServer&&(j=global.defaultBanditServer),j||(console.debug("[IB] Creating default InstantBanditServer..."),j=h(a)),d.default.isDev()&&(global.defaultBanditServer=j),j} |
@@ -1,44 +0,1 @@ | ||
"use strict"; | ||
var _a; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getInternalDevServer = void 0; | ||
var tslib_1 = require("tslib"); | ||
var environment_1 = tslib_1.__importDefault(require("./environment")); | ||
var server_core_1 = require("./server-core"); | ||
var json_sites_1 = require("./backends/json-sites"); | ||
var redis_1 = require("./backends/redis"); | ||
var redis = (0, redis_1.getRedisBackend)(); | ||
var json = (0, json_sites_1.getJsonSiteBackend)(); | ||
var DEFAULT_DEV_SERVER_OPTIONS = { | ||
clientOrigins: ((_a = environment_1.default.IB_ORIGINS_ALLOWLIST) !== null && _a !== void 0 ? _a : environment_1.default.IB_BASE_API_URL), | ||
sessions: redis, | ||
metrics: redis, | ||
models: json, | ||
}; | ||
/** | ||
* This server helper intended is intended for use by the Instant Bandit repo. | ||
* | ||
* Implementers who import the Instant Bandit package should create their own with | ||
* their desired configuration. | ||
* | ||
* See the note in `server-helpers` as to why we tack onto `global` here. | ||
* @private | ||
*/ | ||
var internalBanditServer; | ||
function getInternalDevServer(options) { | ||
if (options === void 0) { options = DEFAULT_DEV_SERVER_OPTIONS; } | ||
if (environment_1.default.isDev()) { | ||
if (global["internalBanditServer"]) { | ||
internalBanditServer = global["internalBanditServer"]; | ||
} | ||
} | ||
if (!internalBanditServer) { | ||
console.debug("[IB] Creating internal InstantBanditServer helper..."); | ||
internalBanditServer = (0, server_core_1.buildInstantBanditServer)(options); | ||
} | ||
if (environment_1.default.isDev()) { | ||
global["internalBanditServer"] = internalBanditServer; | ||
} | ||
return internalBanditServer; | ||
} | ||
exports.getInternalDevServer = getInternalDevServer; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getInternalDevServer=void 0;var a,j,b=require("tslib"),c=b.__importDefault(require("./environment")),e=require("./server-core"),f=require("./backends/json-sites"),g=require("./backends/redis"),d=(0,g.getRedisBackend)(),h=(0,f.getJsonSiteBackend)(),i=b.__assign(b.__assign({},e.DEFAULT_SERVER_OPTIONS),{clientOrigins:null!==(a=c.default.IB_ORIGINS_ALLOWLIST)&& void 0!==a?a:c.default.IB_BASE_API_URL,sessions:d,metrics:d,models:h});Object.freeze(i),exports.getInternalDevServer=function(a){return void 0===a&&(a=i),c.default.isDev()&&global.internalBanditServer&&(j=global.internalBanditServer),j||(console.debug("[IB] Creating internal InstantBanditServer helper..."),j=(0,e.buildInstantBanditServer)(a)),c.default.isDev()&&(global.internalBanditServer=j),j} |
@@ -10,5 +10,5 @@ /// <reference types="node" /> | ||
export declare function serverSideRenderedSite(server: InstantBanditServer, siteName: string, req: IncomingMessage & { | ||
cookies: { | ||
cookies: Partial<{ | ||
[key: string]: string; | ||
}; | ||
}>; | ||
}): Promise<{ | ||
@@ -15,0 +15,0 @@ site: import("../models").Site; |
@@ -1,108 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.serverSideRenderedSite = void 0; | ||
var tslib_1 = require("tslib"); | ||
var constants_1 = require("../constants"); | ||
var contexts_1 = require("../contexts"); | ||
var utils_1 = require("../utils"); | ||
var server_utils_1 = require("./server-utils"); | ||
/** | ||
* Handles details around serving a site in a manner suitable for a full SSR render. | ||
* The selection is persisted in the server's session store, and a session ID is | ||
* returned to the user via cookie. | ||
*/ | ||
function serverSideRenderedSite(server, siteName, req) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var validatedRequest, sid, sessions, session, err_1, _a, loader, metrics, ctx, siteConfig, site, experiment, variant, defer; | ||
return tslib_1.__generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: return [4 /*yield*/, server.init()]; | ||
case 1: | ||
_b.sent(); | ||
return [4 /*yield*/, (0, server_utils_1.validateUserRequest)({ | ||
allowNoSession: true, | ||
allowedOrigins: server.origins, | ||
headers: req.headers, | ||
siteName: siteName, | ||
url: req.url, | ||
})]; | ||
case 2: | ||
validatedRequest = _b.sent(); | ||
if ((0, utils_1.exists)(req.cookies[constants_1.HEADER_SESSION_ID])) { | ||
validatedRequest.sid = req.cookies[constants_1.HEADER_SESSION_ID]; | ||
} | ||
sid = validatedRequest.sid; | ||
sessions = server.sessions; | ||
_b.label = 3; | ||
case 3: | ||
_b.trys.push([3, 7, , 8]); | ||
if (!((0, utils_1.exists)(sid) && server.isBackendConnected(server.sessions))) return [3 /*break*/, 5]; | ||
return [4 /*yield*/, sessions.getOrCreateSession(validatedRequest)]; | ||
case 4: | ||
session = _b.sent(); | ||
return [3 /*break*/, 6]; | ||
case 5: | ||
session = (0, utils_1.makeNewSession)(); | ||
_b.label = 6; | ||
case 6: return [3 /*break*/, 8]; | ||
case 7: | ||
err_1 = _b.sent(); | ||
console.log("[IB] Error fetching session for '".concat(sid, "': ").concat(err_1)); | ||
session = (0, utils_1.makeNewSession)(); | ||
return [3 /*break*/, 8]; | ||
case 8: | ||
_a = contexts_1.DEFAULT_BANDIT_OPTIONS.providers, loader = _a.loader, metrics = _a.metrics; | ||
if (!server.isBackendConnected(server.sessions)) { | ||
ctx = (0, contexts_1.createBanditContext)(); | ||
} | ||
else { | ||
ctx = (0, contexts_1.createBanditContext)({ | ||
providers: { | ||
loader: loader, | ||
metrics: metrics, | ||
// | ||
// Here we inject the session from the server's asynchronous session store into | ||
// the InstantBandit component's *synchronous* store. This is done for SSR. | ||
// | ||
// In order to complete the render entirely on the server and avoid client-side | ||
// hydration, the component needs to render synchronously in one pass. | ||
// | ||
session: function () { | ||
return { | ||
id: session.sid, | ||
getOrCreateSession: function () { return session; }, | ||
hasSeen: function () { | ||
return false; | ||
}, | ||
persistVariant: function () { | ||
// We do this server side below | ||
return; | ||
}, | ||
save: function () { return session; }, | ||
}; | ||
}, | ||
}, | ||
}); | ||
} | ||
return [4 /*yield*/, server.getSite(validatedRequest)]; | ||
case 9: | ||
siteConfig = (_b.sent()).site; | ||
return [4 /*yield*/, ctx.init(siteConfig)]; | ||
case 10: | ||
site = _b.sent(); | ||
experiment = ctx.experiment, variant = ctx.variant; | ||
return [4 /*yield*/, server.sessions.markVariantSeen(session, site.name, experiment.id, variant.name) | ||
.catch(function (err) { return console.warn("[IB]: Error marking variant '".concat(variant.name, "' seen: ").concat(err)); })]; | ||
case 11: | ||
_b.sent(); | ||
defer = server.isBackendConnected(sessions) === false; | ||
return [2 /*return*/, { | ||
site: site, | ||
select: defer ? null : variant.name, | ||
defer: defer, | ||
}]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.serverSideRenderedSite = serverSideRenderedSite; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.serverSideRenderedSite=void 0;var a=require("tslib"),b=require("../constants"),c=require("../contexts"),d=require("../utils"),e=require("./server-utils");exports.serverSideRenderedSite=function(f,g,h){return a.__awaiter(this,void 0,void 0,function(){var i,j,k,l,m,n,o,p,q,r,s,t,u,v;return a.__generator(this,function(a){switch(a.label){case 0:return[4,f.init()];case 1:return a.sent(),[4,(0,e.validateUserRequest)({allowNoSession:!0,allowedOrigins:f.origins,headers:h.headers,siteName:g,url:h.url})];case 2:i=a.sent(),(0,d.exists)(h.cookies[b.HEADER_SESSION_ID])&&(i.sid=h.cookies[b.HEADER_SESSION_ID]),j=i.sid,k=f.sessions,a.label=3;case 3:if(a.trys.push([3,7,,8]),!((0,d.exists)(j)&&f.isBackendConnected(f.sessions)))return[3,5];return[4,k.getOrCreateSession(i)];case 4:return l=a.sent(),[3,6];case 5:l=(0,d.makeNewSession)(),a.label=6;case 6:return[3,8];case 7:return m=a.sent(),console.log("[IB] Error fetching session for '".concat(j,"': ").concat(m)),l=(0,d.makeNewSession)(),[3,8];case 8:return o=(n=c.DEFAULT_BANDIT_OPTIONS.providers).loader,p=n.metrics,q=f.isBackendConnected(f.sessions)?(0,c.createBanditContext)({providers:{loader:o,metrics:p,session:function(){return{id:l.sid,getOrCreateSession:function(){return l},hasSeen:function(){return!1},persistVariant:function(){},save:function(){return l}}}}}):(0,c.createBanditContext)(),[4,f.getSite(i)];case 9:return r=a.sent().site,[4,q.init(r)];case 10:return s=a.sent(),t=q.experiment,u=q.variant,[4,f.sessions.markVariantSeen(l,s.name,t.id,u.name).catch(function(a){return console.warn("[IB]: Error marking variant '".concat(u.name,"' seen: ").concat(a))})];case 11:return a.sent(),[2,{site:s,select:(v=!1===f.isBackendConnected(k))?null:u.name,defer:v}]}})})} |
@@ -14,2 +14,5 @@ /// <reference types="node" /> | ||
clientOrigins: string | string[]; | ||
allowMetricsPayloads: boolean; | ||
maxBatchItemLength: number; | ||
maxBatchLength: number; | ||
}; | ||
@@ -21,2 +24,3 @@ /** | ||
export declare type InstantBanditServer = { | ||
readonly options: InstantBanditServerOptions; | ||
readonly origins: Origins; | ||
@@ -124,1 +128,9 @@ readonly metrics: MetricsBackend; | ||
}; | ||
/** | ||
* Options for decoding metrics batches | ||
*/ | ||
export declare type MetricsDecodeOptions = { | ||
maxBatchLength: number; | ||
maxBatchItemLength: number; | ||
allowMetricsPayloads: boolean; | ||
}; |
@@ -1,3 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var constants_1 = require("../constants"); | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),require("../constants") |
@@ -1,5 +0,16 @@ | ||
import { InstantBanditHeaders, Origins, RequestValidationArgs, ValidatedRequest } from "./server-types"; | ||
import { InstantBanditHeaders, MetricsDecodeOptions, Origins, RequestValidationArgs, ValidatedRequest } from "./server-types"; | ||
import { MetricsBatch } from "../models"; | ||
import { SessionDescriptor } from "../types"; | ||
/** | ||
* Decodes an encoded `MetricsBatch` from text/plain ND-JSON | ||
*/ | ||
export declare function decodeMetricsBatch(text: string, options: MetricsDecodeOptions): MetricsBatch; | ||
/** | ||
* Ensures that an object contains only the given keys | ||
* @param obj | ||
* @param allowed | ||
* @returns | ||
*/ | ||
export declare function allowKeys<T>(obj: T, allowed: (keyof T)[], maxLength?: number): void; | ||
/** | ||
* Emits a cookie for the session including any configured settings for cookies | ||
@@ -6,0 +17,0 @@ * @param session |
@@ -1,188 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.toNumber = exports.makeKey = exports.randomId = exports.validateMetricsBatch = exports.validateClientReportedOrigin = exports.getSessionIdFromHeaders = exports.createNewClientSession = exports.validateUserRequest = exports.normalizeOrigins = exports.emitCookie = void 0; | ||
var tslib_1 = require("tslib"); | ||
var crypto_1 = require("crypto"); | ||
var environment_1 = tslib_1.__importDefault(require("./environment")); | ||
var constants = tslib_1.__importStar(require("../constants")); | ||
var utils_1 = require("../utils"); | ||
var defaults_1 = require("../defaults"); | ||
/** | ||
* Emits a cookie for the session including any configured settings for cookies | ||
* @param session | ||
* @returns | ||
*/ | ||
function emitCookie(req, session) { | ||
var settings = environment_1.default.IB_COOKIE_SETTINGS; | ||
return "".concat(constants.HEADER_SESSION_ID, "=").concat(session.sid, "; ").concat(settings); | ||
} | ||
exports.emitCookie = emitCookie; | ||
/** | ||
* Normalizes an array of origins and produces a map for quick lookup. | ||
* Origin lookups are to prevent x-domain POST attacks and to enable proper CORS access. | ||
* @returns | ||
*/ | ||
function normalizeOrigins(originsArg, injected) { | ||
if (injected === void 0) { injected = []; } | ||
if (!(0, utils_1.exists)(originsArg)) { | ||
return new Map(); | ||
} | ||
var originsListRaw = Array.isArray(originsArg) ? originsArg : [originsArg]; | ||
var originsList = originsListRaw | ||
.concat(injected) | ||
.join(",") | ||
.split(",") | ||
.map(function (o) { return o.trim(); }) | ||
.map(function (o) { return o.toLowerCase(); }) | ||
.map(function (o) { return o; }); | ||
var origins = new Map(); | ||
originsList | ||
.reduce(function (p, entry) { return origins.set(entry, { name: entry }); }, origins); | ||
return origins; | ||
} | ||
exports.normalizeOrigins = normalizeOrigins; | ||
/** | ||
* Ensures that an incoming user request is well-formed and bears the appropriate information. | ||
* Note: Does not validate the session itself. | ||
*/ | ||
function validateUserRequest(args) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var headers, allowedOrigins, allowNoSession, requireOrigin, siteName, origin, allowed, sid; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
headers = args.headers, allowedOrigins = args.allowedOrigins, allowNoSession = args.allowNoSession, requireOrigin = args.requireOrigin, siteName = args.siteName; | ||
origin = (0, utils_1.exists)(headers["origin"]) ? headers["origin"] : null; | ||
// Null origins allowed by default | ||
if (origin !== null || requireOrigin === true) { | ||
allowed = validateClientReportedOrigin(allowedOrigins, origin); | ||
if (!allowed) { | ||
console.warn("[IB] Invalid request for '".concat(args.url, "' from origin '").concat(origin, "'")); | ||
// Intentionally vague on error response | ||
throw new Error("Invalid origin"); | ||
} | ||
} | ||
return [4 /*yield*/, getSessionIdFromHeaders(headers)]; | ||
case 1: | ||
sid = _a.sent(); | ||
if (!allowNoSession && !(0, utils_1.exists)(sid)) { | ||
throw new Error("Missing session"); | ||
} | ||
return [2 /*return*/, { | ||
sid: sid || "", | ||
origin: origin !== null && origin !== void 0 ? origin : "null", | ||
headers: headers, | ||
siteName: siteName || defaults_1.DEFAULT_SITE.name, | ||
session: null, | ||
}]; | ||
} | ||
}); | ||
}); | ||
} | ||
exports.validateUserRequest = validateUserRequest; | ||
function createNewClientSession() { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var sid, session; | ||
return tslib_1.__generator(this, function (_a) { | ||
sid = (0, crypto_1.randomUUID)(); | ||
session = (0, utils_1.makeNewSession)(sid); | ||
return [2 /*return*/, session]; | ||
}); | ||
}); | ||
} | ||
exports.createNewClientSession = createNewClientSession; | ||
function getSessionIdFromHeaders(headers) { | ||
var _a; | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var id; | ||
return tslib_1.__generator(this, function (_b) { | ||
id = (0, utils_1.getCookie)(constants.HEADER_SESSION_ID, headers["cookie"]); | ||
if (!(0, utils_1.exists)(id)) { | ||
id = (_a = headers[constants.HEADER_SESSION_ID]) !== null && _a !== void 0 ? _a : null; | ||
} | ||
return [2 /*return*/, id]; | ||
}); | ||
}); | ||
} | ||
exports.getSessionIdFromHeaders = getSessionIdFromHeaders; | ||
/** | ||
* Checks a client's reported "origin" header against an allowlist | ||
* @param allowedOrigins | ||
* @param origin | ||
* @returns | ||
*/ | ||
function validateClientReportedOrigin(allowedOrigins, origin) { | ||
if (!(0, utils_1.exists)(origin)) { | ||
origin = "null"; | ||
} | ||
return allowedOrigins.has(origin); | ||
} | ||
exports.validateClientReportedOrigin = validateClientReportedOrigin; | ||
/** | ||
* Validates a batch of metrics by ensuring they match their request | ||
* @param req | ||
* @param batch | ||
* @returns | ||
*/ | ||
function validateMetricsBatch(req, batch) { | ||
var sid = req.sid; | ||
if (!(0, utils_1.exists)(sid)) { | ||
throw new Error("Missing or invalid session for metrics"); | ||
} | ||
return batch; | ||
} | ||
exports.validateMetricsBatch = validateMetricsBatch; | ||
/** | ||
* Generates a cryptographic quality random identifier | ||
* @param len | ||
* @returns | ||
*/ | ||
function randomId(len) { | ||
if (len === void 0) { len = 16; } | ||
return (0, crypto_1.randomBytes)(len).toString("base64url"); | ||
} | ||
exports.randomId = randomId; | ||
/** | ||
* Prefixes a storage key with top-level key space identifiers, e.g. the site and experiment name | ||
* @param pieces | ||
* @returns | ||
*/ | ||
function makeKey(pieces) { | ||
if (pieces.length < 1) { | ||
throw new Error("Expected key fragments"); | ||
} | ||
// Enforce a max key length | ||
pieces.reduce(function (length, piece, ix) { | ||
if (!(0, utils_1.exists)(piece)) { | ||
return length; | ||
} | ||
length += piece.length + ix; | ||
if (length > constants.MAX_STORAGE_KEY_LENGTH) { | ||
throw new Error("Maximum storage key size exceeded at length ".concat(length)); | ||
} | ||
return length; | ||
}, 0); | ||
return pieces.map(function (p) { return p.replaceAll(":", "_"); }).join(":"); | ||
} | ||
exports.makeKey = makeKey; | ||
/** | ||
* Converts a string to a number. | ||
* If the string has a decimal point, `parseFloat` is used. | ||
* If a number is supplied, returns the number. | ||
* If `null` or `undefined` is passed, returns 0 | ||
*/ | ||
function toNumber(val) { | ||
if (typeof val === "string") { | ||
return (val.indexOf(".") > -1) ? parseFloat(val) : parseInt(val); | ||
} | ||
else if (typeof val === "number") { | ||
return val; | ||
} | ||
else if (val === null || val === undefined) { | ||
return 0; | ||
} | ||
else { | ||
throw new Error("Invalid value '".concat(val, "' interpreted as number")); | ||
} | ||
} | ||
exports.toNumber = toNumber; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.toNumber=exports.makeKey=exports.randomId=exports.validateMetricsBatch=exports.validateClientReportedOrigin=exports.getSessionIdFromHeaders=exports.createNewClientSession=exports.validateUserRequest=exports.normalizeOrigins=exports.emitCookie=exports.allowKeys=exports.decodeMetricsBatch=void 0;var a=require("tslib"),e=require("crypto"),f=a.__importDefault(require("./environment")),g=a.__importStar(require("../constants")),h=require("../utils"),i=require("../defaults");function b(b,c,a){void 0===a&&(a=g.MAX_STORAGE_VALUE_LENGTH),Object.keys(b).forEach(function(d){if(c.includes(d)&&b[d].toString().length<=a)return!0;throw Error("Invalid key '".concat(d.toString(),"'"))})}function c(b){var c;return a.__awaiter(this,void 0,void 0,function(){var d;return a.__generator(this,function(a){return d=(0,h.getCookie)(g.HEADER_SESSION_ID,b.cookie),(0,h.exists)(d)||(d=null!==(c=b[g.HEADER_SESSION_ID])&& void 0!==c?c:null),[2,d]})})}function d(b,a){return"true"!==f.default.IB_ENFORCE_ORIGIN_CHECK||((0,h.exists)(a)||(a="null"),b.has(a))}exports.decodeMetricsBatch=function(j,f){var o=f.allowMetricsPayloads,h=f.maxBatchItemLength,p=f.maxBatchLength;if(j.length>p)throw Error("Invalid metrics batch length");var k=j.split("\n"),l=k[0],i=k.slice(1),m=[];if(l.length>h)throw Error("Invalid metrics batch header");var c=JSON.parse(l);b(c,["site","experiment","variant"]);for(var d=0;d<i.length;++d){var e=i[d];if(e.length>h)throw Error("Batch item exceeds max length");var a=JSON.parse(e);if(b(a,["ts","name","payload"]),1===a.payload){var n=i[++d];if(n.length>h)console.warn("Batch item payload too large. Item: '".concat(e,"'")),a.payload=g.METRICS_PAYLOAD_SIZE_ERR;else if(o){var q=JSON.parse(n);a.payload=q}else console.warn("[IB] Received metrics payload for item '".concat(e,"'...ignoring")),a.payload=g.METRICS_PAYLOAD_IGNORED}m.push(a)}var r=c.site,s=c.experiment,t=c.variant;return{site:r,experiment:s,variant:t,entries:m}},exports.allowKeys=b,exports.emitCookie=function(c,a){var b=f.default.IB_COOKIE_SETTINGS;return"".concat(g.HEADER_SESSION_ID,"=").concat(a.sid,"; ").concat(b)},exports.normalizeOrigins=function(a,b){if(void 0===b&&(b=[]),!(0,h.exists)(a))return new Map;var d=(Array.isArray(a)?a:[a]).concat(b).join(",").split(",").map(function(a){return a.trim()}).map(function(a){return a.toLowerCase()}).map(function(a){return a}),c=new Map;return d.reduce(function(b,a){return c.set(a,{name:a})},c),c},exports.validateUserRequest=function(b){return a.__awaiter(this,void 0,void 0,function(){var e,f,g,j,k,l,m,n;return a.__generator(this,function(a){switch(a.label){case 0:if(e=b.headers,f=b.allowedOrigins,g=b.allowNoSession,j=b.requireOrigin,k=b.siteName,(null!==(l=(0,h.exists)(e.origin)?e.origin:null)|| !0===j)&&!(m=d(f,l)))throw console.warn("[IB] Invalid request for '".concat(b.url,"' from origin '").concat(l,"'")),Error("Invalid origin");return[4,c(e)];case 1:if(n=a.sent(),!g&&!(0,h.exists)(n))throw Error("Missing session");return[2,{sid:n||"",origin:null!=l?l:"null",headers:e,siteName:k||i.DEFAULT_SITE.name,session:null}]}})})},exports.createNewClientSession=function(){return a.__awaiter(this,void 0,void 0,function(){var b,c;return a.__generator(this,function(a){return b=(0,e.randomUUID)(),[2,c=(0,h.makeNewSession)(b)]})})},exports.getSessionIdFromHeaders=c,exports.validateClientReportedOrigin=d,exports.validateMetricsBatch=function(a,b){var c=a.sid;if(!(0,h.exists)(c))throw Error("Missing or invalid session for metrics");return b},exports.randomId=function(a){return void 0===a&&(a=16),(0,e.randomBytes)(a).toString("base64url")},exports.makeKey=function(a){if(a.length<1)throw Error("Expected key fragments");return a.reduce(function(a,b,c){if(!(0,h.exists)(b))return a;if((a+=b.length+c)>g.MAX_STORAGE_KEY_LENGTH)throw Error("Maximum storage key size exceeded at length ".concat(a));return a},0),a.map(function(a){return a.replaceAll(":","_")}).join(":")},exports.toNumber=function(a){if("string"==typeof a)return a.indexOf(".")> -1?parseFloat(a):parseInt(a);if("number"==typeof a)return a;if(null==a)return 0;throw Error("Invalid value '".concat(a,"' interpreted as number"))} |
@@ -7,4 +7,2 @@ /// <reference types="node" /> | ||
export declare type InstantBanditProps = { | ||
preserveSession?: boolean; | ||
probabilities?: ProbabilityDistribution | null; | ||
siteName?: string; | ||
@@ -14,3 +12,2 @@ select?: string; | ||
defer?: boolean; | ||
debug?: boolean; | ||
options?: InstantBanditOptions; | ||
@@ -17,0 +14,0 @@ timeout?: number; |
@@ -1,19 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Algorithm = exports.LoadState = void 0; | ||
var LoadState; | ||
(function (LoadState) { | ||
LoadState["PRELOAD"] = "pre"; | ||
LoadState["WAIT"] = "wait-for-data"; | ||
LoadState["SELECTING"] = "selecting"; | ||
LoadState["READY"] = "ready"; | ||
})(LoadState = exports.LoadState || (exports.LoadState = {})); | ||
/** | ||
* Algorithms to use when selecting a variant. | ||
*/ | ||
var Algorithm; | ||
(function (Algorithm) { | ||
Algorithm["DEFAULT"] = "default"; | ||
Algorithm["RANDOM"] = "random"; | ||
Algorithm["MAB_EPSILON_GREEDY"] = "mab-epsilon-greedy"; | ||
})(Algorithm = exports.Algorithm || (exports.Algorithm = {})); | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Algorithm=exports.LoadState=void 0,function(a){a.PRELOAD="pre",a.WAIT="wait-for-data",a.SELECTING="selecting",a.READY="ready"}(exports.LoadState||(exports.LoadState={})),function(a){a.DEFAULT="default",a.RANDOM="random",a.MAB_EPSILON_GREEDY="mab-epsilon-greedy"}(exports.Algorithm||(exports.Algorithm={})) |
import { useLayoutEffect } from "react"; | ||
import { MetricsBatch } from "./models"; | ||
import { SessionDescriptor } from "./types"; | ||
/** | ||
* Encodes a batch of metrics entries as ND-JSON lines. | ||
* The first line is the "header" and represents the batch metadata. | ||
* Subsequent lines represents the batch entries. | ||
* The field `payload` is set to 1` based on the existence of a payload object. | ||
* If a payload is present, it is represented by the next line. | ||
* @param batch | ||
*/ | ||
export declare function encodeMetricsBatch(batch: MetricsBatch): string; | ||
export declare function makeNewSession(sid?: string): SessionDescriptor; | ||
@@ -4,0 +14,0 @@ export declare function markVariantInSession(session: SessionDescriptor, site: string, experiment: string, variant: string): void; |
@@ -1,127 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.flushPromises = exports.useIsomorphicLayoutEffect = exports.isBrowserEnvironment = exports.getBaseUrl = exports.getCookie = exports.env = exports.exists = exports.deepFreeze = exports.markVariantInSession = exports.makeNewSession = void 0; | ||
var tslib_1 = require("tslib"); | ||
var react_1 = require("react"); | ||
var constants = tslib_1.__importStar(require("./constants")); | ||
function makeNewSession(sid) { | ||
if (sid === void 0) { sid = ""; } | ||
var session = { | ||
sid: sid, | ||
selections: {}, | ||
}; | ||
return session; | ||
} | ||
exports.makeNewSession = makeNewSession; | ||
function markVariantInSession(session, site, experiment, variant) { | ||
var _a; | ||
var sites = session.selections; | ||
if (!sites) { | ||
sites = session.selections = {}; | ||
} | ||
var experiments = sites[site]; | ||
if (!experiments) { | ||
experiments = sites[site] = (_a = {}, | ||
_a[experiment] = [], | ||
_a); | ||
} | ||
var variants = experiments[experiment]; | ||
if (!exists(variants)) { | ||
variants = experiments[experiment] = []; | ||
} | ||
// Put the most recently presented variant at the end | ||
var ix = variants.indexOf(variant); | ||
if (ix > -1) { | ||
variants.splice(ix, 1); | ||
} | ||
variants.push(variant); | ||
} | ||
exports.markVariantInSession = markVariantInSession; | ||
/** | ||
* Freezes an entire object tree | ||
* @param obj | ||
* @returns | ||
*/ | ||
function deepFreeze(obj, seen) { | ||
if (seen === void 0) { seen = new WeakMap(); } | ||
if (!exists(obj)) { | ||
return obj; | ||
} | ||
seen.set(obj, true); | ||
Object.getOwnPropertyNames(obj) | ||
.filter(function (prop) { return !seen.has(obj[prop]); }) | ||
.filter(function (prop) { return obj[prop] && typeof obj[prop] === "object"; }) | ||
.forEach(function (prop) { return deepFreeze(obj[prop], seen); }); | ||
return Object.freeze(obj); | ||
} | ||
exports.deepFreeze = deepFreeze; | ||
/** | ||
* Returns `true` if something is non-null and non-undefined | ||
* @param thing | ||
* @returns | ||
*/ | ||
function exists(thing) { | ||
return (thing !== null && thing !== undefined); | ||
} | ||
exports.exists = exists; | ||
/** | ||
* Pulls an environment variable either from a server environment | ||
* or from Next.js' public env vars exposed to the client. | ||
* @param name | ||
* @returns | ||
*/ | ||
function env(name) { | ||
if (typeof process === "undefined") { | ||
return undefined; | ||
} | ||
else if (exports.isBrowserEnvironment) { | ||
return process.env[constants.NEXTJS_PUBLIC_PREFIX + name]; | ||
} | ||
else { | ||
return process.env[name]; | ||
} | ||
} | ||
exports.env = env; | ||
/** | ||
* Extracts a cookie by name from a request header value | ||
* @param cookieName | ||
* @param str | ||
* @returns | ||
*/ | ||
function getCookie(cookieName, str) { | ||
if (str === void 0) { str = ""; } | ||
var fullStr = ""; | ||
if (typeof document === "undefined" && str === "") { | ||
return null; | ||
} | ||
else if (str !== "") { | ||
fullStr = str; | ||
} | ||
else { | ||
fullStr = document.cookie; | ||
} | ||
var cookie = fullStr | ||
.split(";") | ||
.map(function (cookie) { return cookie.trim(); }) | ||
.filter(function (cookie) { return cookie.indexOf(cookieName) === 0; }) | ||
.map(function (cookie) { return cookie.substring(cookieName.length + 1, cookie.length); }) | ||
.pop(); | ||
return cookie !== null && cookie !== void 0 ? cookie : null; | ||
} | ||
exports.getCookie = getCookie; | ||
/** | ||
* Gets the base URL, observing environment variables if in a Node environment | ||
* @returns | ||
*/ | ||
function getBaseUrl() { | ||
var _a; | ||
return (_a = env(constants.VARNAME_BASE_URL)) !== null && _a !== void 0 ? _a : constants.DEFAULT_BASE_URL; | ||
} | ||
exports.getBaseUrl = getBaseUrl; | ||
exports.isBrowserEnvironment = typeof window !== "undefined"; | ||
exports.useIsomorphicLayoutEffect = exports.isBrowserEnvironment ? react_1.useLayoutEffect : react_1.useEffect; | ||
var flushPromises = function () { return tslib_1.__awaiter(void 0, void 0, void 0, function () { return tslib_1.__generator(this, function (_a) { | ||
return [2 /*return*/, new Promise(function (resolve) { scheduler(resolve); })]; | ||
}); }); }; | ||
exports.flushPromises = flushPromises; | ||
var scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.flushPromises=exports.useIsomorphicLayoutEffect=exports.isBrowserEnvironment=exports.getBaseUrl=exports.getCookie=exports.env=exports.exists=exports.deepFreeze=exports.markVariantInSession=exports.makeNewSession=exports.encodeMetricsBatch=void 0;var b=require("tslib"),a=require("react"),f=b.__importStar(require("./constants"));function c(a,b){return(void 0===b&&(b=new WeakMap),function(a){return null!=a}(a))?(b.set(a,!0),Object.getOwnPropertyNames(a).filter(function(c){return!b.has(a[c])}).filter(function(b){return a[b]&&"object"==typeof a[b]}).forEach(function(d){return c(a[d],b)}),Object.freeze(a)):a}function d(a){return null!=a}function e(a){return"undefined"==typeof process?void 0:exports.isBrowserEnvironment?process.env[f.NEXTJS_PUBLIC_PREFIX+a]:process.env[a]}exports.encodeMetricsBatch=function(a){var c=a.site,d=a.experiment,e=a.variant,f=a.entries,g=JSON.stringify({site:c,experiment:d,variant:e}),h=f.map(function(a){var f=a.name,g=a.ts,b=a.payload,c=function(a){return null!=a}(b),d={ts:g,name:f,payload:void 0};c&&(d.payload=1);var e=JSON.stringify(d);return c?e+"\n"+JSON.stringify(b):e});return b.__spreadArray([g],h,!0).join("\n")},exports.makeNewSession=function(a){return void 0===a&&(a=""),{sid:a,selections:{}}},exports.markVariantInSession=function(e,f,d,g){var h,b=e.selections;b||(b=e.selections={});var c=b[f];c||(c=b[f]=((h={})[d]=[],h));var i,a=c[d];i=a,null!=i||(a=c[d]=[]);var j=a.indexOf(g);j> -1&&a.splice(j,1),a.push(g)},exports.deepFreeze=c,exports.exists=d,exports.env=e,exports.getCookie=function(d,a){void 0===a&&(a="");var c="";if("undefined"==typeof document&&""===a)return null;var b=(c=""!==a?a:document.cookie).split(";").map(function(a){return a.trim()}).filter(function(a){return 0===a.indexOf(d)}).map(function(a){return a.substring(d.length+1,a.length)}).pop();return null!=b?b:null},exports.getBaseUrl=function(){var a;return null!==(a=e(f.VARNAME_BASE_URL))&& void 0!==a?a:f.DEFAULT_BASE_URL},exports.isBrowserEnvironment="undefined"!=typeof window,exports.useIsomorphicLayoutEffect=exports.isBrowserEnvironment?a.useLayoutEffect:a.useEffect,exports.flushPromises=function(){return b.__awaiter(void 0,void 0,void 0,function(){return b.__generator(this,function(a){return[2,new Promise(function(a){g(a)})]})})};var g="function"==typeof setImmediate?setImmediate:setTimeout |
@@ -1,72 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createMetricsEndpoint = void 0; | ||
var tslib_1 = require("tslib"); | ||
var server_internal_1 = require("../../lib/server/server-internal"); | ||
var server_utils_1 = require("../../lib/server/server-utils"); | ||
var MetricsEndpoint = createMetricsEndpoint(); | ||
exports.default = MetricsEndpoint; | ||
/** | ||
* Creates a Next.js endpoint for ingesting metrics. | ||
* Remember to return the resulting endpoint function as your module's default export. | ||
* See [metrics].ts. | ||
*/ | ||
function createMetricsEndpoint(server) { | ||
// This endpoint accepts POST requests bearing batches of metrics to ingest. | ||
// In development environments, shows site metrics on GET | ||
function handleMetricsRequest(req, res) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var metrics, origins, sessions, method, headers, sid, needsSession, batch, validatedReq, session; | ||
return tslib_1.__generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!server) { | ||
server = (0, server_internal_1.getInternalDevServer)(); | ||
} | ||
return [4 /*yield*/, server.init()]; | ||
case 1: | ||
_a.sent(); | ||
metrics = server.metrics, origins = server.origins, sessions = server.sessions; | ||
method = req.method, headers = req.headers; | ||
if (server.isBackendConnected(metrics) === false) { | ||
res.status(503).end(); | ||
return [2 /*return*/]; | ||
} | ||
return [4 /*yield*/, (0, server_utils_1.getSessionIdFromHeaders)(headers)]; | ||
case 2: | ||
sid = _a.sent(); | ||
needsSession = (!sid || sid === ""); | ||
if (!(method === "POST")) return [3 /*break*/, 6]; | ||
batch = req.body; | ||
return [4 /*yield*/, (0, server_utils_1.validateUserRequest)({ | ||
allowedOrigins: origins, | ||
headers: headers, | ||
url: req.url, | ||
allowNoSession: true, | ||
siteName: batch.site, | ||
})]; | ||
case 3: | ||
validatedReq = _a.sent(); | ||
return [4 /*yield*/, sessions.getOrCreateSession(validatedReq)]; | ||
case 4: | ||
session = _a.sent(); | ||
validatedReq.session = session; | ||
return [4 /*yield*/, metrics.ingestBatch(validatedReq, req.body)]; | ||
case 5: | ||
_a.sent(); | ||
// Grant a session if the request needs one (is new), or if we created a new one | ||
if (needsSession || session.sid !== sid) { | ||
res.setHeader("Set-Cookie", (0, server_utils_1.emitCookie)(validatedReq, session)); | ||
} | ||
res.status(200).json(session); | ||
return [2 /*return*/]; | ||
case 6: | ||
res.status(400).end(); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
return handleMetricsRequest; | ||
} | ||
exports.createMetricsEndpoint = createMetricsEndpoint; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.createMetricsEndpoint=void 0;var c=require("tslib"),d=require("../../lib/server/server-internal"),e=require("../../lib/server/server-utils"),f=require("../../lib/utils"),g=require("../../lib/constants"),a=b();function b(a){return function(b,h){return c.__awaiter(this,void 0,void 0,function(){var i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;return c.__generator(this,function(c){switch(c.label){case 0:return a||(a=(0,d.getInternalDevServer)()),[4,a.init()];case 1:if(c.sent(),i=a.metrics,j=a.options,k=a.origins,l=a.sessions,m=b.method,n=b.headers,o=j.allowMetricsPayloads,p=j.maxBatchLength,q=j.maxBatchItemLength,!1===a.isBackendConnected(i))return h.status(503).end(),[2];return[4,(0,e.getSessionIdFromHeaders)(n)];case 2:if(s=!(r=c.sent())||""===r,"POST"!==m)return[3,9];if(t=b.headers["content-length"],(0,f.exists)(t)&&parseInt(t)>p)return console.warn("[IB] Recived metrics payload larger than ".concat(p," bytes")),h.status(400).end(),[2];u=b.body,v={allowMetricsPayloads:!!o,maxBatchLength:null!=p?p:g.METRICS_MAX_LENGTH,maxBatchItemLength:null!=q?q:g.METRICS_MAX_ITEM_LENGTH},w=void 0,x=void 0,c.label=3;case 3:return c.trys.push([3,5,,6]),w=(0,e.decodeMetricsBatch)(u,v),[4,(0,e.validateUserRequest)({allowedOrigins:k,headers:n,url:b.url,allowNoSession:!0,siteName:w.site})];case 4:return x=c.sent(),[3,6];case 5:return y=c.sent(),console.warn("[IB] Error handling incoming metrics: ".concat(y)),h.status(400).end(),[2];case 6:return[4,l.getOrCreateSession(x)];case 7:return z=c.sent(),x.session=z,[4,i.ingestBatch(x,w)];case 8:return c.sent(),(s||z.sid!==r)&&h.setHeader("Set-Cookie",(0,e.emitCookie)(x,z)),h.status(200).json(z),[2];case 9:return h.status(400).end(),[2]}})})}}exports.default=a,exports.createMetricsEndpoint=b |
@@ -1,76 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createSiteEndpoint = void 0; | ||
var tslib_1 = require("tslib"); | ||
var environment_1 = tslib_1.__importDefault(require("../../../lib/server/environment")); | ||
var defaults_1 = require("../../../lib/defaults"); | ||
var utils_1 = require("../../../lib/utils"); | ||
var server_utils_1 = require("../../../lib/server/server-utils"); | ||
var server_internal_1 = require("../../../lib/server/server-internal"); | ||
var SiteEndpoint = createSiteEndpoint(); | ||
exports.default = SiteEndpoint; | ||
/** | ||
* Creates a Next.js endpoint for serving site configurations. | ||
* Remember to return the resulting endpoint function as your module's default export. | ||
* See [siteName].ts. | ||
*/ | ||
function createSiteEndpoint(server) { | ||
// This endpoint serves site configurations by name and creates user sessions. | ||
// Site are hydrated with variant probabilities inlined. | ||
// If a site is not found, the default site is returned. | ||
// If the user does not have a session, one is created. | ||
// Session IDs are transmitted via a 1st-party cookie. | ||
function handleSiteRequest(req, res) { | ||
return tslib_1.__awaiter(this, void 0, void 0, function () { | ||
var siteNameParam, origins, url, headers, siteName, validatedRequest, _a, site, responseHeaders; | ||
return tslib_1.__generator(this, function (_b) { | ||
switch (_b.label) { | ||
case 0: | ||
if (!server) { | ||
server = (0, server_internal_1.getInternalDevServer)(); | ||
} | ||
return [4 /*yield*/, server.init()]; | ||
case 1: | ||
_b.sent(); | ||
siteNameParam = req.query.siteName; | ||
origins = server.origins; | ||
url = req.url, headers = req.headers; | ||
siteName = defaults_1.DEFAULT_SITE.name; | ||
if ((0, utils_1.exists)(siteNameParam)) { | ||
if (Array.isArray(siteNameParam)) { | ||
siteName = siteNameParam.join(""); | ||
} | ||
else { | ||
siteName = siteNameParam; | ||
} | ||
} | ||
return [4 /*yield*/, (0, server_utils_1.validateUserRequest)({ | ||
url: url, | ||
headers: headers, | ||
allowedOrigins: origins, | ||
siteName: siteName, | ||
// No session required for a site request. One will be created if need be. | ||
allowNoSession: true, | ||
})]; | ||
case 2: | ||
validatedRequest = _b.sent(); | ||
return [4 /*yield*/, server.getSite(validatedRequest)]; | ||
case 3: | ||
_a = _b.sent(), site = _a.site, responseHeaders = _a.responseHeaders; | ||
// Relay headers | ||
Object.keys(responseHeaders) | ||
.forEach(function (header) { var _a; return res.setHeader(header, (_a = responseHeaders[header]) !== null && _a !== void 0 ? _a : ""); }); | ||
if (environment_1.default.isDev()) { | ||
res.status(200).send(JSON.stringify(site, null, 2)); | ||
} | ||
else { | ||
res.status(200).send(site); | ||
} | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
return handleSiteRequest; | ||
} | ||
exports.createSiteEndpoint = createSiteEndpoint; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.createSiteEndpoint=void 0;var a=require("tslib"),d=a.__importDefault(require("../../../lib/server/environment")),e=require("../../../lib/defaults"),f=require("../../../lib/utils"),g=require("../../../lib/server/server-utils"),h=require("../../../lib/server/server-internal"),b=c();function c(b){return function(c,i){return a.__awaiter(this,void 0,void 0,function(){var j,k,l,m,n,o,p,q,r;return a.__generator(this,function(a){switch(a.label){case 0:return b||(b=(0,h.getInternalDevServer)()),[4,b.init()];case 1:return a.sent(),j=c.query.siteName,k=b.origins,l=c.url,m=c.headers,n=e.DEFAULT_SITE.name,(0,f.exists)(j)&&(n=Array.isArray(j)?j.join(""):j),[4,(0,g.validateUserRequest)({url:l,headers:m,allowedOrigins:k,siteName:n,allowNoSession:!0})];case 2:return o=a.sent(),[4,b.getSite(o)];case 3:return q=(p=a.sent()).site,Object.keys(r=p.responseHeaders).forEach(function(b){var a;return i.setHeader(b,null!==(a=r[b])&& void 0!==a?a:"")}),d.default.isDev()?i.status(200).send(JSON.stringify(q,null,2)):i.status(200).send(q),[2]}})})}}exports.default=b,exports.createSiteEndpoint=c |
@@ -1,17 +0,1 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createMetricsEndpoint = exports.createSiteEndpoint = exports.env = void 0; | ||
var tslib_1 = require("tslib"); | ||
var environment_1 = tslib_1.__importDefault(require("./lib/server/environment")); | ||
exports.env = environment_1.default; | ||
tslib_1.__exportStar(require("./lib/server/environment"), exports); | ||
tslib_1.__exportStar(require("./lib/server/server-core"), exports); | ||
tslib_1.__exportStar(require("./lib/server/server-rendering"), exports); | ||
tslib_1.__exportStar(require("./lib/server/server-types"), exports); | ||
tslib_1.__exportStar(require("./lib/server/server-utils"), exports); | ||
tslib_1.__exportStar(require("./lib/server/backends/json-sites"), exports); | ||
tslib_1.__exportStar(require("./lib/server/backends/redis"), exports); | ||
var _siteName_1 = require("./pages/api/sites/[siteName]"); | ||
Object.defineProperty(exports, "createSiteEndpoint", { enumerable: true, get: function () { return _siteName_1.createSiteEndpoint; } }); | ||
var metrics_1 = require("./pages/api/metrics"); | ||
Object.defineProperty(exports, "createMetricsEndpoint", { enumerable: true, get: function () { return metrics_1.createMetricsEndpoint; } }); | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.createMetricsEndpoint=exports.createSiteEndpoint=exports.env=void 0;var a=require("tslib"),b=a.__importDefault(require("./lib/server/environment"));exports.env=b.default,a.__exportStar(require("./lib/server/environment"),exports),a.__exportStar(require("./lib/server/server-core"),exports),a.__exportStar(require("./lib/server/server-rendering"),exports),a.__exportStar(require("./lib/server/server-types"),exports),a.__exportStar(require("./lib/server/server-utils"),exports),a.__exportStar(require("./lib/server/backends/json-sites"),exports),a.__exportStar(require("./lib/server/backends/redis"),exports);var c=require("./pages/api/sites/[siteName]");Object.defineProperty(exports,"createSiteEndpoint",{enumerable:!0,get:function(){return c.createSiteEndpoint}});var d=require("./pages/api/metrics");Object.defineProperty(exports,"createMetricsEndpoint",{enumerable:!0,get:function(){return d.createMetricsEndpoint}}) |
{ | ||
"name": "@instantdomain/bandit", | ||
"version": "1.0.9", | ||
"version": "1.0.10", | ||
"license": "MIT", | ||
@@ -5,0 +5,0 @@ "repository": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 3 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
89712
766
27
3