@firebase/remote-config 0.7.0 → 0.8.0-20260114160934
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.
- package/dist/esm/index.esm.js +85 -6
- package/dist/esm/index.esm.js.map +1 -1
- package/dist/esm/src/abt/experiment.d.ts +13 -0
- package/dist/esm/src/errors.d.ts +5 -1
- package/dist/esm/src/public_types.d.ts +19 -0
- package/dist/esm/src/remote_config.d.ts +11 -1
- package/dist/esm/src/storage/storage.d.ts +3 -1
- package/dist/index.cjs.js +85 -6
- package/dist/index.cjs.js.map +1 -1
- package/dist/remote-config-public.d.ts +20 -0
- package/dist/remote-config.d.ts +20 -0
- package/dist/src/abt/experiment.d.ts +13 -0
- package/dist/src/errors.d.ts +5 -1
- package/dist/src/global_index.d.ts +20 -1
- package/dist/src/public_types.d.ts +19 -0
- package/dist/src/remote_config.d.ts +11 -1
- package/dist/src/storage/storage.d.ts +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FirebaseExperimentDescription } from '../public_types';
|
|
2
|
+
import { RemoteConfig } from '../remote_config';
|
|
3
|
+
export declare class Experiment {
|
|
4
|
+
private storage;
|
|
5
|
+
private logger;
|
|
6
|
+
private analyticsProvider;
|
|
7
|
+
constructor(rc: RemoteConfig);
|
|
8
|
+
updateActiveExperiments(latestExperiments: FirebaseExperimentDescription[]): Promise<void>;
|
|
9
|
+
private createExperimentInfoMap;
|
|
10
|
+
private addActiveExperiments;
|
|
11
|
+
private removeInactiveExperiments;
|
|
12
|
+
private addExperimentToAnalytics;
|
|
13
|
+
}
|
package/dist/esm/src/errors.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ export declare const enum ErrorCode {
|
|
|
35
35
|
CONFIG_UPDATE_STREAM_ERROR = "stream-error",
|
|
36
36
|
CONFIG_UPDATE_UNAVAILABLE = "realtime-unavailable",
|
|
37
37
|
CONFIG_UPDATE_MESSAGE_INVALID = "update-message-invalid",
|
|
38
|
-
CONFIG_UPDATE_NOT_FETCHED = "update-not-fetched"
|
|
38
|
+
CONFIG_UPDATE_NOT_FETCHED = "update-not-fetched",
|
|
39
|
+
ANALYTICS_UNAVAILABLE = "analytics-unavailable"
|
|
39
40
|
}
|
|
40
41
|
interface ErrorParams {
|
|
41
42
|
[ErrorCode.STORAGE_OPEN]: {
|
|
@@ -77,6 +78,9 @@ interface ErrorParams {
|
|
|
77
78
|
[ErrorCode.CONFIG_UPDATE_NOT_FETCHED]: {
|
|
78
79
|
originalErrorMessage: string;
|
|
79
80
|
};
|
|
81
|
+
[ErrorCode.ANALYTICS_UNAVAILABLE]: {
|
|
82
|
+
originalErrorMessage: string;
|
|
83
|
+
};
|
|
80
84
|
}
|
|
81
85
|
export declare const ERROR_FACTORY: ErrorFactory<ErrorCode, ErrorParams>;
|
|
82
86
|
export declare function hasErrorCode(e: Error, errorCode: ErrorCode): boolean;
|
|
@@ -54,6 +54,19 @@ export interface RemoteConfig {
|
|
|
54
54
|
export interface FirebaseRemoteConfigObject {
|
|
55
55
|
[key: string]: string;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Defines experiment and variant attached to a config parameter.
|
|
59
|
+
*
|
|
60
|
+
* @public
|
|
61
|
+
*/
|
|
62
|
+
export interface FirebaseExperimentDescription {
|
|
63
|
+
experimentId: string;
|
|
64
|
+
variantId: string;
|
|
65
|
+
experimentStartTime: string;
|
|
66
|
+
triggerTimeoutMillis: string;
|
|
67
|
+
timeToLiveMillis: string;
|
|
68
|
+
affectedParameterKeys?: string[];
|
|
69
|
+
}
|
|
57
70
|
/**
|
|
58
71
|
* Defines a successful response (200 or 304).
|
|
59
72
|
*
|
|
@@ -90,6 +103,12 @@ export interface FetchResponse {
|
|
|
90
103
|
* The version number of the config template fetched from the server.
|
|
91
104
|
*/
|
|
92
105
|
templateVersion?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Metadata for A/B testing and Remote Config Rollout experiments.
|
|
108
|
+
*
|
|
109
|
+
* @remarks Only defined for 200 responses.
|
|
110
|
+
*/
|
|
111
|
+
experiments?: FirebaseExperimentDescription[];
|
|
93
112
|
}
|
|
94
113
|
/**
|
|
95
114
|
* Options for Remote Config initialization.
|
|
@@ -20,6 +20,8 @@ import { StorageCache } from './storage/storage_cache';
|
|
|
20
20
|
import { RemoteConfigFetchClient } from './client/remote_config_fetch_client';
|
|
21
21
|
import { Storage } from './storage/storage';
|
|
22
22
|
import { Logger } from '@firebase/logger';
|
|
23
|
+
import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types';
|
|
24
|
+
import { Provider } from '@firebase/component';
|
|
23
25
|
import { RealtimeHandler } from './client/realtime_handler';
|
|
24
26
|
/**
|
|
25
27
|
* Encapsulates business logic mapping network and storage dependencies to the public SDK API.
|
|
@@ -48,6 +50,10 @@ export declare class RemoteConfig implements RemoteConfigType {
|
|
|
48
50
|
* @internal
|
|
49
51
|
*/
|
|
50
52
|
readonly _realtimeHandler: RealtimeHandler;
|
|
53
|
+
/**
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
readonly _analyticsProvider: Provider<FirebaseAnalyticsInternalName>;
|
|
51
57
|
/**
|
|
52
58
|
* Tracks completion of initialization promise.
|
|
53
59
|
* @internal
|
|
@@ -84,5 +90,9 @@ export declare class RemoteConfig implements RemoteConfigType {
|
|
|
84
90
|
/**
|
|
85
91
|
* @internal
|
|
86
92
|
*/
|
|
87
|
-
_realtimeHandler: RealtimeHandler
|
|
93
|
+
_realtimeHandler: RealtimeHandler,
|
|
94
|
+
/**
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
_analyticsProvider: Provider<FirebaseAnalyticsInternalName>);
|
|
88
98
|
}
|
|
@@ -43,7 +43,7 @@ export interface RealtimeBackoffMetadata {
|
|
|
43
43
|
*
|
|
44
44
|
* <p>This seems like a small price to avoid potentially subtle bugs caused by a typo.
|
|
45
45
|
*/
|
|
46
|
-
type ProjectNamespaceKeyFieldValue = 'active_config' | 'active_config_etag' | 'last_fetch_status' | 'last_successful_fetch_timestamp_millis' | 'last_successful_fetch_response' | 'settings' | 'throttle_metadata' | 'custom_signals' | 'realtime_backoff_metadata' | 'last_known_template_version';
|
|
46
|
+
type ProjectNamespaceKeyFieldValue = 'active_config' | 'active_config_etag' | 'active_experiments' | 'last_fetch_status' | 'last_successful_fetch_timestamp_millis' | 'last_successful_fetch_response' | 'settings' | 'throttle_metadata' | 'custom_signals' | 'realtime_backoff_metadata' | 'last_known_template_version';
|
|
47
47
|
export declare function openDatabase(): Promise<IDBDatabase>;
|
|
48
48
|
/**
|
|
49
49
|
* Abstracts data persistence.
|
|
@@ -59,6 +59,8 @@ export declare abstract class Storage {
|
|
|
59
59
|
setActiveConfig(config: FirebaseRemoteConfigObject): Promise<void>;
|
|
60
60
|
getActiveConfigEtag(): Promise<string | undefined>;
|
|
61
61
|
setActiveConfigEtag(etag: string): Promise<void>;
|
|
62
|
+
getActiveExperiments(): Promise<Set<string> | undefined>;
|
|
63
|
+
setActiveExperiments(experiments: Set<string>): Promise<void>;
|
|
62
64
|
getThrottleMetadata(): Promise<ThrottleMetadata | undefined>;
|
|
63
65
|
setThrottleMetadata(metadata: ThrottleMetadata): Promise<void>;
|
|
64
66
|
deleteThrottleMetadata(): Promise<void>;
|
package/dist/index.cjs.js
CHANGED
|
@@ -9,7 +9,7 @@ var logger = require('@firebase/logger');
|
|
|
9
9
|
require('@firebase/installations');
|
|
10
10
|
|
|
11
11
|
const name = "@firebase/remote-config";
|
|
12
|
-
const version = "0.
|
|
12
|
+
const version = "0.8.0-20260114160934";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @license
|
|
@@ -109,7 +109,8 @@ const ERROR_DESCRIPTION_MAP = {
|
|
|
109
109
|
["stream-error" /* ErrorCode.CONFIG_UPDATE_STREAM_ERROR */]: 'The stream was not able to connect to the backend: {$originalErrorMessage}.',
|
|
110
110
|
["realtime-unavailable" /* ErrorCode.CONFIG_UPDATE_UNAVAILABLE */]: 'The Realtime service is unavailable: {$originalErrorMessage}',
|
|
111
111
|
["update-message-invalid" /* ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID */]: 'The stream invalidation message was unparsable: {$originalErrorMessage}',
|
|
112
|
-
["update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */]: 'Unable to fetch the latest config: {$originalErrorMessage}'
|
|
112
|
+
["update-not-fetched" /* ErrorCode.CONFIG_UPDATE_NOT_FETCHED */]: 'Unable to fetch the latest config: {$originalErrorMessage}',
|
|
113
|
+
["analytics-unavailable" /* ErrorCode.ANALYTICS_UNAVAILABLE */]: 'Connection to Firebase Analytics failed: {$originalErrorMessage}'
|
|
113
114
|
};
|
|
114
115
|
const ERROR_FACTORY = new util.ErrorFactory('remoteconfig' /* service */, 'Remote Config' /* service name */, ERROR_DESCRIPTION_MAP);
|
|
115
116
|
// Note how this is like typeof/instanceof, but for ErrorCode.
|
|
@@ -166,6 +167,64 @@ class Value {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
class Experiment {
|
|
171
|
+
constructor(rc) {
|
|
172
|
+
this.storage = rc._storage;
|
|
173
|
+
this.logger = rc._logger;
|
|
174
|
+
this.analyticsProvider = rc._analyticsProvider;
|
|
175
|
+
}
|
|
176
|
+
async updateActiveExperiments(latestExperiments) {
|
|
177
|
+
const currentActiveExperiments = (await this.storage.getActiveExperiments()) || new Set();
|
|
178
|
+
const experimentInfoMap = this.createExperimentInfoMap(latestExperiments);
|
|
179
|
+
this.addActiveExperiments(experimentInfoMap);
|
|
180
|
+
this.removeInactiveExperiments(currentActiveExperiments, experimentInfoMap);
|
|
181
|
+
return this.storage.setActiveExperiments(new Set(experimentInfoMap.keys()));
|
|
182
|
+
}
|
|
183
|
+
createExperimentInfoMap(latestExperiments) {
|
|
184
|
+
const experimentInfoMap = new Map();
|
|
185
|
+
for (const experiment of latestExperiments) {
|
|
186
|
+
experimentInfoMap.set(experiment.experimentId, experiment);
|
|
187
|
+
}
|
|
188
|
+
return experimentInfoMap;
|
|
189
|
+
}
|
|
190
|
+
addActiveExperiments(experimentInfoMap) {
|
|
191
|
+
const customProperty = {};
|
|
192
|
+
for (const [experimentId, experimentInfo] of experimentInfoMap.entries()) {
|
|
193
|
+
customProperty[`firebase${experimentId}`] = experimentInfo.variantId;
|
|
194
|
+
}
|
|
195
|
+
this.addExperimentToAnalytics(customProperty);
|
|
196
|
+
}
|
|
197
|
+
removeInactiveExperiments(currentActiveExperiments, experimentInfoMap) {
|
|
198
|
+
const customProperty = {};
|
|
199
|
+
for (const experimentId of currentActiveExperiments) {
|
|
200
|
+
if (!experimentInfoMap.has(experimentId)) {
|
|
201
|
+
customProperty[`firebase${experimentId}`] = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
this.addExperimentToAnalytics(customProperty);
|
|
205
|
+
}
|
|
206
|
+
addExperimentToAnalytics(customProperty) {
|
|
207
|
+
if (Object.keys(customProperty).length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const analytics = this.analyticsProvider.getImmediate({ optional: true });
|
|
212
|
+
if (analytics) {
|
|
213
|
+
analytics.setUserProperties(customProperty);
|
|
214
|
+
analytics.logEvent(`set_firebase_experiment_state`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
this.logger.warn(`Analytics import failed. Verify if you have imported Firebase Analytics in your app code.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
throw ERROR_FACTORY.create("analytics-unavailable" /* ErrorCode.ANALYTICS_UNAVAILABLE */, {
|
|
222
|
+
originalErrorMessage: error?.message
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
169
228
|
/**
|
|
170
229
|
* @license
|
|
171
230
|
* Copyright 2020 Google LLC
|
|
@@ -243,10 +302,15 @@ async function activate(remoteConfig) {
|
|
|
243
302
|
// config.
|
|
244
303
|
return false;
|
|
245
304
|
}
|
|
305
|
+
const experiment = new Experiment(rc);
|
|
306
|
+
const updateActiveExperiments = lastSuccessfulFetchResponse.experiments
|
|
307
|
+
? experiment.updateActiveExperiments(lastSuccessfulFetchResponse.experiments)
|
|
308
|
+
: Promise.resolve();
|
|
246
309
|
await Promise.all([
|
|
247
310
|
rc._storageCache.setActiveConfig(lastSuccessfulFetchResponse.config),
|
|
248
311
|
rc._storage.setActiveConfigEtag(lastSuccessfulFetchResponse.eTag),
|
|
249
|
-
rc._storage.setActiveConfigTemplateVersion(lastSuccessfulFetchResponse.templateVersion)
|
|
312
|
+
rc._storage.setActiveConfigTemplateVersion(lastSuccessfulFetchResponse.templateVersion),
|
|
313
|
+
updateActiveExperiments
|
|
250
314
|
]);
|
|
251
315
|
return true;
|
|
252
316
|
}
|
|
@@ -701,6 +765,7 @@ class RestClient {
|
|
|
701
765
|
let config;
|
|
702
766
|
let state;
|
|
703
767
|
let templateVersion;
|
|
768
|
+
let experiments;
|
|
704
769
|
// JSON parsing throws SyntaxError if the response body isn't a JSON string.
|
|
705
770
|
// Requesting application/json and checking for a 200 ensures there's JSON data.
|
|
706
771
|
if (response.status === 200) {
|
|
@@ -716,6 +781,7 @@ class RestClient {
|
|
|
716
781
|
config = responseBody['entries'];
|
|
717
782
|
state = responseBody['state'];
|
|
718
783
|
templateVersion = responseBody['templateVersion'];
|
|
784
|
+
experiments = responseBody['experimentDescriptions'];
|
|
719
785
|
}
|
|
720
786
|
// Normalizes based on legacy state.
|
|
721
787
|
if (state === 'INSTANCE_STATE_UNSPECIFIED') {
|
|
@@ -727,6 +793,7 @@ class RestClient {
|
|
|
727
793
|
else if (state === 'NO_TEMPLATE' || state === 'EMPTY_CONFIG') {
|
|
728
794
|
// These cases can be fixed remotely, so normalize to safe value.
|
|
729
795
|
config = {};
|
|
796
|
+
experiments = [];
|
|
730
797
|
}
|
|
731
798
|
// Normalize to exception-based control flow for non-success cases.
|
|
732
799
|
// Encapsulates HTTP specifics in this class as much as possible. Status is still the best for
|
|
@@ -737,7 +804,7 @@ class RestClient {
|
|
|
737
804
|
httpStatus: status
|
|
738
805
|
});
|
|
739
806
|
}
|
|
740
|
-
return { status, eTag: responseEtag, config, templateVersion };
|
|
807
|
+
return { status, eTag: responseEtag, config, templateVersion, experiments };
|
|
741
808
|
}
|
|
742
809
|
}
|
|
743
810
|
|
|
@@ -903,13 +970,18 @@ class RemoteConfig {
|
|
|
903
970
|
/**
|
|
904
971
|
* @internal
|
|
905
972
|
*/
|
|
906
|
-
_realtimeHandler
|
|
973
|
+
_realtimeHandler,
|
|
974
|
+
/**
|
|
975
|
+
* @internal
|
|
976
|
+
*/
|
|
977
|
+
_analyticsProvider) {
|
|
907
978
|
this.app = app;
|
|
908
979
|
this._client = _client;
|
|
909
980
|
this._storageCache = _storageCache;
|
|
910
981
|
this._storage = _storage;
|
|
911
982
|
this._logger = _logger;
|
|
912
983
|
this._realtimeHandler = _realtimeHandler;
|
|
984
|
+
this._analyticsProvider = _analyticsProvider;
|
|
913
985
|
/**
|
|
914
986
|
* Tracks completion of initialization promise.
|
|
915
987
|
* @internal
|
|
@@ -1030,6 +1102,12 @@ class Storage {
|
|
|
1030
1102
|
setActiveConfigEtag(etag) {
|
|
1031
1103
|
return this.set('active_config_etag', etag);
|
|
1032
1104
|
}
|
|
1105
|
+
getActiveExperiments() {
|
|
1106
|
+
return this.get('active_experiments');
|
|
1107
|
+
}
|
|
1108
|
+
setActiveExperiments(experiments) {
|
|
1109
|
+
return this.set('active_experiments', experiments);
|
|
1110
|
+
}
|
|
1033
1111
|
getThrottleMetadata() {
|
|
1034
1112
|
return this.get('throttle_metadata');
|
|
1035
1113
|
}
|
|
@@ -2020,6 +2098,7 @@ function registerRemoteConfig() {
|
|
|
2020
2098
|
const installations = container
|
|
2021
2099
|
.getProvider('installations-internal')
|
|
2022
2100
|
.getImmediate();
|
|
2101
|
+
const analyticsProvider = container.getProvider('analytics-internal');
|
|
2023
2102
|
// Normalizes optional inputs.
|
|
2024
2103
|
const { projectId, apiKey, appId } = app$1.options;
|
|
2025
2104
|
if (!projectId) {
|
|
@@ -2046,7 +2125,7 @@ function registerRemoteConfig() {
|
|
|
2046
2125
|
const retryingClient = new RetryingClient(restClient, storage);
|
|
2047
2126
|
const cachingClient = new CachingClient(retryingClient, storage, storageCache, logger$1);
|
|
2048
2127
|
const realtimeHandler = new RealtimeHandler(installations, storage, app.SDK_VERSION, namespace, projectId, apiKey, appId, logger$1, storageCache, cachingClient);
|
|
2049
|
-
const remoteConfigInstance = new RemoteConfig(app$1, cachingClient, storageCache, storage, logger$1, realtimeHandler);
|
|
2128
|
+
const remoteConfigInstance = new RemoteConfig(app$1, cachingClient, storageCache, storage, logger$1, realtimeHandler, analyticsProvider);
|
|
2050
2129
|
// Starts warming cache.
|
|
2051
2130
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
2052
2131
|
ensureInitialized(remoteConfigInstance);
|