-
Notifications
You must be signed in to change notification settings - Fork 985
feat(rc): Web support for ABT & Rollouts #9293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
8ee4b72
17126e9
a19c1a7
fa63f97
b5f96dd
851a47f
dc45a19
34c5750
4aa9bd5
2ef04a7
84ba36e
9a082d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@firebase/remote-config': minor | ||
| 'firebase': minor | ||
| --- | ||
|
|
||
| Web support for ABT and Rollouts | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| Project: /docs/reference/js/_project.yaml | ||
| Book: /docs/reference/_book.yaml | ||
| page_type: reference | ||
|
|
||
| {% comment %} | ||
| DO NOT EDIT THIS FILE! | ||
| This is generated by the JS SDK team, and any local changes will be | ||
| overwritten. Changes should be made in the source code at | ||
| https://github.com/firebase/firebase-js-sdk | ||
| {% endcomment %} | ||
|
|
||
| # FirebaseExperimentDescription interface | ||
| Defines experiment and variant attached to a config parameter. | ||
|
|
||
| <b>Signature:</b> | ||
|
|
||
| ```typescript | ||
| export interface FirebaseExperimentDescription | ||
| ``` | ||
|
|
||
| ## Properties | ||
|
|
||
| | Property | Type | Description | | ||
| | --- | --- | --- | | ||
| | [affectedParameterKeys](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptionaffectedparameterkeys) | string\[\] | | | ||
| | [experimentId](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptionexperimentid) | string | | | ||
| | [experimentStartTime](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptionexperimentstarttime) | string | | | ||
| | [timeToLiveMillis](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptiontimetolivemillis) | string | | | ||
| | [triggerTimeoutMillis](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptiontriggertimeoutmillis) | string | | | ||
| | [variantId](./remote-config.firebaseexperimentdescription.md#firebaseexperimentdescriptionvariantid) | string | | | ||
|
|
||
| ## FirebaseExperimentDescription.affectedParameterKeys | ||
|
|
||
| <b>Signature:</b> | ||
|
|
||
| ```typescript | ||
| affectedParameterKeys?: string[]; | ||
| ``` | ||
|
|
||
| ## FirebaseExperimentDescription.experimentId | ||
|
|
||
| <b>Signature:</b> | ||
|
|
||
| ```typescript | ||
| experimentId: string; | ||
| ``` | ||
|
|
||
| ## FirebaseExperimentDescription.experimentStartTime | ||
|
|
||
| <b>Signature:</b> | ||
|
|
||
| ```typescript | ||
| experimentStartTime: string; | ||
| ``` | ||
|
|
||
| ## FirebaseExperimentDescription.timeToLiveMillis | ||
|
|
||
| <b>Signature:</b> | ||
|
|
||
| ```typescript | ||
| timeToLiveMillis: string; | ||
| ``` | ||
|
|
||
| ## FirebaseExperimentDescription.triggerTimeoutMillis | ||
|
|
||
| <b>Signature:</b> | ||
|
|
||
| ```typescript | ||
| triggerTimeoutMillis: string; | ||
| ``` | ||
|
|
||
| ## FirebaseExperimentDescription.variantId | ||
|
|
||
| <b>Signature:</b> | ||
|
|
||
| ```typescript | ||
| variantId: string; | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| import { Storage } from '../storage/storage'; | ||
| import { FirebaseExperimentDescription } from '../public_types'; | ||
| import { Provider } from '@firebase/component'; | ||
| import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types'; | ||
| import { Logger } from '@firebase/logger'; | ||
| import { RemoteConfig } from '../remote_config'; | ||
| import { ERROR_FACTORY, ErrorCode } from '../errors'; | ||
|
|
||
| export class Experiment { | ||
| private storage: Storage; | ||
| private logger: Logger; | ||
| private analyticsProvider: Provider<FirebaseAnalyticsInternalName>; | ||
|
|
||
| constructor(rc: RemoteConfig) { | ||
| this.storage = rc._storage; | ||
| this.logger = rc._logger; | ||
| this.analyticsProvider = rc._analyticsProvider; | ||
| } | ||
|
|
||
| async updateActiveExperiments( | ||
| latestExperiments: FirebaseExperimentDescription[] | ||
| ): Promise<void> { | ||
| const currentActiveExperiments = | ||
| (await this.storage.getActiveExperiments()) || new Set<string>(); | ||
| const experimentInfoMap = this.createExperimentInfoMap(latestExperiments); | ||
| this.addActiveExperiments(experimentInfoMap); | ||
| this.removeInactiveExperiments(currentActiveExperiments, experimentInfoMap); | ||
| return this.storage.setActiveExperiments(new Set(experimentInfoMap.keys())); | ||
| } | ||
|
|
||
| private createExperimentInfoMap( | ||
| latestExperiments: FirebaseExperimentDescription[] | ||
| ): Map<string, FirebaseExperimentDescription> { | ||
| const experimentInfoMap = new Map<string, FirebaseExperimentDescription>(); | ||
| for (const experiment of latestExperiments) { | ||
| experimentInfoMap.set(experiment.experimentId, experiment); | ||
| } | ||
| return experimentInfoMap; | ||
| } | ||
|
|
||
| private addActiveExperiments( | ||
| experimentInfoMap: Map<string, FirebaseExperimentDescription> | ||
| ): void { | ||
| const customProperty: Record<string, string | null> = {}; | ||
| for (const [experimentId, experimentInfo] of experimentInfoMap.entries()) { | ||
| customProperty[`firebase${experimentId}`] = experimentInfo.variantId; | ||
| } | ||
| this.addExperimentToAnalytics(customProperty); | ||
| } | ||
|
|
||
| private removeInactiveExperiments( | ||
| currentActiveExperiments: Set<string>, | ||
| experimentInfoMap: Map<string, FirebaseExperimentDescription> | ||
| ): void { | ||
| const customProperty: Record<string, string | null> = {}; | ||
| for (const experimentId of currentActiveExperiments) { | ||
| if (!experimentInfoMap.has(experimentId)) { | ||
| customProperty[`firebase${experimentId}`] = null; | ||
| } | ||
| } | ||
| this.addExperimentToAnalytics(customProperty); | ||
| } | ||
|
|
||
| private addExperimentToAnalytics( | ||
| customProperty: Record<string, string | null> | ||
| ): void { | ||
| if (Object.keys(customProperty).length === 0) { | ||
| return; | ||
| } | ||
| try { | ||
| const analytics = this.analyticsProvider.getImmediate({ optional: true }); | ||
| if (analytics) { | ||
| analytics.setUserProperties(customProperty); | ||
| analytics.logEvent(`set_firebase_experiment_state`); | ||
| } else { | ||
| this.logger.warn(`Analytics import failed`); | ||
|
||
| } | ||
| } catch (error) { | ||
| throw ERROR_FACTORY.create(ErrorCode.ANALYTICS_UNAVAILABLE, { | ||
| originalErrorMessage: (error as Error)?.message | ||
| }); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.