@amplitude/analytics-core 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ import { RemoteConfigStorage, RemoteConfigInfo } from './remote-config';
2
+ import { ILogger } from '../logger';
3
+ export declare class RemoteConfigLocalStorage implements RemoteConfigStorage {
4
+ private readonly key;
5
+ private readonly logger;
6
+ constructor(apiKey: string, logger: ILogger);
7
+ fetchConfig(): Promise<RemoteConfigInfo>;
8
+ setConfig(config: RemoteConfigInfo): Promise<boolean>;
9
+ }
10
+ //# sourceMappingURL=remote-config-localstorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-config-localstorage.d.ts","sourceRoot":"","sources":["../../../src/remote-config/remote-config-localstorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,qBAAa,wBAAyB,YAAW,mBAAmB;IAClE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;gBAErB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;IAK3C,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAiCxC,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CAUtD"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RemoteConfigLocalStorage = void 0;
4
+ var RemoteConfigLocalStorage = /** @class */ (function () {
5
+ function RemoteConfigLocalStorage(apiKey, logger) {
6
+ this.key = "AMP_remote_config_".concat(apiKey.substring(0, 10));
7
+ this.logger = logger;
8
+ }
9
+ RemoteConfigLocalStorage.prototype.fetchConfig = function () {
10
+ var result = null;
11
+ var failedRemoteConfigInfo = {
12
+ remoteConfig: null,
13
+ lastFetch: new Date(),
14
+ };
15
+ try {
16
+ result = localStorage.getItem(this.key);
17
+ }
18
+ catch (error) {
19
+ this.logger.debug('Remote config localstorage failed to access: ', error);
20
+ return Promise.resolve(failedRemoteConfigInfo);
21
+ }
22
+ if (result === null) {
23
+ this.logger.debug('Remote config localstorage gets null because the key does not exist');
24
+ return Promise.resolve(failedRemoteConfigInfo);
25
+ }
26
+ try {
27
+ var remoteConfigInfo = JSON.parse(result);
28
+ this.logger.debug('Remote config localstorage parsed successfully.');
29
+ return Promise.resolve({
30
+ remoteConfig: remoteConfigInfo.remoteConfig,
31
+ lastFetch: new Date(remoteConfigInfo.lastFetch),
32
+ });
33
+ }
34
+ catch (error) {
35
+ this.logger.debug('Remote config localstorage failed to parse: ', error);
36
+ localStorage.removeItem(this.key);
37
+ return Promise.resolve(failedRemoteConfigInfo);
38
+ }
39
+ };
40
+ RemoteConfigLocalStorage.prototype.setConfig = function (config) {
41
+ try {
42
+ localStorage.setItem(this.key, JSON.stringify(config));
43
+ this.logger.debug('Remote config localstorage set successfully.');
44
+ return Promise.resolve(true);
45
+ }
46
+ catch (error) {
47
+ this.logger.debug('Remote config localstorage failed to set: ', error);
48
+ }
49
+ return Promise.resolve(false);
50
+ };
51
+ return RemoteConfigLocalStorage;
52
+ }());
53
+ exports.RemoteConfigLocalStorage = RemoteConfigLocalStorage;
54
+ //# sourceMappingURL=remote-config-localstorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-config-localstorage.js","sourceRoot":"","sources":["../../../src/remote-config/remote-config-localstorage.ts"],"names":[],"mappings":";;;AAGA;IAIE,kCAAY,MAAc,EAAE,MAAe;QACzC,IAAI,CAAC,GAAG,GAAG,4BAAqB,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAE,CAAC;QAC1D,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,8CAAW,GAAX;QACE,IAAI,MAAM,GAAkB,IAAI,CAAC;QACjC,IAAM,sBAAsB,GAAG;YAC7B,YAAY,EAAE,IAAI;YAClB,SAAS,EAAE,IAAI,IAAI,EAAE;SACtB,CAAC;QAEF,IAAI;YACF,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SACzC;QAAC,OAAO,KAAK,EAAE;YACd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,+CAA+C,EAAE,KAAK,CAAC,CAAC;YAC1E,OAAO,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;SAChD;QAED,IAAI,MAAM,KAAK,IAAI,EAAE;YACnB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;YACzF,OAAO,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;SAChD;QAED,IAAI;YACF,IAAM,gBAAgB,GAAqB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAqB,CAAC;YAClF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;YACrE,OAAO,OAAO,CAAC,OAAO,CAAC;gBACrB,YAAY,EAAE,gBAAgB,CAAC,YAAY;gBAC3C,SAAS,EAAE,IAAI,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;aAChD,CAAC,CAAC;SACJ;QAAC,OAAO,KAAK,EAAE;YACd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;YACzE,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClC,OAAO,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;SAChD;IACH,CAAC;IAED,4CAAS,GAAT,UAAU,MAAwB;QAChC,IAAI;YACF,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YACvD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;YAClE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;SAC9B;QAAC,OAAO,KAAK,EAAE;YACd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,4CAA4C,EAAE,KAAK,CAAC,CAAC;SACxE;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IACH,+BAAC;AAAD,CAAC,AApDD,IAoDC;AApDY,4DAAwB","sourcesContent":["import { RemoteConfigStorage, RemoteConfigInfo } from './remote-config';\nimport { ILogger } from '../logger';\n\nexport class RemoteConfigLocalStorage implements RemoteConfigStorage {\n private readonly key: string;\n private readonly logger: ILogger;\n\n constructor(apiKey: string, logger: ILogger) {\n this.key = `AMP_remote_config_${apiKey.substring(0, 10)}`;\n this.logger = logger;\n }\n\n fetchConfig(): Promise<RemoteConfigInfo> {\n let result: string | null = null;\n const failedRemoteConfigInfo = {\n remoteConfig: null,\n lastFetch: new Date(),\n };\n\n try {\n result = localStorage.getItem(this.key);\n } catch (error) {\n this.logger.debug('Remote config localstorage failed to access: ', error);\n return Promise.resolve(failedRemoteConfigInfo);\n }\n\n if (result === null) {\n this.logger.debug('Remote config localstorage gets null because the key does not exist');\n return Promise.resolve(failedRemoteConfigInfo);\n }\n\n try {\n const remoteConfigInfo: RemoteConfigInfo = JSON.parse(result) as RemoteConfigInfo;\n this.logger.debug('Remote config localstorage parsed successfully.');\n return Promise.resolve({\n remoteConfig: remoteConfigInfo.remoteConfig,\n lastFetch: new Date(remoteConfigInfo.lastFetch),\n });\n } catch (error) {\n this.logger.debug('Remote config localstorage failed to parse: ', error);\n localStorage.removeItem(this.key);\n return Promise.resolve(failedRemoteConfigInfo);\n }\n }\n\n setConfig(config: RemoteConfigInfo): Promise<boolean> {\n try {\n localStorage.setItem(this.key, JSON.stringify(config));\n this.logger.debug('Remote config localstorage set successfully.');\n return Promise.resolve(true);\n } catch (error) {\n this.logger.debug('Remote config localstorage failed to set: ', error);\n }\n return Promise.resolve(false);\n }\n}\n"]}
@@ -0,0 +1,115 @@
1
+ import { ServerZoneType } from '../types/server-zone';
2
+ import { ILogger } from '../logger';
3
+ /**
4
+ * Modes for receiving remote config updates:
5
+ * - `'all'` – Receive all config updates as they occur.
6
+ * - `{ timeout: number }` – Wait for a remote response until the specified timeout (in milliseconds),
7
+ * then return a cached copy if available.
8
+ */
9
+ export type DeliveryMode = 'all' | {
10
+ timeout: number;
11
+ };
12
+ /**
13
+ * Sources of returned remote config:
14
+ * - `cache` - Fetched from local storage.
15
+ * - `remote` - Fetched from remote.
16
+ */
17
+ export type Source = 'cache' | 'remote';
18
+ export declare const US_SERVER_URL = "https://sr-client-cfg.amplitude.com/config";
19
+ export declare const EU_SERVER_URL = "https://sr-client-cfg.eu.amplitude.com/config";
20
+ export declare const DEFAULT_MAX_RETRIES = 3;
21
+ export declare const FETCHED_KEYS: string[];
22
+ export interface RemoteConfig {
23
+ [key: string]: any;
24
+ }
25
+ export interface RemoteConfigInfo {
26
+ remoteConfig: RemoteConfig | null;
27
+ lastFetch: Date;
28
+ }
29
+ export interface RemoteConfigStorage {
30
+ /**
31
+ * Fetch remote config from storage asynchronously.
32
+ */
33
+ fetchConfig(): Promise<RemoteConfigInfo>;
34
+ /**
35
+ * Set remote config to storage asynchronously.
36
+ */
37
+ setConfig(config: RemoteConfigInfo): Promise<boolean>;
38
+ }
39
+ /**
40
+ * Information about each callback registered by `RemoteConfigClient.subscribe()`,
41
+ * managed internally by `RemoteConfigClient`.
42
+ */
43
+ export interface CallbackInfo {
44
+ id: string;
45
+ key?: string;
46
+ deliveryMode: DeliveryMode;
47
+ callback: RemoteConfigCallback;
48
+ lastCallback?: Date;
49
+ }
50
+ /**
51
+ * Callback used in `RemoteConfigClient.subscribe()`.
52
+ * This function is called when the remote config is fetched.
53
+ */
54
+ type RemoteConfigCallback = (remoteConfig: RemoteConfig | null, source: Source, lastFetch: Date) => void;
55
+ export interface IRemoteConfigClient {
56
+ /**
57
+ * Subscribe for updates to remote config.
58
+ * Callback is guaranteed to be called at least once,
59
+ * Whether we are able to fetch a config or not.
60
+ *
61
+ * @param key - a string containing a series of period delimited keys to filter the returned config.
62
+ * Ie, {a: {b: {c: ...}}} would return {b: {c: ...}} for "a" or {c: ...} for "a.b".
63
+ * Set to `undefined` to subscribe all keys.
64
+ * @param deliveryMode - how the initial callback is sent.
65
+ * @param callback - a block that will be called when remote config is fetched.
66
+ * @return id - identification of the subscribe and can be used to unsubscribe from updates.
67
+ */
68
+ subscribe(key: string | undefined, deliveryMode: DeliveryMode, callback: RemoteConfigCallback): string;
69
+ /**
70
+ * Unsubscribe a callback from receiving future updates.
71
+ *
72
+ * @param id - identification of the callback that you want to unsubscribe.
73
+ * It's the return value of subscribe().
74
+ * @return boolean - whether the callback is removed.
75
+ */
76
+ unsubscribe(id: string): boolean;
77
+ /**
78
+ * Request the remote config client to fetch from remote, update cache, and callback.
79
+ */
80
+ updateConfigs(): void;
81
+ }
82
+ export declare class RemoteConfigClient implements IRemoteConfigClient {
83
+ readonly apiKey: string;
84
+ readonly serverUrl: string;
85
+ readonly logger: ILogger;
86
+ readonly storage: RemoteConfigStorage;
87
+ callbackInfos: CallbackInfo[];
88
+ constructor(apiKey: string, logger: ILogger, serverZone?: ServerZoneType);
89
+ subscribe(key: string | undefined, deliveryMode: DeliveryMode, callback: RemoteConfigCallback): string;
90
+ unsubscribe(id: string): boolean;
91
+ updateConfigs(): Promise<void>;
92
+ /**
93
+ * Send remote first. If it's already complete, we can skip the cached response.
94
+ * - if remote is fetched first, no cache fetch.
95
+ * - if cache is fetched first, still fetching remote.
96
+ */
97
+ subscribeAll(callbackInfo: CallbackInfo): Promise<void>;
98
+ /**
99
+ * Waits for a remote response until the given timeout, then return a cached copy, if available.
100
+ */
101
+ subscribeWaitForRemote(callbackInfo: CallbackInfo, timeout: number): Promise<void>;
102
+ /**
103
+ * Call the callback with filtered remote config based on key.
104
+ * @param remoteConfigInfo - the whole remote config object without filtering by key.
105
+ */
106
+ sendCallback(callbackInfo: CallbackInfo, remoteConfigInfo: RemoteConfigInfo, source: Source): void;
107
+ fetch(retries?: number, timeout?: number): Promise<RemoteConfigInfo>;
108
+ /**
109
+ * Return jitter in the bound of [0,baseDelay) and then floor round.
110
+ */
111
+ getJitterDelay(baseDelay: number): number;
112
+ getUrlParams(): string;
113
+ }
114
+ export {};
115
+ //# sourceMappingURL=remote-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-config.d.ts","sourceRoot":"","sources":["../../../src/remote-config/remote-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExC,eAAO,MAAM,aAAa,+CAA+C,CAAC;AAC1E,eAAO,MAAM,aAAa,kDAAkD,CAAC;AAC7E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AASrC,eAAO,MAAM,YAAY,UAOxB,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAElC,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAEzC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,YAAY,CAAC,EAAE,IAAI,CAAC;CACrB;AAED;;;GAGG;AACH,KAAK,oBAAoB,GAAG,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,KAAK,IAAI,CAAC;AAEzG,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAEvG;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAEjC;;OAEG;IACH,aAAa,IAAI,IAAI,CAAC;CACvB;AAED,qBAAa,kBAAmB,YAAW,mBAAmB;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IAEtC,aAAa,EAAE,YAAY,EAAE,CAAM;gBAEvB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAE,cAAqB;IAO9E,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,oBAAoB,GAAG,MAAM;IAmBtG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAY1B,aAAa;IAQnB;;;;OAIG;IACG,YAAY,CAAC,YAAY,EAAE,YAAY;IAsB7C;;OAEG;IACG,sBAAsB,CAAC,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM;IA4BxE;;;OAGG;IACH,YAAY,CAAC,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM;IAuBrF,KAAK,CAAC,OAAO,GAAE,MAA4B,EAAE,OAAO,GAAE,MAAwB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA4ChH;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIzC,YAAY,IAAI,MAAM;CAUvB"}
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RemoteConfigClient = exports.FETCHED_KEYS = exports.DEFAULT_MAX_RETRIES = exports.EU_SERVER_URL = exports.US_SERVER_URL = void 0;
4
+ var tslib_1 = require("tslib");
5
+ var remote_config_localstorage_1 = require("./remote-config-localstorage");
6
+ var uuid_1 = require("../utils/uuid");
7
+ exports.US_SERVER_URL = 'https://sr-client-cfg.amplitude.com/config';
8
+ exports.EU_SERVER_URL = 'https://sr-client-cfg.eu.amplitude.com/config';
9
+ exports.DEFAULT_MAX_RETRIES = 3;
10
+ /**
11
+ * The default timeout for fetch in milliseconds.
12
+ * Linear backoff policy: timeout / retry times is the interval between fetch retry.
13
+ */
14
+ var DEFAULT_TIMEOUT = 1000;
15
+ // TODO(xinyi)
16
+ // const DEFAULT_MIN_TIME_BETWEEN_FETCHES = 5 * 60 * 1000; // 5 minutes
17
+ exports.FETCHED_KEYS = [
18
+ 'analyticsSDK.browserSDK',
19
+ 'sessionReplay.sr_interaction_config',
20
+ 'sessionReplay.sr_logging_config',
21
+ 'sessionReplay.sr_privacy_config',
22
+ 'sessionReplay.sr_sampling_config',
23
+ 'sessionReplay.sr_targeting_config',
24
+ ];
25
+ var RemoteConfigClient = /** @class */ (function () {
26
+ function RemoteConfigClient(apiKey, logger, serverZone) {
27
+ if (serverZone === void 0) { serverZone = 'US'; }
28
+ // Registered callbackInfos by subscribe().
29
+ this.callbackInfos = [];
30
+ this.apiKey = apiKey;
31
+ this.serverUrl = serverZone === 'US' ? exports.US_SERVER_URL : exports.EU_SERVER_URL;
32
+ this.logger = logger;
33
+ this.storage = new remote_config_localstorage_1.RemoteConfigLocalStorage(apiKey, logger);
34
+ }
35
+ RemoteConfigClient.prototype.subscribe = function (key, deliveryMode, callback) {
36
+ var id = (0, uuid_1.UUID)();
37
+ var callbackInfo = {
38
+ id: id,
39
+ key: key,
40
+ deliveryMode: deliveryMode,
41
+ callback: callback,
42
+ };
43
+ this.callbackInfos.push(callbackInfo);
44
+ if (deliveryMode === 'all') {
45
+ void this.subscribeAll(callbackInfo);
46
+ }
47
+ else {
48
+ void this.subscribeWaitForRemote(callbackInfo, deliveryMode.timeout);
49
+ }
50
+ return id;
51
+ };
52
+ RemoteConfigClient.prototype.unsubscribe = function (id) {
53
+ var index = this.callbackInfos.findIndex(function (callbackInfo) { return callbackInfo.id === id; });
54
+ if (index === -1) {
55
+ this.logger.debug("Remote config client unsubscribe failed because callback with id ".concat(id, " doesn't exist."));
56
+ return false;
57
+ }
58
+ this.callbackInfos.splice(index, 1);
59
+ this.logger.debug("Remote config client unsubscribe succeeded removing callback with id ".concat(id, "."));
60
+ return true;
61
+ };
62
+ RemoteConfigClient.prototype.updateConfigs = function () {
63
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
64
+ var result;
65
+ var _this = this;
66
+ return tslib_1.__generator(this, function (_a) {
67
+ switch (_a.label) {
68
+ case 0: return [4 /*yield*/, this.fetch()];
69
+ case 1:
70
+ result = _a.sent();
71
+ void this.storage.setConfig(result);
72
+ this.callbackInfos.forEach(function (callbackInfo) {
73
+ _this.sendCallback(callbackInfo, result, 'remote');
74
+ });
75
+ return [2 /*return*/];
76
+ }
77
+ });
78
+ });
79
+ };
80
+ /**
81
+ * Send remote first. If it's already complete, we can skip the cached response.
82
+ * - if remote is fetched first, no cache fetch.
83
+ * - if cache is fetched first, still fetching remote.
84
+ */
85
+ RemoteConfigClient.prototype.subscribeAll = function (callbackInfo) {
86
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
87
+ var remotePromise, cachePromise, result;
88
+ var _this = this;
89
+ return tslib_1.__generator(this, function (_a) {
90
+ switch (_a.label) {
91
+ case 0:
92
+ remotePromise = this.fetch().then(function (result) {
93
+ _this.logger.debug('Remote config client subscription all mode fetched from remote.');
94
+ _this.sendCallback(callbackInfo, result, 'remote');
95
+ void _this.storage.setConfig(result);
96
+ });
97
+ cachePromise = this.storage.fetchConfig().then(function (result) {
98
+ return result;
99
+ });
100
+ return [4 /*yield*/, Promise.race([remotePromise, cachePromise])];
101
+ case 1:
102
+ result = _a.sent();
103
+ // If cache is fetched first, wait for remote.
104
+ if (result !== undefined) {
105
+ this.logger.debug('Remote config client subscription all mode fetched from cache.');
106
+ this.sendCallback(callbackInfo, result, 'cache');
107
+ }
108
+ return [4 /*yield*/, remotePromise];
109
+ case 2:
110
+ _a.sent();
111
+ return [2 /*return*/];
112
+ }
113
+ });
114
+ });
115
+ };
116
+ /**
117
+ * Waits for a remote response until the given timeout, then return a cached copy, if available.
118
+ */
119
+ RemoteConfigClient.prototype.subscribeWaitForRemote = function (callbackInfo, timeout) {
120
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
121
+ var timeoutPromise, result, error_1, result;
122
+ return tslib_1.__generator(this, function (_a) {
123
+ switch (_a.label) {
124
+ case 0:
125
+ timeoutPromise = new Promise(function (_, reject) {
126
+ setTimeout(function () {
127
+ reject('Timeout exceeded');
128
+ }, timeout);
129
+ });
130
+ _a.label = 1;
131
+ case 1:
132
+ _a.trys.push([1, 3, , 5]);
133
+ return [4 /*yield*/, Promise.race([this.fetch(), timeoutPromise])];
134
+ case 2:
135
+ result = (_a.sent());
136
+ this.logger.debug('Remote config client subscription wait for remote mode returns from remote.');
137
+ this.sendCallback(callbackInfo, result, 'remote');
138
+ void this.storage.setConfig(result);
139
+ return [3 /*break*/, 5];
140
+ case 3:
141
+ error_1 = _a.sent();
142
+ this.logger.debug('Remote config client subscription wait for remote mode exceeded timeout. Try to fetch from cache.');
143
+ return [4 /*yield*/, this.storage.fetchConfig()];
144
+ case 4:
145
+ result = _a.sent();
146
+ if (result.remoteConfig !== null) {
147
+ this.logger.debug('Remote config client subscription wait for remote mode returns a cached copy.');
148
+ this.sendCallback(callbackInfo, result, 'cache');
149
+ }
150
+ else {
151
+ this.logger.debug('Remote config client subscription wait for remote mode failed to fetch cache.');
152
+ this.sendCallback(callbackInfo, result, 'remote');
153
+ }
154
+ return [3 /*break*/, 5];
155
+ case 5: return [2 /*return*/];
156
+ }
157
+ });
158
+ });
159
+ };
160
+ /**
161
+ * Call the callback with filtered remote config based on key.
162
+ * @param remoteConfigInfo - the whole remote config object without filtering by key.
163
+ */
164
+ RemoteConfigClient.prototype.sendCallback = function (callbackInfo, remoteConfigInfo, source) {
165
+ callbackInfo.lastCallback = new Date();
166
+ var filteredConfig;
167
+ if (callbackInfo.key) {
168
+ // Filter remote config by key.
169
+ // For example, if remote config is {a: {b: {c: 1}}},
170
+ // if key = 'a', filter result is {b: {c: 1}};
171
+ // if key = 'a.b', filter result is {c: 1}
172
+ filteredConfig = callbackInfo.key.split('.').reduce(function (config, key) {
173
+ if (config === null) {
174
+ return config;
175
+ }
176
+ return key in config ? config[key] : null;
177
+ }, remoteConfigInfo.remoteConfig);
178
+ }
179
+ else {
180
+ filteredConfig = remoteConfigInfo.remoteConfig;
181
+ }
182
+ callbackInfo.callback(filteredConfig, source, remoteConfigInfo.lastFetch);
183
+ };
184
+ RemoteConfigClient.prototype.fetch = function (retries, timeout) {
185
+ if (retries === void 0) { retries = exports.DEFAULT_MAX_RETRIES; }
186
+ if (timeout === void 0) { timeout = DEFAULT_TIMEOUT; }
187
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
188
+ var interval, failedRemoteConfigInfo, attempt, res, body, remoteConfig, error_2;
189
+ var _this = this;
190
+ return tslib_1.__generator(this, function (_a) {
191
+ switch (_a.label) {
192
+ case 0:
193
+ interval = timeout / retries;
194
+ failedRemoteConfigInfo = {
195
+ remoteConfig: null,
196
+ lastFetch: new Date(),
197
+ };
198
+ attempt = 0;
199
+ _a.label = 1;
200
+ case 1:
201
+ if (!(attempt < retries)) return [3 /*break*/, 12];
202
+ _a.label = 2;
203
+ case 2:
204
+ _a.trys.push([2, 8, , 9]);
205
+ return [4 /*yield*/, fetch(this.getUrlParams(), {
206
+ method: 'GET',
207
+ headers: {
208
+ Accept: '*/*',
209
+ },
210
+ })];
211
+ case 3:
212
+ res = _a.sent();
213
+ if (!!res.ok) return [3 /*break*/, 5];
214
+ return [4 /*yield*/, res.text()];
215
+ case 4:
216
+ body = _a.sent();
217
+ this.logger.debug("Remote config client fetch with retry time ".concat(retries, " failed with ").concat(res.status, ": ").concat(body));
218
+ return [3 /*break*/, 7];
219
+ case 5: return [4 /*yield*/, res.json()];
220
+ case 6:
221
+ remoteConfig = (_a.sent());
222
+ return [2 /*return*/, {
223
+ remoteConfig: remoteConfig,
224
+ lastFetch: new Date(),
225
+ }];
226
+ case 7: return [3 /*break*/, 9];
227
+ case 8:
228
+ error_2 = _a.sent();
229
+ // Handle rejects when the request fails, for example, a network error
230
+ this.logger.debug("Remote config client fetch with retry time ".concat(retries, " is rejected because: "), error_2);
231
+ return [3 /*break*/, 9];
232
+ case 9:
233
+ if (!(attempt < retries - 1)) return [3 /*break*/, 11];
234
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, _this.getJitterDelay(interval)); })];
235
+ case 10:
236
+ _a.sent();
237
+ _a.label = 11;
238
+ case 11:
239
+ attempt++;
240
+ return [3 /*break*/, 1];
241
+ case 12: return [2 /*return*/, failedRemoteConfigInfo];
242
+ }
243
+ });
244
+ });
245
+ };
246
+ /**
247
+ * Return jitter in the bound of [0,baseDelay) and then floor round.
248
+ */
249
+ RemoteConfigClient.prototype.getJitterDelay = function (baseDelay) {
250
+ return Math.floor(Math.random() * baseDelay);
251
+ };
252
+ RemoteConfigClient.prototype.getUrlParams = function () {
253
+ var urlParams = new URLSearchParams({
254
+ api_key: this.apiKey,
255
+ });
256
+ exports.FETCHED_KEYS.forEach(function (key) {
257
+ urlParams.append('config_keys', key);
258
+ });
259
+ return "".concat(this.serverUrl, "?").concat(urlParams.toString());
260
+ };
261
+ return RemoteConfigClient;
262
+ }());
263
+ exports.RemoteConfigClient = RemoteConfigClient;
264
+ //# sourceMappingURL=remote-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-config.js","sourceRoot":"","sources":["../../../src/remote-config/remote-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { ServerZoneType } from '../types/server-zone';\nimport { ILogger } from '../logger';\nimport { RemoteConfigLocalStorage } from './remote-config-localstorage';\nimport { UUID } from '../utils/uuid';\n\n/**\n * Modes for receiving remote config updates:\n * - `'all'` – Receive all config updates as they occur.\n * - `{ timeout: number }` – Wait for a remote response until the specified timeout (in milliseconds),\n * then return a cached copy if available.\n */\nexport type DeliveryMode = 'all' | { timeout: number };\n\n/**\n * Sources of returned remote config:\n * - `cache` - Fetched from local storage.\n * - `remote` - Fetched from remote.\n */\nexport type Source = 'cache' | 'remote';\n\nexport const US_SERVER_URL = 'https://sr-client-cfg.amplitude.com/config';\nexport const EU_SERVER_URL = 'https://sr-client-cfg.eu.amplitude.com/config';\nexport const DEFAULT_MAX_RETRIES = 3;\n\n/**\n * The default timeout for fetch in milliseconds.\n * Linear backoff policy: timeout / retry times is the interval between fetch retry.\n */\nconst DEFAULT_TIMEOUT = 1000;\n// TODO(xinyi)\n// const DEFAULT_MIN_TIME_BETWEEN_FETCHES = 5 * 60 * 1000; // 5 minutes\nexport const FETCHED_KEYS = [\n 'analyticsSDK.browserSDK',\n 'sessionReplay.sr_interaction_config',\n 'sessionReplay.sr_logging_config',\n 'sessionReplay.sr_privacy_config',\n 'sessionReplay.sr_sampling_config',\n 'sessionReplay.sr_targeting_config',\n];\n\nexport interface RemoteConfig {\n [key: string]: any;\n}\n\nexport interface RemoteConfigInfo {\n remoteConfig: RemoteConfig | null;\n // Timestamp of when the remote config was fetched.\n lastFetch: Date;\n}\n\nexport interface RemoteConfigStorage {\n /**\n * Fetch remote config from storage asynchronously.\n */\n fetchConfig(): Promise<RemoteConfigInfo>;\n\n /**\n * Set remote config to storage asynchronously.\n */\n setConfig(config: RemoteConfigInfo): Promise<boolean>;\n}\n\n/**\n * Information about each callback registered by `RemoteConfigClient.subscribe()`,\n * managed internally by `RemoteConfigClient`.\n */\nexport interface CallbackInfo {\n id: string;\n key?: string;\n deliveryMode: DeliveryMode;\n callback: RemoteConfigCallback;\n lastCallback?: Date;\n}\n\n/**\n * Callback used in `RemoteConfigClient.subscribe()`.\n * This function is called when the remote config is fetched.\n */\ntype RemoteConfigCallback = (remoteConfig: RemoteConfig | null, source: Source, lastFetch: Date) => void;\n\nexport interface IRemoteConfigClient {\n /**\n * Subscribe for updates to remote config.\n * Callback is guaranteed to be called at least once,\n * Whether we are able to fetch a config or not.\n *\n * @param key - a string containing a series of period delimited keys to filter the returned config.\n * Ie, {a: {b: {c: ...}}} would return {b: {c: ...}} for \"a\" or {c: ...} for \"a.b\".\n * Set to `undefined` to subscribe all keys.\n * @param deliveryMode - how the initial callback is sent.\n * @param callback - a block that will be called when remote config is fetched.\n * @return id - identification of the subscribe and can be used to unsubscribe from updates.\n */\n subscribe(key: string | undefined, deliveryMode: DeliveryMode, callback: RemoteConfigCallback): string;\n\n /**\n * Unsubscribe a callback from receiving future updates.\n *\n * @param id - identification of the callback that you want to unsubscribe.\n * It's the return value of subscribe().\n * @return boolean - whether the callback is removed.\n */\n unsubscribe(id: string): boolean;\n\n /**\n * Request the remote config client to fetch from remote, update cache, and callback.\n */\n updateConfigs(): void;\n}\n\nexport class RemoteConfigClient implements IRemoteConfigClient {\n readonly apiKey: string;\n readonly serverUrl: string;\n readonly logger: ILogger;\n readonly storage: RemoteConfigStorage;\n // Registered callbackInfos by subscribe().\n callbackInfos: CallbackInfo[] = [];\n\n constructor(apiKey: string, logger: ILogger, serverZone: ServerZoneType = 'US') {\n this.apiKey = apiKey;\n this.serverUrl = serverZone === 'US' ? US_SERVER_URL : EU_SERVER_URL;\n this.logger = logger;\n this.storage = new RemoteConfigLocalStorage(apiKey, logger);\n }\n\n subscribe(key: string | undefined, deliveryMode: DeliveryMode, callback: RemoteConfigCallback): string {\n const id = UUID();\n const callbackInfo = {\n id: id,\n key: key,\n deliveryMode: deliveryMode,\n callback: callback,\n };\n this.callbackInfos.push(callbackInfo);\n\n if (deliveryMode === 'all') {\n void this.subscribeAll(callbackInfo);\n } else {\n void this.subscribeWaitForRemote(callbackInfo, deliveryMode.timeout);\n }\n\n return id;\n }\n\n unsubscribe(id: string): boolean {\n const index = this.callbackInfos.findIndex((callbackInfo) => callbackInfo.id === id);\n if (index === -1) {\n this.logger.debug(`Remote config client unsubscribe failed because callback with id ${id} doesn't exist.`);\n return false;\n }\n\n this.callbackInfos.splice(index, 1);\n this.logger.debug(`Remote config client unsubscribe succeeded removing callback with id ${id}.`);\n return true;\n }\n\n async updateConfigs() {\n const result = await this.fetch();\n void this.storage.setConfig(result);\n this.callbackInfos.forEach((callbackInfo) => {\n this.sendCallback(callbackInfo, result, 'remote');\n });\n }\n\n /**\n * Send remote first. If it's already complete, we can skip the cached response.\n * - if remote is fetched first, no cache fetch.\n * - if cache is fetched first, still fetching remote.\n */\n async subscribeAll(callbackInfo: CallbackInfo) {\n const remotePromise = this.fetch().then((result) => {\n this.logger.debug('Remote config client subscription all mode fetched from remote.');\n this.sendCallback(callbackInfo, result, 'remote');\n void this.storage.setConfig(result);\n });\n\n const cachePromise = this.storage.fetchConfig().then((result) => {\n return result;\n });\n\n // Wait for the first result to resolve\n const result = await Promise.race([remotePromise, cachePromise]);\n\n // If cache is fetched first, wait for remote.\n if (result !== undefined) {\n this.logger.debug('Remote config client subscription all mode fetched from cache.');\n this.sendCallback(callbackInfo, result, 'cache');\n }\n await remotePromise;\n }\n\n /**\n * Waits for a remote response until the given timeout, then return a cached copy, if available.\n */\n async subscribeWaitForRemote(callbackInfo: CallbackInfo, timeout: number) {\n const timeoutPromise = new Promise((_, reject) => {\n setTimeout(() => {\n reject('Timeout exceeded');\n }, timeout);\n });\n\n try {\n const result: RemoteConfigInfo = (await Promise.race([this.fetch(), timeoutPromise])) as RemoteConfigInfo;\n\n this.logger.debug('Remote config client subscription wait for remote mode returns from remote.');\n this.sendCallback(callbackInfo, result, 'remote');\n void this.storage.setConfig(result);\n } catch (error) {\n this.logger.debug(\n 'Remote config client subscription wait for remote mode exceeded timeout. Try to fetch from cache.',\n );\n const result = await this.storage.fetchConfig();\n if (result.remoteConfig !== null) {\n this.logger.debug('Remote config client subscription wait for remote mode returns a cached copy.');\n this.sendCallback(callbackInfo, result, 'cache');\n } else {\n this.logger.debug('Remote config client subscription wait for remote mode failed to fetch cache.');\n this.sendCallback(callbackInfo, result, 'remote');\n }\n }\n }\n\n /**\n * Call the callback with filtered remote config based on key.\n * @param remoteConfigInfo - the whole remote config object without filtering by key.\n */\n sendCallback(callbackInfo: CallbackInfo, remoteConfigInfo: RemoteConfigInfo, source: Source) {\n callbackInfo.lastCallback = new Date();\n\n let filteredConfig: RemoteConfig | null;\n if (callbackInfo.key) {\n // Filter remote config by key.\n // For example, if remote config is {a: {b: {c: 1}}},\n // if key = 'a', filter result is {b: {c: 1}};\n // if key = 'a.b', filter result is {c: 1}\n filteredConfig = callbackInfo.key.split('.').reduce((config, key) => {\n if (config === null) {\n return config;\n }\n\n return key in config ? (config[key] as RemoteConfig) : null;\n }, remoteConfigInfo.remoteConfig);\n } else {\n filteredConfig = remoteConfigInfo.remoteConfig;\n }\n\n callbackInfo.callback(filteredConfig, source, remoteConfigInfo.lastFetch);\n }\n\n async fetch(retries: number = DEFAULT_MAX_RETRIES, timeout: number = DEFAULT_TIMEOUT): Promise<RemoteConfigInfo> {\n const interval = timeout / retries;\n const failedRemoteConfigInfo: RemoteConfigInfo = {\n remoteConfig: null,\n lastFetch: new Date(),\n };\n\n for (let attempt = 0; attempt < retries; attempt++) {\n try {\n const res = await fetch(this.getUrlParams(), {\n method: 'GET',\n headers: {\n Accept: '*/*',\n },\n });\n\n // Handle unsuccessful fetch\n if (!res.ok) {\n const body = await res.text();\n this.logger.debug(`Remote config client fetch with retry time ${retries} failed with ${res.status}: ${body}`);\n } else {\n // Handle successful fetch\n const remoteConfig: RemoteConfig = (await res.json()) as RemoteConfig;\n return {\n remoteConfig: remoteConfig,\n lastFetch: new Date(),\n };\n }\n } catch (error) {\n // Handle rejects when the request fails, for example, a network error\n this.logger.debug(`Remote config client fetch with retry time ${retries} is rejected because: `, error);\n }\n\n // Linear backoff:\n // wait for the specified interval before the next attempt\n // except after the last attempt.\n if (attempt < retries - 1) {\n await new Promise((resolve) => setTimeout(resolve, this.getJitterDelay(interval)));\n }\n }\n\n return failedRemoteConfigInfo;\n }\n\n /**\n * Return jitter in the bound of [0,baseDelay) and then floor round.\n */\n getJitterDelay(baseDelay: number): number {\n return Math.floor(Math.random() * baseDelay);\n }\n\n getUrlParams(): string {\n const urlParams = new URLSearchParams({\n api_key: this.apiKey,\n });\n FETCHED_KEYS.forEach((key) => {\n urlParams.append('config_keys', key);\n });\n\n return `${this.serverUrl}?${urlParams.toString()}`;\n }\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { RemoteConfigStorage, RemoteConfigInfo } from './remote-config';
2
+ import { ILogger } from '../logger';
3
+ export declare class RemoteConfigLocalStorage implements RemoteConfigStorage {
4
+ private readonly key;
5
+ private readonly logger;
6
+ constructor(apiKey: string, logger: ILogger);
7
+ fetchConfig(): Promise<RemoteConfigInfo>;
8
+ setConfig(config: RemoteConfigInfo): Promise<boolean>;
9
+ }
10
+ //# sourceMappingURL=remote-config-localstorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-config-localstorage.d.ts","sourceRoot":"","sources":["../../../src/remote-config/remote-config-localstorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,qBAAa,wBAAyB,YAAW,mBAAmB;IAClE,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;gBAErB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO;IAK3C,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAiCxC,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CAUtD"}
@@ -0,0 +1,51 @@
1
+ var RemoteConfigLocalStorage = /** @class */ (function () {
2
+ function RemoteConfigLocalStorage(apiKey, logger) {
3
+ this.key = "AMP_remote_config_".concat(apiKey.substring(0, 10));
4
+ this.logger = logger;
5
+ }
6
+ RemoteConfigLocalStorage.prototype.fetchConfig = function () {
7
+ var result = null;
8
+ var failedRemoteConfigInfo = {
9
+ remoteConfig: null,
10
+ lastFetch: new Date(),
11
+ };
12
+ try {
13
+ result = localStorage.getItem(this.key);
14
+ }
15
+ catch (error) {
16
+ this.logger.debug('Remote config localstorage failed to access: ', error);
17
+ return Promise.resolve(failedRemoteConfigInfo);
18
+ }
19
+ if (result === null) {
20
+ this.logger.debug('Remote config localstorage gets null because the key does not exist');
21
+ return Promise.resolve(failedRemoteConfigInfo);
22
+ }
23
+ try {
24
+ var remoteConfigInfo = JSON.parse(result);
25
+ this.logger.debug('Remote config localstorage parsed successfully.');
26
+ return Promise.resolve({
27
+ remoteConfig: remoteConfigInfo.remoteConfig,
28
+ lastFetch: new Date(remoteConfigInfo.lastFetch),
29
+ });
30
+ }
31
+ catch (error) {
32
+ this.logger.debug('Remote config localstorage failed to parse: ', error);
33
+ localStorage.removeItem(this.key);
34
+ return Promise.resolve(failedRemoteConfigInfo);
35
+ }
36
+ };
37
+ RemoteConfigLocalStorage.prototype.setConfig = function (config) {
38
+ try {
39
+ localStorage.setItem(this.key, JSON.stringify(config));
40
+ this.logger.debug('Remote config localstorage set successfully.');
41
+ return Promise.resolve(true);
42
+ }
43
+ catch (error) {
44
+ this.logger.debug('Remote config localstorage failed to set: ', error);
45
+ }
46
+ return Promise.resolve(false);
47
+ };
48
+ return RemoteConfigLocalStorage;
49
+ }());
50
+ export { RemoteConfigLocalStorage };
51
+ //# sourceMappingURL=remote-config-localstorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-config-localstorage.js","sourceRoot":"","sources":["../../../src/remote-config/remote-config-localstorage.ts"],"names":[],"mappings":"AAGA;IAIE,kCAAY,MAAc,EAAE,MAAe;QACzC,IAAI,CAAC,GAAG,GAAG,4BAAqB,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAE,CAAC;QAC1D,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,8CAAW,GAAX;QACE,IAAI,MAAM,GAAkB,IAAI,CAAC;QACjC,IAAM,sBAAsB,GAAG;YAC7B,YAAY,EAAE,IAAI;YAClB,SAAS,EAAE,IAAI,IAAI,EAAE;SACtB,CAAC;QAEF,IAAI;YACF,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SACzC;QAAC,OAAO,KAAK,EAAE;YACd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,+CAA+C,EAAE,KAAK,CAAC,CAAC;YAC1E,OAAO,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;SAChD;QAED,IAAI,MAAM,KAAK,IAAI,EAAE;YACnB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;YACzF,OAAO,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;SAChD;QAED,IAAI;YACF,IAAM,gBAAgB,GAAqB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAqB,CAAC;YAClF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;YACrE,OAAO,OAAO,CAAC,OAAO,CAAC;gBACrB,YAAY,EAAE,gBAAgB,CAAC,YAAY;gBAC3C,SAAS,EAAE,IAAI,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC;aAChD,CAAC,CAAC;SACJ;QAAC,OAAO,KAAK,EAAE;YACd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,EAAE,KAAK,CAAC,CAAC;YACzE,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClC,OAAO,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;SAChD;IACH,CAAC;IAED,4CAAS,GAAT,UAAU,MAAwB;QAChC,IAAI;YACF,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YACvD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;YAClE,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;SAC9B;QAAC,OAAO,KAAK,EAAE;YACd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,4CAA4C,EAAE,KAAK,CAAC,CAAC;SACxE;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IACH,+BAAC;AAAD,CAAC,AApDD,IAoDC","sourcesContent":["import { RemoteConfigStorage, RemoteConfigInfo } from './remote-config';\nimport { ILogger } from '../logger';\n\nexport class RemoteConfigLocalStorage implements RemoteConfigStorage {\n private readonly key: string;\n private readonly logger: ILogger;\n\n constructor(apiKey: string, logger: ILogger) {\n this.key = `AMP_remote_config_${apiKey.substring(0, 10)}`;\n this.logger = logger;\n }\n\n fetchConfig(): Promise<RemoteConfigInfo> {\n let result: string | null = null;\n const failedRemoteConfigInfo = {\n remoteConfig: null,\n lastFetch: new Date(),\n };\n\n try {\n result = localStorage.getItem(this.key);\n } catch (error) {\n this.logger.debug('Remote config localstorage failed to access: ', error);\n return Promise.resolve(failedRemoteConfigInfo);\n }\n\n if (result === null) {\n this.logger.debug('Remote config localstorage gets null because the key does not exist');\n return Promise.resolve(failedRemoteConfigInfo);\n }\n\n try {\n const remoteConfigInfo: RemoteConfigInfo = JSON.parse(result) as RemoteConfigInfo;\n this.logger.debug('Remote config localstorage parsed successfully.');\n return Promise.resolve({\n remoteConfig: remoteConfigInfo.remoteConfig,\n lastFetch: new Date(remoteConfigInfo.lastFetch),\n });\n } catch (error) {\n this.logger.debug('Remote config localstorage failed to parse: ', error);\n localStorage.removeItem(this.key);\n return Promise.resolve(failedRemoteConfigInfo);\n }\n }\n\n setConfig(config: RemoteConfigInfo): Promise<boolean> {\n try {\n localStorage.setItem(this.key, JSON.stringify(config));\n this.logger.debug('Remote config localstorage set successfully.');\n return Promise.resolve(true);\n } catch (error) {\n this.logger.debug('Remote config localstorage failed to set: ', error);\n }\n return Promise.resolve(false);\n }\n}\n"]}
@@ -0,0 +1,115 @@
1
+ import { ServerZoneType } from '../types/server-zone';
2
+ import { ILogger } from '../logger';
3
+ /**
4
+ * Modes for receiving remote config updates:
5
+ * - `'all'` – Receive all config updates as they occur.
6
+ * - `{ timeout: number }` – Wait for a remote response until the specified timeout (in milliseconds),
7
+ * then return a cached copy if available.
8
+ */
9
+ export type DeliveryMode = 'all' | {
10
+ timeout: number;
11
+ };
12
+ /**
13
+ * Sources of returned remote config:
14
+ * - `cache` - Fetched from local storage.
15
+ * - `remote` - Fetched from remote.
16
+ */
17
+ export type Source = 'cache' | 'remote';
18
+ export declare const US_SERVER_URL = "https://sr-client-cfg.amplitude.com/config";
19
+ export declare const EU_SERVER_URL = "https://sr-client-cfg.eu.amplitude.com/config";
20
+ export declare const DEFAULT_MAX_RETRIES = 3;
21
+ export declare const FETCHED_KEYS: string[];
22
+ export interface RemoteConfig {
23
+ [key: string]: any;
24
+ }
25
+ export interface RemoteConfigInfo {
26
+ remoteConfig: RemoteConfig | null;
27
+ lastFetch: Date;
28
+ }
29
+ export interface RemoteConfigStorage {
30
+ /**
31
+ * Fetch remote config from storage asynchronously.
32
+ */
33
+ fetchConfig(): Promise<RemoteConfigInfo>;
34
+ /**
35
+ * Set remote config to storage asynchronously.
36
+ */
37
+ setConfig(config: RemoteConfigInfo): Promise<boolean>;
38
+ }
39
+ /**
40
+ * Information about each callback registered by `RemoteConfigClient.subscribe()`,
41
+ * managed internally by `RemoteConfigClient`.
42
+ */
43
+ export interface CallbackInfo {
44
+ id: string;
45
+ key?: string;
46
+ deliveryMode: DeliveryMode;
47
+ callback: RemoteConfigCallback;
48
+ lastCallback?: Date;
49
+ }
50
+ /**
51
+ * Callback used in `RemoteConfigClient.subscribe()`.
52
+ * This function is called when the remote config is fetched.
53
+ */
54
+ type RemoteConfigCallback = (remoteConfig: RemoteConfig | null, source: Source, lastFetch: Date) => void;
55
+ export interface IRemoteConfigClient {
56
+ /**
57
+ * Subscribe for updates to remote config.
58
+ * Callback is guaranteed to be called at least once,
59
+ * Whether we are able to fetch a config or not.
60
+ *
61
+ * @param key - a string containing a series of period delimited keys to filter the returned config.
62
+ * Ie, {a: {b: {c: ...}}} would return {b: {c: ...}} for "a" or {c: ...} for "a.b".
63
+ * Set to `undefined` to subscribe all keys.
64
+ * @param deliveryMode - how the initial callback is sent.
65
+ * @param callback - a block that will be called when remote config is fetched.
66
+ * @return id - identification of the subscribe and can be used to unsubscribe from updates.
67
+ */
68
+ subscribe(key: string | undefined, deliveryMode: DeliveryMode, callback: RemoteConfigCallback): string;
69
+ /**
70
+ * Unsubscribe a callback from receiving future updates.
71
+ *
72
+ * @param id - identification of the callback that you want to unsubscribe.
73
+ * It's the return value of subscribe().
74
+ * @return boolean - whether the callback is removed.
75
+ */
76
+ unsubscribe(id: string): boolean;
77
+ /**
78
+ * Request the remote config client to fetch from remote, update cache, and callback.
79
+ */
80
+ updateConfigs(): void;
81
+ }
82
+ export declare class RemoteConfigClient implements IRemoteConfigClient {
83
+ readonly apiKey: string;
84
+ readonly serverUrl: string;
85
+ readonly logger: ILogger;
86
+ readonly storage: RemoteConfigStorage;
87
+ callbackInfos: CallbackInfo[];
88
+ constructor(apiKey: string, logger: ILogger, serverZone?: ServerZoneType);
89
+ subscribe(key: string | undefined, deliveryMode: DeliveryMode, callback: RemoteConfigCallback): string;
90
+ unsubscribe(id: string): boolean;
91
+ updateConfigs(): Promise<void>;
92
+ /**
93
+ * Send remote first. If it's already complete, we can skip the cached response.
94
+ * - if remote is fetched first, no cache fetch.
95
+ * - if cache is fetched first, still fetching remote.
96
+ */
97
+ subscribeAll(callbackInfo: CallbackInfo): Promise<void>;
98
+ /**
99
+ * Waits for a remote response until the given timeout, then return a cached copy, if available.
100
+ */
101
+ subscribeWaitForRemote(callbackInfo: CallbackInfo, timeout: number): Promise<void>;
102
+ /**
103
+ * Call the callback with filtered remote config based on key.
104
+ * @param remoteConfigInfo - the whole remote config object without filtering by key.
105
+ */
106
+ sendCallback(callbackInfo: CallbackInfo, remoteConfigInfo: RemoteConfigInfo, source: Source): void;
107
+ fetch(retries?: number, timeout?: number): Promise<RemoteConfigInfo>;
108
+ /**
109
+ * Return jitter in the bound of [0,baseDelay) and then floor round.
110
+ */
111
+ getJitterDelay(baseDelay: number): number;
112
+ getUrlParams(): string;
113
+ }
114
+ export {};
115
+ //# sourceMappingURL=remote-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-config.d.ts","sourceRoot":"","sources":["../../../src/remote-config/remote-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvD;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExC,eAAO,MAAM,aAAa,+CAA+C,CAAC;AAC1E,eAAO,MAAM,aAAa,kDAAkD,CAAC;AAC7E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AASrC,eAAO,MAAM,YAAY,UAOxB,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAElC,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAEzC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,YAAY,CAAC,EAAE,IAAI,CAAC;CACrB;AAED;;;GAGG;AACH,KAAK,oBAAoB,GAAG,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,KAAK,IAAI,CAAC;AAEzG,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAEvG;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAEjC;;OAEG;IACH,aAAa,IAAI,IAAI,CAAC;CACvB;AAED,qBAAa,kBAAmB,YAAW,mBAAmB;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IAEtC,aAAa,EAAE,YAAY,EAAE,CAAM;gBAEvB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAE,cAAqB;IAO9E,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,oBAAoB,GAAG,MAAM;IAmBtG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAY1B,aAAa;IAQnB;;;;OAIG;IACG,YAAY,CAAC,YAAY,EAAE,YAAY;IAsB7C;;OAEG;IACG,sBAAsB,CAAC,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM;IA4BxE;;;OAGG;IACH,YAAY,CAAC,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM;IAuBrF,KAAK,CAAC,OAAO,GAAE,MAA4B,EAAE,OAAO,GAAE,MAAwB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA4ChH;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIzC,YAAY,IAAI,MAAM;CAUvB"}
@@ -0,0 +1,261 @@
1
+ import { __awaiter, __generator } from "tslib";
2
+ import { RemoteConfigLocalStorage } from './remote-config-localstorage';
3
+ import { UUID } from '../utils/uuid';
4
+ export var US_SERVER_URL = 'https://sr-client-cfg.amplitude.com/config';
5
+ export var EU_SERVER_URL = 'https://sr-client-cfg.eu.amplitude.com/config';
6
+ export var DEFAULT_MAX_RETRIES = 3;
7
+ /**
8
+ * The default timeout for fetch in milliseconds.
9
+ * Linear backoff policy: timeout / retry times is the interval between fetch retry.
10
+ */
11
+ var DEFAULT_TIMEOUT = 1000;
12
+ // TODO(xinyi)
13
+ // const DEFAULT_MIN_TIME_BETWEEN_FETCHES = 5 * 60 * 1000; // 5 minutes
14
+ export var FETCHED_KEYS = [
15
+ 'analyticsSDK.browserSDK',
16
+ 'sessionReplay.sr_interaction_config',
17
+ 'sessionReplay.sr_logging_config',
18
+ 'sessionReplay.sr_privacy_config',
19
+ 'sessionReplay.sr_sampling_config',
20
+ 'sessionReplay.sr_targeting_config',
21
+ ];
22
+ var RemoteConfigClient = /** @class */ (function () {
23
+ function RemoteConfigClient(apiKey, logger, serverZone) {
24
+ if (serverZone === void 0) { serverZone = 'US'; }
25
+ // Registered callbackInfos by subscribe().
26
+ this.callbackInfos = [];
27
+ this.apiKey = apiKey;
28
+ this.serverUrl = serverZone === 'US' ? US_SERVER_URL : EU_SERVER_URL;
29
+ this.logger = logger;
30
+ this.storage = new RemoteConfigLocalStorage(apiKey, logger);
31
+ }
32
+ RemoteConfigClient.prototype.subscribe = function (key, deliveryMode, callback) {
33
+ var id = UUID();
34
+ var callbackInfo = {
35
+ id: id,
36
+ key: key,
37
+ deliveryMode: deliveryMode,
38
+ callback: callback,
39
+ };
40
+ this.callbackInfos.push(callbackInfo);
41
+ if (deliveryMode === 'all') {
42
+ void this.subscribeAll(callbackInfo);
43
+ }
44
+ else {
45
+ void this.subscribeWaitForRemote(callbackInfo, deliveryMode.timeout);
46
+ }
47
+ return id;
48
+ };
49
+ RemoteConfigClient.prototype.unsubscribe = function (id) {
50
+ var index = this.callbackInfos.findIndex(function (callbackInfo) { return callbackInfo.id === id; });
51
+ if (index === -1) {
52
+ this.logger.debug("Remote config client unsubscribe failed because callback with id ".concat(id, " doesn't exist."));
53
+ return false;
54
+ }
55
+ this.callbackInfos.splice(index, 1);
56
+ this.logger.debug("Remote config client unsubscribe succeeded removing callback with id ".concat(id, "."));
57
+ return true;
58
+ };
59
+ RemoteConfigClient.prototype.updateConfigs = function () {
60
+ return __awaiter(this, void 0, void 0, function () {
61
+ var result;
62
+ var _this = this;
63
+ return __generator(this, function (_a) {
64
+ switch (_a.label) {
65
+ case 0: return [4 /*yield*/, this.fetch()];
66
+ case 1:
67
+ result = _a.sent();
68
+ void this.storage.setConfig(result);
69
+ this.callbackInfos.forEach(function (callbackInfo) {
70
+ _this.sendCallback(callbackInfo, result, 'remote');
71
+ });
72
+ return [2 /*return*/];
73
+ }
74
+ });
75
+ });
76
+ };
77
+ /**
78
+ * Send remote first. If it's already complete, we can skip the cached response.
79
+ * - if remote is fetched first, no cache fetch.
80
+ * - if cache is fetched first, still fetching remote.
81
+ */
82
+ RemoteConfigClient.prototype.subscribeAll = function (callbackInfo) {
83
+ return __awaiter(this, void 0, void 0, function () {
84
+ var remotePromise, cachePromise, result;
85
+ var _this = this;
86
+ return __generator(this, function (_a) {
87
+ switch (_a.label) {
88
+ case 0:
89
+ remotePromise = this.fetch().then(function (result) {
90
+ _this.logger.debug('Remote config client subscription all mode fetched from remote.');
91
+ _this.sendCallback(callbackInfo, result, 'remote');
92
+ void _this.storage.setConfig(result);
93
+ });
94
+ cachePromise = this.storage.fetchConfig().then(function (result) {
95
+ return result;
96
+ });
97
+ return [4 /*yield*/, Promise.race([remotePromise, cachePromise])];
98
+ case 1:
99
+ result = _a.sent();
100
+ // If cache is fetched first, wait for remote.
101
+ if (result !== undefined) {
102
+ this.logger.debug('Remote config client subscription all mode fetched from cache.');
103
+ this.sendCallback(callbackInfo, result, 'cache');
104
+ }
105
+ return [4 /*yield*/, remotePromise];
106
+ case 2:
107
+ _a.sent();
108
+ return [2 /*return*/];
109
+ }
110
+ });
111
+ });
112
+ };
113
+ /**
114
+ * Waits for a remote response until the given timeout, then return a cached copy, if available.
115
+ */
116
+ RemoteConfigClient.prototype.subscribeWaitForRemote = function (callbackInfo, timeout) {
117
+ return __awaiter(this, void 0, void 0, function () {
118
+ var timeoutPromise, result, error_1, result;
119
+ return __generator(this, function (_a) {
120
+ switch (_a.label) {
121
+ case 0:
122
+ timeoutPromise = new Promise(function (_, reject) {
123
+ setTimeout(function () {
124
+ reject('Timeout exceeded');
125
+ }, timeout);
126
+ });
127
+ _a.label = 1;
128
+ case 1:
129
+ _a.trys.push([1, 3, , 5]);
130
+ return [4 /*yield*/, Promise.race([this.fetch(), timeoutPromise])];
131
+ case 2:
132
+ result = (_a.sent());
133
+ this.logger.debug('Remote config client subscription wait for remote mode returns from remote.');
134
+ this.sendCallback(callbackInfo, result, 'remote');
135
+ void this.storage.setConfig(result);
136
+ return [3 /*break*/, 5];
137
+ case 3:
138
+ error_1 = _a.sent();
139
+ this.logger.debug('Remote config client subscription wait for remote mode exceeded timeout. Try to fetch from cache.');
140
+ return [4 /*yield*/, this.storage.fetchConfig()];
141
+ case 4:
142
+ result = _a.sent();
143
+ if (result.remoteConfig !== null) {
144
+ this.logger.debug('Remote config client subscription wait for remote mode returns a cached copy.');
145
+ this.sendCallback(callbackInfo, result, 'cache');
146
+ }
147
+ else {
148
+ this.logger.debug('Remote config client subscription wait for remote mode failed to fetch cache.');
149
+ this.sendCallback(callbackInfo, result, 'remote');
150
+ }
151
+ return [3 /*break*/, 5];
152
+ case 5: return [2 /*return*/];
153
+ }
154
+ });
155
+ });
156
+ };
157
+ /**
158
+ * Call the callback with filtered remote config based on key.
159
+ * @param remoteConfigInfo - the whole remote config object without filtering by key.
160
+ */
161
+ RemoteConfigClient.prototype.sendCallback = function (callbackInfo, remoteConfigInfo, source) {
162
+ callbackInfo.lastCallback = new Date();
163
+ var filteredConfig;
164
+ if (callbackInfo.key) {
165
+ // Filter remote config by key.
166
+ // For example, if remote config is {a: {b: {c: 1}}},
167
+ // if key = 'a', filter result is {b: {c: 1}};
168
+ // if key = 'a.b', filter result is {c: 1}
169
+ filteredConfig = callbackInfo.key.split('.').reduce(function (config, key) {
170
+ if (config === null) {
171
+ return config;
172
+ }
173
+ return key in config ? config[key] : null;
174
+ }, remoteConfigInfo.remoteConfig);
175
+ }
176
+ else {
177
+ filteredConfig = remoteConfigInfo.remoteConfig;
178
+ }
179
+ callbackInfo.callback(filteredConfig, source, remoteConfigInfo.lastFetch);
180
+ };
181
+ RemoteConfigClient.prototype.fetch = function (retries, timeout) {
182
+ if (retries === void 0) { retries = DEFAULT_MAX_RETRIES; }
183
+ if (timeout === void 0) { timeout = DEFAULT_TIMEOUT; }
184
+ return __awaiter(this, void 0, void 0, function () {
185
+ var interval, failedRemoteConfigInfo, attempt, res, body, remoteConfig, error_2;
186
+ var _this = this;
187
+ return __generator(this, function (_a) {
188
+ switch (_a.label) {
189
+ case 0:
190
+ interval = timeout / retries;
191
+ failedRemoteConfigInfo = {
192
+ remoteConfig: null,
193
+ lastFetch: new Date(),
194
+ };
195
+ attempt = 0;
196
+ _a.label = 1;
197
+ case 1:
198
+ if (!(attempt < retries)) return [3 /*break*/, 12];
199
+ _a.label = 2;
200
+ case 2:
201
+ _a.trys.push([2, 8, , 9]);
202
+ return [4 /*yield*/, fetch(this.getUrlParams(), {
203
+ method: 'GET',
204
+ headers: {
205
+ Accept: '*/*',
206
+ },
207
+ })];
208
+ case 3:
209
+ res = _a.sent();
210
+ if (!!res.ok) return [3 /*break*/, 5];
211
+ return [4 /*yield*/, res.text()];
212
+ case 4:
213
+ body = _a.sent();
214
+ this.logger.debug("Remote config client fetch with retry time ".concat(retries, " failed with ").concat(res.status, ": ").concat(body));
215
+ return [3 /*break*/, 7];
216
+ case 5: return [4 /*yield*/, res.json()];
217
+ case 6:
218
+ remoteConfig = (_a.sent());
219
+ return [2 /*return*/, {
220
+ remoteConfig: remoteConfig,
221
+ lastFetch: new Date(),
222
+ }];
223
+ case 7: return [3 /*break*/, 9];
224
+ case 8:
225
+ error_2 = _a.sent();
226
+ // Handle rejects when the request fails, for example, a network error
227
+ this.logger.debug("Remote config client fetch with retry time ".concat(retries, " is rejected because: "), error_2);
228
+ return [3 /*break*/, 9];
229
+ case 9:
230
+ if (!(attempt < retries - 1)) return [3 /*break*/, 11];
231
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, _this.getJitterDelay(interval)); })];
232
+ case 10:
233
+ _a.sent();
234
+ _a.label = 11;
235
+ case 11:
236
+ attempt++;
237
+ return [3 /*break*/, 1];
238
+ case 12: return [2 /*return*/, failedRemoteConfigInfo];
239
+ }
240
+ });
241
+ });
242
+ };
243
+ /**
244
+ * Return jitter in the bound of [0,baseDelay) and then floor round.
245
+ */
246
+ RemoteConfigClient.prototype.getJitterDelay = function (baseDelay) {
247
+ return Math.floor(Math.random() * baseDelay);
248
+ };
249
+ RemoteConfigClient.prototype.getUrlParams = function () {
250
+ var urlParams = new URLSearchParams({
251
+ api_key: this.apiKey,
252
+ });
253
+ FETCHED_KEYS.forEach(function (key) {
254
+ urlParams.append('config_keys', key);
255
+ });
256
+ return "".concat(this.serverUrl, "?").concat(urlParams.toString());
257
+ };
258
+ return RemoteConfigClient;
259
+ }());
260
+ export { RemoteConfigClient };
261
+ //# sourceMappingURL=remote-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-config.js","sourceRoot":"","sources":["../../../src/remote-config/remote-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { ServerZoneType } from '../types/server-zone';\nimport { ILogger } from '../logger';\nimport { RemoteConfigLocalStorage } from './remote-config-localstorage';\nimport { UUID } from '../utils/uuid';\n\n/**\n * Modes for receiving remote config updates:\n * - `'all'` – Receive all config updates as they occur.\n * - `{ timeout: number }` – Wait for a remote response until the specified timeout (in milliseconds),\n * then return a cached copy if available.\n */\nexport type DeliveryMode = 'all' | { timeout: number };\n\n/**\n * Sources of returned remote config:\n * - `cache` - Fetched from local storage.\n * - `remote` - Fetched from remote.\n */\nexport type Source = 'cache' | 'remote';\n\nexport const US_SERVER_URL = 'https://sr-client-cfg.amplitude.com/config';\nexport const EU_SERVER_URL = 'https://sr-client-cfg.eu.amplitude.com/config';\nexport const DEFAULT_MAX_RETRIES = 3;\n\n/**\n * The default timeout for fetch in milliseconds.\n * Linear backoff policy: timeout / retry times is the interval between fetch retry.\n */\nconst DEFAULT_TIMEOUT = 1000;\n// TODO(xinyi)\n// const DEFAULT_MIN_TIME_BETWEEN_FETCHES = 5 * 60 * 1000; // 5 minutes\nexport const FETCHED_KEYS = [\n 'analyticsSDK.browserSDK',\n 'sessionReplay.sr_interaction_config',\n 'sessionReplay.sr_logging_config',\n 'sessionReplay.sr_privacy_config',\n 'sessionReplay.sr_sampling_config',\n 'sessionReplay.sr_targeting_config',\n];\n\nexport interface RemoteConfig {\n [key: string]: any;\n}\n\nexport interface RemoteConfigInfo {\n remoteConfig: RemoteConfig | null;\n // Timestamp of when the remote config was fetched.\n lastFetch: Date;\n}\n\nexport interface RemoteConfigStorage {\n /**\n * Fetch remote config from storage asynchronously.\n */\n fetchConfig(): Promise<RemoteConfigInfo>;\n\n /**\n * Set remote config to storage asynchronously.\n */\n setConfig(config: RemoteConfigInfo): Promise<boolean>;\n}\n\n/**\n * Information about each callback registered by `RemoteConfigClient.subscribe()`,\n * managed internally by `RemoteConfigClient`.\n */\nexport interface CallbackInfo {\n id: string;\n key?: string;\n deliveryMode: DeliveryMode;\n callback: RemoteConfigCallback;\n lastCallback?: Date;\n}\n\n/**\n * Callback used in `RemoteConfigClient.subscribe()`.\n * This function is called when the remote config is fetched.\n */\ntype RemoteConfigCallback = (remoteConfig: RemoteConfig | null, source: Source, lastFetch: Date) => void;\n\nexport interface IRemoteConfigClient {\n /**\n * Subscribe for updates to remote config.\n * Callback is guaranteed to be called at least once,\n * Whether we are able to fetch a config or not.\n *\n * @param key - a string containing a series of period delimited keys to filter the returned config.\n * Ie, {a: {b: {c: ...}}} would return {b: {c: ...}} for \"a\" or {c: ...} for \"a.b\".\n * Set to `undefined` to subscribe all keys.\n * @param deliveryMode - how the initial callback is sent.\n * @param callback - a block that will be called when remote config is fetched.\n * @return id - identification of the subscribe and can be used to unsubscribe from updates.\n */\n subscribe(key: string | undefined, deliveryMode: DeliveryMode, callback: RemoteConfigCallback): string;\n\n /**\n * Unsubscribe a callback from receiving future updates.\n *\n * @param id - identification of the callback that you want to unsubscribe.\n * It's the return value of subscribe().\n * @return boolean - whether the callback is removed.\n */\n unsubscribe(id: string): boolean;\n\n /**\n * Request the remote config client to fetch from remote, update cache, and callback.\n */\n updateConfigs(): void;\n}\n\nexport class RemoteConfigClient implements IRemoteConfigClient {\n readonly apiKey: string;\n readonly serverUrl: string;\n readonly logger: ILogger;\n readonly storage: RemoteConfigStorage;\n // Registered callbackInfos by subscribe().\n callbackInfos: CallbackInfo[] = [];\n\n constructor(apiKey: string, logger: ILogger, serverZone: ServerZoneType = 'US') {\n this.apiKey = apiKey;\n this.serverUrl = serverZone === 'US' ? US_SERVER_URL : EU_SERVER_URL;\n this.logger = logger;\n this.storage = new RemoteConfigLocalStorage(apiKey, logger);\n }\n\n subscribe(key: string | undefined, deliveryMode: DeliveryMode, callback: RemoteConfigCallback): string {\n const id = UUID();\n const callbackInfo = {\n id: id,\n key: key,\n deliveryMode: deliveryMode,\n callback: callback,\n };\n this.callbackInfos.push(callbackInfo);\n\n if (deliveryMode === 'all') {\n void this.subscribeAll(callbackInfo);\n } else {\n void this.subscribeWaitForRemote(callbackInfo, deliveryMode.timeout);\n }\n\n return id;\n }\n\n unsubscribe(id: string): boolean {\n const index = this.callbackInfos.findIndex((callbackInfo) => callbackInfo.id === id);\n if (index === -1) {\n this.logger.debug(`Remote config client unsubscribe failed because callback with id ${id} doesn't exist.`);\n return false;\n }\n\n this.callbackInfos.splice(index, 1);\n this.logger.debug(`Remote config client unsubscribe succeeded removing callback with id ${id}.`);\n return true;\n }\n\n async updateConfigs() {\n const result = await this.fetch();\n void this.storage.setConfig(result);\n this.callbackInfos.forEach((callbackInfo) => {\n this.sendCallback(callbackInfo, result, 'remote');\n });\n }\n\n /**\n * Send remote first. If it's already complete, we can skip the cached response.\n * - if remote is fetched first, no cache fetch.\n * - if cache is fetched first, still fetching remote.\n */\n async subscribeAll(callbackInfo: CallbackInfo) {\n const remotePromise = this.fetch().then((result) => {\n this.logger.debug('Remote config client subscription all mode fetched from remote.');\n this.sendCallback(callbackInfo, result, 'remote');\n void this.storage.setConfig(result);\n });\n\n const cachePromise = this.storage.fetchConfig().then((result) => {\n return result;\n });\n\n // Wait for the first result to resolve\n const result = await Promise.race([remotePromise, cachePromise]);\n\n // If cache is fetched first, wait for remote.\n if (result !== undefined) {\n this.logger.debug('Remote config client subscription all mode fetched from cache.');\n this.sendCallback(callbackInfo, result, 'cache');\n }\n await remotePromise;\n }\n\n /**\n * Waits for a remote response until the given timeout, then return a cached copy, if available.\n */\n async subscribeWaitForRemote(callbackInfo: CallbackInfo, timeout: number) {\n const timeoutPromise = new Promise((_, reject) => {\n setTimeout(() => {\n reject('Timeout exceeded');\n }, timeout);\n });\n\n try {\n const result: RemoteConfigInfo = (await Promise.race([this.fetch(), timeoutPromise])) as RemoteConfigInfo;\n\n this.logger.debug('Remote config client subscription wait for remote mode returns from remote.');\n this.sendCallback(callbackInfo, result, 'remote');\n void this.storage.setConfig(result);\n } catch (error) {\n this.logger.debug(\n 'Remote config client subscription wait for remote mode exceeded timeout. Try to fetch from cache.',\n );\n const result = await this.storage.fetchConfig();\n if (result.remoteConfig !== null) {\n this.logger.debug('Remote config client subscription wait for remote mode returns a cached copy.');\n this.sendCallback(callbackInfo, result, 'cache');\n } else {\n this.logger.debug('Remote config client subscription wait for remote mode failed to fetch cache.');\n this.sendCallback(callbackInfo, result, 'remote');\n }\n }\n }\n\n /**\n * Call the callback with filtered remote config based on key.\n * @param remoteConfigInfo - the whole remote config object without filtering by key.\n */\n sendCallback(callbackInfo: CallbackInfo, remoteConfigInfo: RemoteConfigInfo, source: Source) {\n callbackInfo.lastCallback = new Date();\n\n let filteredConfig: RemoteConfig | null;\n if (callbackInfo.key) {\n // Filter remote config by key.\n // For example, if remote config is {a: {b: {c: 1}}},\n // if key = 'a', filter result is {b: {c: 1}};\n // if key = 'a.b', filter result is {c: 1}\n filteredConfig = callbackInfo.key.split('.').reduce((config, key) => {\n if (config === null) {\n return config;\n }\n\n return key in config ? (config[key] as RemoteConfig) : null;\n }, remoteConfigInfo.remoteConfig);\n } else {\n filteredConfig = remoteConfigInfo.remoteConfig;\n }\n\n callbackInfo.callback(filteredConfig, source, remoteConfigInfo.lastFetch);\n }\n\n async fetch(retries: number = DEFAULT_MAX_RETRIES, timeout: number = DEFAULT_TIMEOUT): Promise<RemoteConfigInfo> {\n const interval = timeout / retries;\n const failedRemoteConfigInfo: RemoteConfigInfo = {\n remoteConfig: null,\n lastFetch: new Date(),\n };\n\n for (let attempt = 0; attempt < retries; attempt++) {\n try {\n const res = await fetch(this.getUrlParams(), {\n method: 'GET',\n headers: {\n Accept: '*/*',\n },\n });\n\n // Handle unsuccessful fetch\n if (!res.ok) {\n const body = await res.text();\n this.logger.debug(`Remote config client fetch with retry time ${retries} failed with ${res.status}: ${body}`);\n } else {\n // Handle successful fetch\n const remoteConfig: RemoteConfig = (await res.json()) as RemoteConfig;\n return {\n remoteConfig: remoteConfig,\n lastFetch: new Date(),\n };\n }\n } catch (error) {\n // Handle rejects when the request fails, for example, a network error\n this.logger.debug(`Remote config client fetch with retry time ${retries} is rejected because: `, error);\n }\n\n // Linear backoff:\n // wait for the specified interval before the next attempt\n // except after the last attempt.\n if (attempt < retries - 1) {\n await new Promise((resolve) => setTimeout(resolve, this.getJitterDelay(interval)));\n }\n }\n\n return failedRemoteConfigInfo;\n }\n\n /**\n * Return jitter in the bound of [0,baseDelay) and then floor round.\n */\n getJitterDelay(baseDelay: number): number {\n return Math.floor(Math.random() * baseDelay);\n }\n\n getUrlParams(): string {\n const urlParams = new URLSearchParams({\n api_key: this.apiKey,\n });\n FETCHED_KEYS.forEach((key) => {\n urlParams.append('config_keys', key);\n });\n\n return `${this.serverUrl}?${urlParams.toString()}`;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amplitude/analytics-core",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "",
5
5
  "author": "Amplitude Inc",
6
6
  "homepage": "https://github.com/amplitude/Amplitude-TypeScript",
@@ -40,5 +40,5 @@
40
40
  "files": [
41
41
  "lib"
42
42
  ],
43
- "gitHead": "752b2b1ac8983914f501c4cf69da22b75ef572e4"
43
+ "gitHead": "0f7347c36a91764514bea8508f5792d1a27fde52"
44
44
  }