UNPKG

statsig-js

Version:

Statsig JavaScript client SDK for single user environments.

374 lines (373 loc) 19.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StatsigEndpoint = void 0; var Errors_1 = require("./Errors"); var StatsigRuntime_1 = __importDefault(require("./StatsigRuntime")); var Diagnostics_1 = __importDefault(require("./utils/Diagnostics")); var StatsigEndpoint; (function (StatsigEndpoint) { StatsigEndpoint["Initialize"] = "initialize"; StatsigEndpoint["Rgstr"] = "rgstr"; StatsigEndpoint["LogEventBeacon"] = "log_event_beacon"; })(StatsigEndpoint = exports.StatsigEndpoint || (exports.StatsigEndpoint = {})); var NO_CONTENT = 204; var StatsigNetwork = /** @class */ (function () { function StatsigNetwork(sdkInternal) { this.retryCodes = { 408: true, 500: true, 502: true, 503: true, 504: true, 522: true, 524: true, 599: true, }; this.canUseKeepalive = false; this.sdkInternal = sdkInternal; this.leakyBucket = {}; this.init(); } StatsigNetwork.prototype.init = function () { if (!this.sdkInternal.getOptions().getDisableNetworkKeepalive()) { try { this.canUseKeepalive = 'keepalive' in new Request(''); } catch (_e) { this.canUseKeepalive = false; } } }; StatsigNetwork.prototype.fetchValues = function (args) { var user = args.user, sinceTime = args.sinceTime, timeout = args.timeout, useDeltas = args.useDeltas, prefetchUsers = args.prefetchUsers, previousDerivedFields = args.previousDerivedFields, hadBadDeltaChecksum = args.hadBadDeltaChecksum, badChecksum = args.badChecksum, badMergedConfigs = args.badMergedConfigs, badFullResponse = args.badFullResponse; var input = { user: user, prefetchUsers: prefetchUsers, statsigMetadata: this.sdkInternal.getStatsigMetadata(), sinceTime: sinceTime !== null && sinceTime !== void 0 ? sinceTime : undefined, deltasResponseRequested: useDeltas, hash: this.sdkInternal.getOptions().getDisableHashing() ? 'none' : 'djb2', previousDerivedFields: previousDerivedFields, hadBadDeltaChecksum: hadBadDeltaChecksum, badChecksum: badChecksum, badMergedConfigs: badMergedConfigs, badFullResponse: badFullResponse, }; return this.postWithTimeout(StatsigEndpoint.Initialize, input, { timeout: timeout, retries: 3, diagnostics: Diagnostics_1.default.mark.initialize.networkRequest, }); }; StatsigNetwork.prototype.postWithTimeout = function (endpointName, body, options) { var _this = this; var _a = options !== null && options !== void 0 ? options : {}, _b = _a.timeout, timeout = _b === void 0 ? 0 : _b, _c = _a.retries, retries = _c === void 0 ? 0 : _c, _d = _a.backoff, backoff = _d === void 0 ? 1000 : _d, _f = _a.diagnostics, diagnostics = _f === void 0 ? null : _f; var hasTimedOut = false; var timer = null; var cachedReturnValue = null; var eventuals = []; var eventually = function (boundScope) { return function (fn) { if (hasTimedOut && cachedReturnValue) { fn(cachedReturnValue); } else { eventuals.push(fn); } return boundScope; }; }; if (timeout != 0) { timer = new Promise(function (_, reject) { setTimeout(function () { hasTimedOut = true; reject(new Errors_1.StatsigInitializationTimeoutError(timeout)); }, timeout); }); } var res; var fetchPromise = this.postToEndpoint(endpointName, body, { retryOptions: { retryLimit: retries, backoff: backoff, }, diagnostics: diagnostics, }) .then(function (localRes) { res = localRes; if (!res.ok) { return Promise.reject(new Error("Request to " + endpointName + " failed with status " + res.status)); } if (typeof res.data !== 'object') { var error = new Error("Request to " + endpointName + " received invalid response type. Expected 'object' but got '" + typeof res.data + "'"); _this.sdkInternal .getErrorBoundary() .logError('postWithTimeoutInvalidRes', error, { getExtraData: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, this.getErrorData(endpointName, body, retries, backoff, res)]; }); }); }, }); return Promise.reject(error); } var json = res.data; return _this.sdkInternal.getErrorBoundary().capture('postWithTimeout', function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { cachedReturnValue = json; if (hasTimedOut) { eventuals.forEach(function (fn) { return fn(json); }); eventuals = []; } return [2 /*return*/, Promise.resolve(json)]; }); }); }, function () { return Promise.resolve({}); }, { getExtraData: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, this.getErrorData(endpointName, body, retries, backoff, res)]; }); }); }, }); }) .catch(function (e) { return Promise.reject(e); }); var racingPromise = (timer ? Promise.race([fetchPromise, timer]) : fetchPromise); racingPromise.eventually = eventually(racingPromise); return racingPromise; }; StatsigNetwork.prototype.sendLogBeacon = function (payload) { var statsigOpts = this.sdkInternal.getOptions(); if (statsigOpts.getLocalModeEnabled()) { return true; } var url = new URL(statsigOpts.getEventLoggingApi() + StatsigEndpoint.LogEventBeacon); url.searchParams.append('k', this.sdkInternal.getSDKKey()); payload.clientTime = Date.now() + ''; var stringPayload = null; try { stringPayload = JSON.stringify(payload); } catch (_e) { return false; } return navigator.sendBeacon(url.toString(), stringPayload); }; StatsigNetwork.prototype.postToEndpoint = function (endpointName, body, options) { var _a; return __awaiter(this, void 0, void 0, function () { var _b, _c, useKeepalive, _d, diagnostics, _f, _g, retryLimit, _h, attempt, _j, backoff, statsigOpts, api, url, counter, shouldEncode, postBody, encoded, params, res, isRetryCode; var _this = this; return __generator(this, function (_k) { _b = options !== null && options !== void 0 ? options : {}, _c = _b.useKeepalive, useKeepalive = _c === void 0 ? false : _c, _d = _b.diagnostics, diagnostics = _d === void 0 ? null : _d; _f = (_a = options === null || options === void 0 ? void 0 : options.retryOptions) !== null && _a !== void 0 ? _a : {}, _g = _f.retryLimit, retryLimit = _g === void 0 ? 0 : _g, _h = _f.attempt, attempt = _h === void 0 ? 1 : _h, _j = _f.backoff, backoff = _j === void 0 ? 1000 : _j; statsigOpts = this.sdkInternal.getOptions(); if (statsigOpts.getLocalModeEnabled()) { return [2 /*return*/, Promise.reject('no network requests in localMode')]; } if (typeof fetch !== 'function') { // fetch is not defined in this environment, short circuit return [2 /*return*/, Promise.reject('fetch is not defined')]; } if (typeof window === 'undefined' && !statsigOpts.getIgnoreWindowUndefined()) { // by default, dont issue requests from the server return [2 /*return*/, Promise.reject('window is not defined')]; } api = [StatsigEndpoint.Initialize].includes(endpointName) ? statsigOpts.getApi() : statsigOpts.getEventLoggingApi(); url = api + endpointName; counter = this.leakyBucket[url]; if (counter != null && counter >= 30) { return [2 /*return*/, Promise.reject(new Error('Request failed because you are making the same request too frequently.'))]; } if (counter == null) { this.leakyBucket[url] = 1; } else { this.leakyBucket[url] = counter + 1; } shouldEncode = endpointName === StatsigEndpoint.Initialize && StatsigRuntime_1.default.encodeInitializeCall && typeof window !== 'undefined' && typeof (window === null || window === void 0 ? void 0 : window.btoa) === 'function'; postBody = JSON.stringify(body); if (shouldEncode) { try { encoded = window.btoa(postBody).split('').reverse().join(''); postBody = encoded; } catch (_e) { shouldEncode = false; } } params = { method: 'POST', body: postBody, headers: { 'Content-type': 'application/json; charset=UTF-8', 'STATSIG-API-KEY': this.sdkInternal.getSDKKey(), 'STATSIG-CLIENT-TIME': Date.now() + '', 'STATSIG-SDK-TYPE': this.sdkInternal.getSDKType(), 'STATSIG-SDK-VERSION': this.sdkInternal.getSDKVersion(), 'STATSIG-ENCODED': shouldEncode ? '1' : '0', }, }; if (this.canUseKeepalive && useKeepalive) { params.keepalive = true; } diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.start({ attempt: attempt }); isRetryCode = true; return [2 /*return*/, fetch(url, params) .then(function (localRes) { return __awaiter(_this, void 0, void 0, function () { var networkResponse, text, errorText; return __generator(this, function (_a) { switch (_a.label) { case 0: res = localRes; if (!res.ok) return [3 /*break*/, 4]; networkResponse = res; if (!(res.status === NO_CONTENT)) return [3 /*break*/, 1]; networkResponse.data = { has_updates: false, is_no_content: true }; return [3 /*break*/, 3]; case 1: return [4 /*yield*/, res.text()]; case 2: text = _a.sent(); networkResponse.data = JSON.parse(text); _a.label = 3; case 3: diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.end(this.getDiagnosticsData(res, attempt)); return [2 /*return*/, Promise.resolve(networkResponse)]; case 4: if (!this.retryCodes[res.status]) { isRetryCode = false; } return [4 /*yield*/, res.text()]; case 5: errorText = _a.sent(); return [2 /*return*/, Promise.reject(new Error(res.status + ": " + errorText))]; } }); }); }) .catch(function (e) { diagnostics === null || diagnostics === void 0 ? void 0 : diagnostics.end(_this.getDiagnosticsData(res, attempt, e)); if (attempt < retryLimit && isRetryCode) { return new Promise(function (resolve, reject) { setTimeout(function () { _this.leakyBucket[url] = Math.max(_this.leakyBucket[url] - 1, 0); _this.postToEndpoint(endpointName, body, { retryOptions: { retryLimit: retryLimit, attempt: attempt + 1, backoff: backoff * 2, }, useKeepalive: useKeepalive, diagnostics: diagnostics, }) .then(resolve) .catch(reject); }, backoff); }); } return Promise.reject(e); }) .finally(function () { _this.leakyBucket[url] = Math.max(_this.leakyBucket[url] - 1, 0); })]; }); }); }; StatsigNetwork.prototype.supportsKeepalive = function () { return this.canUseKeepalive; }; StatsigNetwork.prototype.getDiagnosticsData = function (res, attempt, e) { var _a, _b; return { success: (res === null || res === void 0 ? void 0 : res.ok) === true, statusCode: res === null || res === void 0 ? void 0 : res.status, sdkRegion: (_a = res === null || res === void 0 ? void 0 : res.headers) === null || _a === void 0 ? void 0 : _a.get('x-statsig-region'), isDelta: ((_b = res === null || res === void 0 ? void 0 : res.data) === null || _b === void 0 ? void 0 : _b.is_delta) === true, attempt: attempt, error: Diagnostics_1.default.formatError(e), }; }; StatsigNetwork.prototype.getErrorData = function (endpointName, body, retries, backoff, res) { var _a; return __awaiter(this, void 0, void 0, function () { var headers_1; return __generator(this, function (_b) { try { headers_1 = {}; ((_a = res.headers) !== null && _a !== void 0 ? _a : []).forEach(function (value, key) { headers_1[key] = value; }); return [2 /*return*/, { responseInfo: { headers: headers_1, status: res.status, statusText: res.statusText, type: res.type, url: res.url, redirected: res.redirected, bodySnippet: res.data ? JSON.stringify(res.data).slice(0, 500) : null, }, requestInfo: { endpointName: endpointName, bodySnippet: JSON.stringify(body).slice(0, 500), retries: retries, backoff: backoff, }, }]; } catch (_e) { return [2 /*return*/, { statusText: 'statsig::failed to extract extra data', }]; } return [2 /*return*/]; }); }); }; return StatsigNetwork; }()); exports.default = StatsigNetwork;