UNPKG

@amplitude/analytics-core

Version:
264 lines 12.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteConfigClient = exports.FETCHED_KEYS = exports.DEFAULT_MAX_RETRIES = exports.EU_SERVER_URL = exports.US_SERVER_URL = void 0; var tslib_1 = require("tslib"); var remote_config_localstorage_1 = require("./remote-config-localstorage"); var uuid_1 = require("../utils/uuid"); exports.US_SERVER_URL = 'https://sr-client-cfg.amplitude.com/config'; exports.EU_SERVER_URL = 'https://sr-client-cfg.eu.amplitude.com/config'; exports.DEFAULT_MAX_RETRIES = 3; /** * The default timeout for fetch in milliseconds. * Linear backoff policy: timeout / retry times is the interval between fetch retry. */ var DEFAULT_TIMEOUT = 1000; // TODO(xinyi) // const DEFAULT_MIN_TIME_BETWEEN_FETCHES = 5 * 60 * 1000; // 5 minutes exports.FETCHED_KEYS = [ 'analyticsSDK.browserSDK', 'sessionReplay.sr_interaction_config', 'sessionReplay.sr_logging_config', 'sessionReplay.sr_privacy_config', 'sessionReplay.sr_sampling_config', 'sessionReplay.sr_targeting_config', ]; var RemoteConfigClient = /** @class */ (function () { function RemoteConfigClient(apiKey, logger, serverZone) { if (serverZone === void 0) { serverZone = 'US'; } // Registered callbackInfos by subscribe(). this.callbackInfos = []; this.apiKey = apiKey; this.serverUrl = serverZone === 'US' ? exports.US_SERVER_URL : exports.EU_SERVER_URL; this.logger = logger; this.storage = new remote_config_localstorage_1.RemoteConfigLocalStorage(apiKey, logger); } RemoteConfigClient.prototype.subscribe = function (key, deliveryMode, callback) { var id = (0, uuid_1.UUID)(); var callbackInfo = { id: id, key: key, deliveryMode: deliveryMode, callback: callback, }; this.callbackInfos.push(callbackInfo); if (deliveryMode === 'all') { void this.subscribeAll(callbackInfo); } else { void this.subscribeWaitForRemote(callbackInfo, deliveryMode.timeout); } return id; }; RemoteConfigClient.prototype.unsubscribe = function (id) { var index = this.callbackInfos.findIndex(function (callbackInfo) { return callbackInfo.id === id; }); if (index === -1) { this.logger.debug("Remote config client unsubscribe failed because callback with id ".concat(id, " doesn't exist.")); return false; } this.callbackInfos.splice(index, 1); this.logger.debug("Remote config client unsubscribe succeeded removing callback with id ".concat(id, ".")); return true; }; RemoteConfigClient.prototype.updateConfigs = function () { return tslib_1.__awaiter(this, void 0, void 0, function () { var result; var _this = this; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.fetch()]; case 1: result = _a.sent(); void this.storage.setConfig(result); this.callbackInfos.forEach(function (callbackInfo) { _this.sendCallback(callbackInfo, result, 'remote'); }); return [2 /*return*/]; } }); }); }; /** * Send remote first. If it's already complete, we can skip the cached response. * - if remote is fetched first, no cache fetch. * - if cache is fetched first, still fetching remote. */ RemoteConfigClient.prototype.subscribeAll = function (callbackInfo) { return tslib_1.__awaiter(this, void 0, void 0, function () { var remotePromise, cachePromise, result; var _this = this; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: remotePromise = this.fetch().then(function (result) { _this.logger.debug('Remote config client subscription all mode fetched from remote.'); _this.sendCallback(callbackInfo, result, 'remote'); void _this.storage.setConfig(result); }); cachePromise = this.storage.fetchConfig().then(function (result) { return result; }); return [4 /*yield*/, Promise.race([remotePromise, cachePromise])]; case 1: result = _a.sent(); // If cache is fetched first, wait for remote. if (result !== undefined) { this.logger.debug('Remote config client subscription all mode fetched from cache.'); this.sendCallback(callbackInfo, result, 'cache'); } return [4 /*yield*/, remotePromise]; case 2: _a.sent(); return [2 /*return*/]; } }); }); }; /** * Waits for a remote response until the given timeout, then return a cached copy, if available. */ RemoteConfigClient.prototype.subscribeWaitForRemote = function (callbackInfo, timeout) { return tslib_1.__awaiter(this, void 0, void 0, function () { var timeoutPromise, result, error_1, result; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: timeoutPromise = new Promise(function (_, reject) { setTimeout(function () { reject('Timeout exceeded'); }, timeout); }); _a.label = 1; case 1: _a.trys.push([1, 3, , 5]); return [4 /*yield*/, Promise.race([this.fetch(), timeoutPromise])]; case 2: result = (_a.sent()); this.logger.debug('Remote config client subscription wait for remote mode returns from remote.'); this.sendCallback(callbackInfo, result, 'remote'); void this.storage.setConfig(result); return [3 /*break*/, 5]; case 3: error_1 = _a.sent(); this.logger.debug('Remote config client subscription wait for remote mode exceeded timeout. Try to fetch from cache.'); return [4 /*yield*/, this.storage.fetchConfig()]; case 4: result = _a.sent(); if (result.remoteConfig !== null) { this.logger.debug('Remote config client subscription wait for remote mode returns a cached copy.'); this.sendCallback(callbackInfo, result, 'cache'); } else { this.logger.debug('Remote config client subscription wait for remote mode failed to fetch cache.'); this.sendCallback(callbackInfo, result, 'remote'); } return [3 /*break*/, 5]; case 5: return [2 /*return*/]; } }); }); }; /** * Call the callback with filtered remote config based on key. * @param remoteConfigInfo - the whole remote config object without filtering by key. */ RemoteConfigClient.prototype.sendCallback = function (callbackInfo, remoteConfigInfo, source) { callbackInfo.lastCallback = new Date(); var filteredConfig; if (callbackInfo.key) { // Filter remote config by key. // For example, if remote config is {a: {b: {c: 1}}}, // if key = 'a', filter result is {b: {c: 1}}; // if key = 'a.b', filter result is {c: 1} filteredConfig = callbackInfo.key.split('.').reduce(function (config, key) { if (config === null) { return config; } return key in config ? config[key] : null; }, remoteConfigInfo.remoteConfig); } else { filteredConfig = remoteConfigInfo.remoteConfig; } callbackInfo.callback(filteredConfig, source, remoteConfigInfo.lastFetch); }; RemoteConfigClient.prototype.fetch = function (retries, timeout) { if (retries === void 0) { retries = exports.DEFAULT_MAX_RETRIES; } if (timeout === void 0) { timeout = DEFAULT_TIMEOUT; } return tslib_1.__awaiter(this, void 0, void 0, function () { var interval, failedRemoteConfigInfo, attempt, res, body, remoteConfig, error_2; var _this = this; return tslib_1.__generator(this, function (_a) { switch (_a.label) { case 0: interval = timeout / retries; failedRemoteConfigInfo = { remoteConfig: null, lastFetch: new Date(), }; attempt = 0; _a.label = 1; case 1: if (!(attempt < retries)) return [3 /*break*/, 12]; _a.label = 2; case 2: _a.trys.push([2, 8, , 9]); return [4 /*yield*/, fetch(this.getUrlParams(), { method: 'GET', headers: { Accept: '*/*', }, })]; case 3: res = _a.sent(); if (!!res.ok) return [3 /*break*/, 5]; return [4 /*yield*/, res.text()]; case 4: body = _a.sent(); this.logger.debug("Remote config client fetch with retry time ".concat(retries, " failed with ").concat(res.status, ": ").concat(body)); return [3 /*break*/, 7]; case 5: return [4 /*yield*/, res.json()]; case 6: remoteConfig = (_a.sent()); return [2 /*return*/, { remoteConfig: remoteConfig, lastFetch: new Date(), }]; case 7: return [3 /*break*/, 9]; case 8: error_2 = _a.sent(); // Handle rejects when the request fails, for example, a network error this.logger.debug("Remote config client fetch with retry time ".concat(retries, " is rejected because: "), error_2); return [3 /*break*/, 9]; case 9: if (!(attempt < retries - 1)) return [3 /*break*/, 11]; return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, _this.getJitterDelay(interval)); })]; case 10: _a.sent(); _a.label = 11; case 11: attempt++; return [3 /*break*/, 1]; case 12: return [2 /*return*/, failedRemoteConfigInfo]; } }); }); }; /** * Return jitter in the bound of [0,baseDelay) and then floor round. */ RemoteConfigClient.prototype.getJitterDelay = function (baseDelay) { return Math.floor(Math.random() * baseDelay); }; RemoteConfigClient.prototype.getUrlParams = function () { var urlParams = new URLSearchParams({ api_key: this.apiKey, }); exports.FETCHED_KEYS.forEach(function (key) { urlParams.append('config_keys', key); }); return "".concat(this.serverUrl, "?").concat(urlParams.toString()); }; return RemoteConfigClient; }()); exports.RemoteConfigClient = RemoteConfigClient; //# sourceMappingURL=remote-config.js.map