@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.
@@ -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
+ }
@@ -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.7.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);