UNPKG

statsig-js

Version:

Statsig JavaScript client SDK for single user environments.

541 lines (540 loc) 25 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 }); var uuid_1 = require("uuid"); var LogEvent_1 = __importDefault(require("./LogEvent")); var StatsigNetwork_1 = require("./StatsigNetwork"); var Constants_1 = require("./utils/Constants"); var Diagnostics_1 = __importDefault(require("./utils/Diagnostics")); var StatsigAsyncStorage_1 = __importDefault(require("./utils/StatsigAsyncStorage")); var StatsigLocalStorage_1 = __importDefault(require("./utils/StatsigLocalStorage")); var INTERNAL_EVENT_PREFIX = 'statsig::'; var CONFIG_EXPOSURE_EVENT = INTERNAL_EVENT_PREFIX + 'config_exposure'; var LAYER_EXPOSURE_EVENT = INTERNAL_EVENT_PREFIX + 'layer_exposure'; var GATE_EXPOSURE_EVENT = INTERNAL_EVENT_PREFIX + 'gate_exposure'; var LOG_FAILURE_EVENT = INTERNAL_EVENT_PREFIX + 'log_event_failed'; var APP_ERROR_EVENT = INTERNAL_EVENT_PREFIX + 'app_error'; var APP_METRICS_PAGE_LOAD_EVENT = INTERNAL_EVENT_PREFIX + 'app_metrics::page_load_time'; var APP_METRICS_DOM_INTERACTIVE_EVENT = INTERNAL_EVENT_PREFIX + 'app_metrics::dom_interactive_time'; var APP_METRICS_SCROLL_DEPTH_EVENT = INTERNAL_EVENT_PREFIX + 'app_metrics::scroll_depth'; var APP_METRICS_SESSION_LENGTH_EVENT = INTERNAL_EVENT_PREFIX + 'app_metrics::time_on_page_ms'; var DIAGNOSTICS_EVENT = INTERNAL_EVENT_PREFIX + 'diagnostics'; var DEFAULT_VALUE_WARNING = INTERNAL_EVENT_PREFIX + 'default_value_type_mismatch'; var MS_RETRY_LOGS_CUTOFF = 5 * 24 * 60 * 60 * 1000; var MAX_BATCHES_TO_RETRY = 100; var MAX_FAILED_EVENTS = 1000; var MAX_LOCAL_STORAGE_SIZE = 1024 * MAX_FAILED_EVENTS; var MAX_ERRORS_TO_LOG = 10; var StatsigLogger = /** @class */ (function () { function StatsigLogger(sdkInternal) { this.failedLogEventCount = 0; this.sdkInternal = sdkInternal; this.queue = []; this.flushInterval = null; this.loggedErrors = new Set(); this.failedLogEvents = []; this.exposureDedupeKeys = {}; this.failedLogEventCount = 0; this.init(); } StatsigLogger.prototype.init = function () { var _this = this; if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') { window.addEventListener('blur', function () { return _this.flush(true); }); window.addEventListener('beforeunload', function () { return _this.flush(true); }); window.addEventListener('load', function () { setTimeout(function () { return _this.flush(); }, 100); setTimeout(function () { return _this.flush(); }, 1000); }); } if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') { document.addEventListener('visibilitychange', function () { _this.flush(document.visibilityState !== 'visible'); }); } if (!this.sdkInternal.getOptions().getIgnoreWindowUndefined() && (typeof window === 'undefined' || window == null)) { // dont set the flush interval outside of client browser environments return; } if (this.sdkInternal.getOptions().getLocalModeEnabled()) { // unnecessary interval in local mode since logs dont flush anyway return; } this.flushInterval = setInterval(function () { _this.flush(); }, this.sdkInternal.getOptions().getLoggingIntervalMillis()); // Quick flush setTimeout(function () { return _this.flush(); }, 100); setTimeout(function () { return _this.flush(); }, 1000); }; StatsigLogger.prototype.log = function (event) { if (this.sdkInternal.getOptions().isAllLoggingDisabled()) { return; } try { if (!this.sdkInternal.getOptions().getDisableCurrentPageLogging() && typeof window !== 'undefined' && window != null && typeof window.location === 'object' && typeof window.location.href === 'string') { // https://stackoverflow.com/questions/6257463/how-to-get-the-url-without-any-parameters-in-javascript var parts = window.location.href.split(/[?#]/); if ((parts === null || parts === void 0 ? void 0 : parts.length) > 0) { event.addStatsigMetadata('currentPage', parts[0]); } } } catch (_a) { // noop } this.queue.push(event.toJsonObject()); if (this.queue.length >= this.sdkInternal.getOptions().getLoggingBufferMaxSize()) { this.flush(); } }; StatsigLogger.prototype.resetDedupeKeys = function () { this.exposureDedupeKeys = {}; }; StatsigLogger.prototype.shouldLogExposure = function (key) { var lastTime = this.exposureDedupeKeys[key]; var now = Date.now(); if (lastTime == null) { this.exposureDedupeKeys[key] = now; return true; } if (lastTime >= now - 600 * 1000) { return false; } this.exposureDedupeKeys[key] = now; return true; }; StatsigLogger.prototype.logGateExposure = function (user, gateName, gateValue, ruleID, secondaryExposures, details, isManualExposure) { var dedupeKey = gateName + String(gateValue) + ruleID + details.reason; if (!this.shouldLogExposure(dedupeKey)) { return; } var metadata = { gate: gateName, gateValue: String(gateValue), ruleID: ruleID, reason: details.reason, time: details.time, }; if (isManualExposure) { metadata['isManualExposure'] = 'true'; } var gateExposure = new LogEvent_1.default(GATE_EXPOSURE_EVENT); gateExposure.setUser(user); gateExposure.setMetadata(metadata); gateExposure.setSecondaryExposures(secondaryExposures); this.log(gateExposure); }; StatsigLogger.prototype.logConfigExposure = function (user, configName, ruleID, secondaryExposures, details, isManualExposure) { var dedupeKey = configName + ruleID + details.reason; if (!this.shouldLogExposure(dedupeKey)) { return; } var metadata = { config: configName, ruleID: ruleID, reason: details.reason, time: details.time, }; if (isManualExposure) { metadata['isManualExposure'] = 'true'; } var configExposure = new LogEvent_1.default(CONFIG_EXPOSURE_EVENT); configExposure.setUser(user); configExposure.setMetadata(metadata); configExposure.setSecondaryExposures(secondaryExposures); this.log(configExposure); }; StatsigLogger.prototype.logLayerExposure = function (user, configName, ruleID, secondaryExposures, allocatedExperiment, parameterName, isExplicitParameter, details, isManualExposure) { var dedupeKey = [ configName, ruleID, allocatedExperiment, parameterName, String(isExplicitParameter), details.reason, ].join('|'); if (!this.shouldLogExposure(dedupeKey)) { return; } var metadata = { config: configName, ruleID: ruleID, allocatedExperiment: allocatedExperiment, parameterName: parameterName, isExplicitParameter: String(isExplicitParameter), reason: details.reason, time: details.time, }; if (isManualExposure) { metadata['isManualExposure'] = 'true'; } var configExposure = new LogEvent_1.default(LAYER_EXPOSURE_EVENT); configExposure.setUser(user); configExposure.setMetadata(metadata); configExposure.setSecondaryExposures(secondaryExposures); this.log(configExposure); }; StatsigLogger.prototype.logConfigDefaultValueFallback = function (user, message, metadata) { this.logGenericEvent(DEFAULT_VALUE_WARNING, user, message, metadata); this.loggedErrors.add(message); this.sdkInternal.getConsoleLogger().error(message); }; StatsigLogger.prototype.logAppError = function (user, message, metadata) { var trimmedMessage = message.substring(0, 128); if (this.loggedErrors.has(trimmedMessage) || this.loggedErrors.size > MAX_ERRORS_TO_LOG) { return; } this.logGenericEvent(APP_ERROR_EVENT, user, trimmedMessage, metadata); this.loggedErrors.add(trimmedMessage); }; StatsigLogger.prototype.logDiagnostics = function (user, context) { var markers = Diagnostics_1.default.getMarkers(context); if (markers.length <= 0) { return; } Diagnostics_1.default.clearContext(context); var event = this.makeDiagnosticsEvent(user, { markers: markers, context: context, statsigOptions: this.sdkInternal.getOptions().getLoggingCopy() }); this.log(event); }; StatsigLogger.prototype.logAppMetrics = function (user) { var _this = this; var _a; if (typeof ((_a = window === null || window === void 0 ? void 0 : window.performance) === null || _a === void 0 ? void 0 : _a.getEntriesByType) !== 'function') { return; } var entries = window.performance.getEntriesByType('navigation'); if (!entries || entries.length < 1) { return; } var navEntry = entries[0]; var metadata = { url: navEntry.name, }; if (navEntry instanceof PerformanceNavigationTiming) { this.logGenericEvent(APP_METRICS_PAGE_LOAD_EVENT, user, navEntry.duration, metadata); this.logGenericEvent(APP_METRICS_DOM_INTERACTIVE_EVENT, user, navEntry.domInteractive - navEntry.startTime, metadata); } if (typeof (window === null || window === void 0 ? void 0 : window.addEventListener) === 'function' && (document === null || document === void 0 ? void 0 : document.body)) { var deepestScroll_1 = 0; window.addEventListener('scroll', function () { var scrollHeight = document.body.scrollHeight || 1; var scrollDepth = Math.min(100, Math.round((window.scrollY + window.innerHeight) / scrollHeight * 100)); if (scrollDepth > deepestScroll_1) { deepestScroll_1 = scrollDepth; } }); window.addEventListener('beforeunload', function () { _this.logGenericEvent(APP_METRICS_SCROLL_DEPTH_EVENT, user, deepestScroll_1, metadata); _this.logGenericEvent(APP_METRICS_SESSION_LENGTH_EVENT, user, window.performance.now(), metadata); }); } }; StatsigLogger.prototype.logGenericEvent = function (eventName, user, value, metadata) { var evt = new LogEvent_1.default(eventName); evt.setUser(user); evt.setValue(value); evt.setMetadata(metadata); this.log(evt); return evt; }; StatsigLogger.prototype.shutdown = function () { if (this.flushInterval) { clearInterval(this.flushInterval); this.flushInterval = null; } this.flush(true); }; StatsigLogger.prototype.flush = function (isClosing) { var _this = this; if (isClosing === void 0) { isClosing = false; } this.addErrorBoundaryDiagnostics(); if (this.queue.length === 0) { return; } var statsigMetadata = this.sdkInternal.getStatsigMetadata(); if (statsigMetadata.sessionID == null) { statsigMetadata.sessionID = (0, uuid_1.v4)(); } var oldQueue = this.queue; this.queue = []; if (isClosing && !this.sdkInternal.getNetwork().supportsKeepalive() && typeof navigator !== 'undefined' && (navigator === null || navigator === void 0 ? void 0 : navigator.sendBeacon) != null) { var beacon = this.sdkInternal.getNetwork().sendLogBeacon({ events: oldQueue, statsigMetadata: this.sdkInternal.getStatsigMetadata(), }); if (!beacon) { this.queue = oldQueue.concat(this.queue); if (this.queue.length > 0) { this.addFailedRequest({ events: this.queue, statsigMetadata: this.sdkInternal.getStatsigMetadata(), time: Date.now(), }); this.queue = []; } this.saveFailedRequests(); } return; } this.sdkInternal .getNetwork() .postToEndpoint(StatsigNetwork_1.StatsigEndpoint.Rgstr, { events: oldQueue, statsigMetadata: this.sdkInternal.getStatsigMetadata(), }, { retryOptions: { retryLimit: 3, backoff: 1000, }, useKeepalive: isClosing, }) .then(function (response) { if (!response.ok) { throw response; } }) .catch(function (error) { if (typeof error.text === 'function') { error.text().then(function (errorText) { _this.sdkInternal .getErrorBoundary() .logError(LOG_FAILURE_EVENT, error, { getExtraData: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, { eventCount: oldQueue.length, error: errorText, }]; }); }); } }); }); } else { _this.sdkInternal .getErrorBoundary() .logError(LOG_FAILURE_EVENT, error, { getExtraData: function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, { eventCount: oldQueue.length, error: error.message, }]; }); }); } }); } _this.newFailedRequest(LOG_FAILURE_EVENT, oldQueue); }) .finally(function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { if (isClosing) { if (this.queue.length > 0) { this.addFailedRequest({ events: this.queue, statsigMetadata: this.sdkInternal.getStatsigMetadata(), time: Date.now(), }); // on app background/window blur, save unsent events as a request and clean up the queue (in case app foregrounds) this.queue = []; } this.saveFailedRequests(); } return [2 /*return*/]; }); }); }); }; StatsigLogger.prototype.saveFailedRequests = function () { var _this = this; if (this.failedLogEvents.length > 0) { var requestsCopy = JSON.stringify(this.failedLogEvents); if (requestsCopy.length > MAX_LOCAL_STORAGE_SIZE) { this.clearLocalStorageRequests(); return; } if (StatsigAsyncStorage_1.default.asyncStorage) { StatsigAsyncStorage_1.default.setItemAsync(Constants_1.STATSIG_LOCAL_STORAGE_LOGGING_REQUEST_KEY, requestsCopy).catch(function (reason) { return _this.sdkInternal .getErrorBoundary() .logError('saveFailedRequests', reason); }); return; } StatsigLocalStorage_1.default.setItem(Constants_1.STATSIG_LOCAL_STORAGE_LOGGING_REQUEST_KEY, requestsCopy); } }; StatsigLogger.prototype.sendSavedRequests = function () { return __awaiter(this, void 0, void 0, function () { var failedRequests, fireAndForget, requestBodies, _loop_1, this_1, _i, requestBodies_1, requestBody; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: fireAndForget = false; if (!StatsigAsyncStorage_1.default.asyncStorage) return [3 /*break*/, 2]; return [4 /*yield*/, StatsigAsyncStorage_1.default.getItemAsync(Constants_1.STATSIG_LOCAL_STORAGE_LOGGING_REQUEST_KEY)]; case 1: failedRequests = _a.sent(); return [3 /*break*/, 3]; case 2: failedRequests = StatsigLocalStorage_1.default.getItem(Constants_1.STATSIG_LOCAL_STORAGE_LOGGING_REQUEST_KEY); _a.label = 3; case 3: if (failedRequests == null) { this.clearLocalStorageRequests(); return [2 /*return*/]; } if (failedRequests.length > MAX_LOCAL_STORAGE_SIZE) { fireAndForget = true; } requestBodies = []; try { requestBodies = JSON.parse(failedRequests); _loop_1 = function (requestBody) { if (requestBody != null && requestBody.events && Array.isArray(requestBody.events)) { this_1.sdkInternal .getNetwork() .postToEndpoint(StatsigNetwork_1.StatsigEndpoint.Rgstr, requestBody) .then(function (response) { if (!response.ok) { throw Error(response.status + ''); } }) .catch(function () { if (fireAndForget) { return; } _this.addFailedRequest(requestBody); }); } }; this_1 = this; for (_i = 0, requestBodies_1 = requestBodies; _i < requestBodies_1.length; _i++) { requestBody = requestBodies_1[_i]; _loop_1(requestBody); } } catch (e) { this.sdkInternal.getErrorBoundary().logError('sendSavedRequests', e); } finally { this.clearLocalStorageRequests(); } return [2 /*return*/]; } }); }); }; StatsigLogger.prototype.addFailedRequest = function (requestBody) { if (requestBody.time < Date.now() - MS_RETRY_LOGS_CUTOFF) { return; } if (this.failedLogEvents.length > MAX_BATCHES_TO_RETRY) { return; } var additionalEvents = requestBody.events.length; if (this.failedLogEventCount + additionalEvents > MAX_FAILED_EVENTS) { return; } this.failedLogEvents.push(requestBody); this.failedLogEventCount += additionalEvents; }; StatsigLogger.prototype.clearLocalStorageRequests = function () { var _this = this; if (StatsigAsyncStorage_1.default.asyncStorage) { StatsigAsyncStorage_1.default.removeItemAsync(Constants_1.STATSIG_LOCAL_STORAGE_LOGGING_REQUEST_KEY).catch(function (reason) { return _this.sdkInternal .getErrorBoundary() .logError('clearLocalStorageRequests', reason); }); } else { StatsigLocalStorage_1.default.removeItem(Constants_1.STATSIG_LOCAL_STORAGE_LOGGING_REQUEST_KEY); } }; StatsigLogger.prototype.newFailedRequest = function (name, queue) { if (this.loggedErrors.has(name)) { return; } this.loggedErrors.add(name); this.failedLogEvents.push({ events: queue, statsigMetadata: this.sdkInternal.getStatsigMetadata(), time: Date.now(), }); this.saveFailedRequests(); }; StatsigLogger.prototype.makeDiagnosticsEvent = function (user, data) { var latencyEvent = new LogEvent_1.default(DIAGNOSTICS_EVENT); latencyEvent.setUser(user); latencyEvent.setMetadata(data); return latencyEvent; }; StatsigLogger.prototype.addErrorBoundaryDiagnostics = function () { if (Diagnostics_1.default.getMarkerCount('api_call') === 0) { return; } var diagEvent = this.makeDiagnosticsEvent(this.sdkInternal.getCurrentUser(), { context: 'api_call', markers: Diagnostics_1.default.getMarkers('api_call'), }); this.queue.push(diagEvent); Diagnostics_1.default.clearContext('api_call'); }; return StatsigLogger; }()); exports.default = StatsigLogger;