statsig-js
Version:
Statsig JavaScript client SDK for single user environments.
541 lines (540 loc) • 25 kB
JavaScript
"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;