@amplitude/analytics-core
Version:
264 lines • 12.6 kB
JavaScript
"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