Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2ec305d
feat: Process experiment metadata in RC fetch response
May 30, 2025
5eb6b8d
feat: Add ABT support for remote config
Jun 6, 2025
08c8863
feat: Integrate firebase internal analytics with ABT
Jun 6, 2025
4af3eb9
Merge branch 'web-experiment' into web-exp-fetch
Jul 19, 2025
b6f2ac9
Merge branch 'web-exp-fetch' into web-exp-abt
Jul 19, 2025
55db6e0
Merge branch 'web-exp-abt' into web-exp-ga
Jul 19, 2025
24848c4
[Fix] Storage cache is not updating when there are no experiments in …
Jul 19, 2025
455b8e3
[Fix] Update experiments after checking fetch response
Jul 19, 2025
e2024d7
Merge branch 'web-exp-abt' into web-exp-ga
Jul 19, 2025
bec6e56
feat: Process experiment metadata in RC fetch response
May 30, 2025
ee703b9
[Fix] Storage cache is not updating when there are no experiments in …
Jul 19, 2025
7c67f85
Add result of running yarn docgen:all
Sep 25, 2025
06398f6
feat: Process experiment metadata in RC fetch response
May 30, 2025
fd049e2
feat: Add ABT support for remote config
Jun 6, 2025
638cc2c
[Fix] Storage cache is not updating when there are no experiments in …
Jul 19, 2025
900eff5
Merge conflict fix
Sep 25, 2025
432ac24
Yarn format fix
Sep 25, 2025
ccc71e1
Fix merge conflicts
Sep 25, 2025
5836eaf
merge web-exp-abt
Sep 25, 2025
6be23df
Integrate ABT with Firebase analytics to add experiment as UP
Sep 25, 2025
66b104b
Fix yarn format errors
Sep 25, 2025
b289636
Address review comments
Sep 25, 2025
d3e0838
Fix yarn format failures
Sep 25, 2025
aa7751e
yarn docgen changes added
Sep 25, 2025
19c0fd6
Export firebaseExperimentDescription
Sep 26, 2025
b3f5fa1
Merge branch 'web-exp-fetch' into web-exp-abt
Sep 26, 2025
27cb4b2
Merge branch 'web-exp-abt' into web-exp-ga
Sep 26, 2025
d09a338
Merge branch 'web-experiment' into web-exp-ga
Sep 29, 2025
4c19690
Address review comments
Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions packages/remote-config/src/abt/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
*/
import { Storage } from '../storage/storage';
import { FirebaseExperimentDescription } from '../public_types';
import { Provider } from '@firebase/component';
import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types';

export class Experiment {
constructor(private readonly storage: Storage) {}
constructor(
private readonly storage: Storage,
private readonly analyticsProvider: Provider<FirebaseAnalyticsInternalName>
) {}

async updateActiveExperiments(
latestExperiments: FirebaseExperimentDescription[]
Expand All @@ -45,32 +50,37 @@ export class Experiment {
currentActiveExperiments: Set<string>,
experimentInfoMap: Map<string, FirebaseExperimentDescription>
): void {
const customProperty: Record<string, string | null> = {};
for (const [experimentId, experimentInfo] of experimentInfoMap.entries()) {
if (!currentActiveExperiments.has(experimentId)) {
this.addExperimentToAnalytics(experimentId, experimentInfo.variantId);
customProperty[experimentId] = experimentInfo.variantId;
}
}
void 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)) {
this.removeExperimentFromAnalytics(experimentId);
customProperty[experimentId] = null;
}
}
void this.addExperimentToAnalytics(customProperty);
}

private addExperimentToAnalytics(
_experimentId: string,
_variantId: string
): void {
// TODO
}

private removeExperimentFromAnalytics(_experimentId: string): void {
// TODO
private async addExperimentToAnalytics(
customProperty: Record<string, string | null>
): Promise<void> {
try {
const analytics = await this.analyticsProvider.get();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think we talked about this before, but being able to use this requires the developer to have called import { anything } from 'firebase/analytics'. If they don't, this line will hang. I'm actually a little concerned about the messaging implementation (the pattern you took this from) running into this. I think the best way to handle this might be to use getImmediate({ optional: true }) instead. Example: https://github.com/firebase/firebase-js-sdk/blob/main/packages/storage/src/service.ts#L286

This returns undefined if the developer did not import 'firebase/analytics'. You could use that to log a warning message, or continue if it was found. You'd also want to make it clear in documentation that users that use this feature must also import and initialize analytics.

analytics.setUserProperties({ properties: customProperty });
} catch (error) {
console.error(`Failed to add experiment to analytics :`, error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use logger instead of console for messages? This enables setLogLevel() to control the messages. https://github.com/firebase/firebase-js-sdk/blob/main/packages/remote-config/src/api.ts#L338

return;
}
}
}
2 changes: 1 addition & 1 deletion packages/remote-config/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export async function activate(remoteConfig: RemoteConfig): Promise<boolean> {
// config.
return false;
}
const experiment = new Experiment(rc._storage);
const experiment = new Experiment(rc._storage, rc._analyticsProvider);
const updateActiveExperiments = lastSuccessfulFetchResponse.experiments
? experiment.updateActiveExperiments(
lastSuccessfulFetchResponse.experiments
Expand Down
4 changes: 3 additions & 1 deletion packages/remote-config/src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function registerRemoteConfig(): void {
const installations = container
.getProvider('installations-internal')
.getImmediate();
const analyticsProvider = container.getProvider('analytics-internal');

// Normalizes optional inputs.
const { projectId, apiKey, appId } = app.options;
Expand Down Expand Up @@ -127,7 +128,8 @@ export function registerRemoteConfig(): void {
storageCache,
storage,
logger,
realtimeHandler
realtimeHandler,
analyticsProvider
);

// Starts warming cache.
Expand Down
8 changes: 7 additions & 1 deletion packages/remote-config/src/remote_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { StorageCache } from './storage/storage_cache';
import { RemoteConfigFetchClient } from './client/remote_config_fetch_client';
import { Storage } from './storage/storage';
import { Logger } from '@firebase/logger';
import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types';
import { Provider } from '@firebase/component';
import { RealtimeHandler } from './client/realtime_handler';

const DEFAULT_FETCH_TIMEOUT_MILLIS = 60 * 1000; // One minute
Expand Down Expand Up @@ -88,6 +90,10 @@ export class RemoteConfig implements RemoteConfigType {
/**
* @internal
*/
readonly _realtimeHandler: RealtimeHandler
readonly _realtimeHandler: RealtimeHandler,
/**
* @internal
*/
readonly _analyticsProvider: Provider<FirebaseAnalyticsInternalName>
) {}
}
20 changes: 18 additions & 2 deletions packages/remote-config/test/abt/experiment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,26 @@ import * as sinon from 'sinon';
import { Experiment } from '../../src/abt/experiment';
import { FirebaseExperimentDescription } from '../../src/public_types';
import { Storage } from '../../src/storage/storage';
import { Provider } from '@firebase/component';
import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types';

describe('Experiment', () => {
const storage = {} as Storage;
const experiment = new Experiment(storage);
const analyticsProvider = {} as Provider<FirebaseAnalyticsInternalName>;
const experiment = new Experiment(storage, analyticsProvider);

describe('updateActiveExperiments', () => {
beforeEach(() => {
storage.getActiveExperiments = sinon.stub();
storage.setActiveExperiments = sinon.stub();
analyticsProvider.get = sinon.stub().returns(
Promise.resolve({
setUserProperties: sinon.stub()
})
);
});

it('adds mew experiments to storage', async () => {
it('adds new experiments to storage', async () => {
const latestExperiments: FirebaseExperimentDescription[] = [
{
experimentId: '_exp_3',
Expand Down Expand Up @@ -59,12 +67,16 @@ describe('Experiment', () => {
storage.getActiveExperiments = sinon
.stub()
.returns(new Set(['_exp_1', '_exp_2']));
const analytics = await analyticsProvider.get();

await experiment.updateActiveExperiments(latestExperiments);

expect(storage.setActiveExperiments).to.have.been.calledWith(
expectedStoredExperiments
);
expect(analytics.setUserProperties).to.have.been.calledWith({
properties: { '_exp_3': '1' }
});
});

it('removes missing experiment in fetch response from storage', async () => {
Expand All @@ -81,12 +93,16 @@ describe('Experiment', () => {
storage.getActiveExperiments = sinon
.stub()
.returns(new Set(['_exp_1', '_exp_2']));
const analytics = await analyticsProvider.get();

await experiment.updateActiveExperiments(latestExperiments);

expect(storage.setActiveExperiments).to.have.been.calledWith(
expectedStoredExperiments
);
expect(analytics.setUserProperties).to.have.been.calledWith({
properties: { '_exp_2': null }
});
});
});
});
13 changes: 11 additions & 2 deletions packages/remote-config/test/remote_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ import {
import * as api from '../src/api';
import { fetchAndActivate } from '../src';
import { restore } from 'sinon';
import { RealtimeHandler } from '../src/client/realtime_handler';
import { Experiment } from '../src/abt/experiment';
import { Provider } from '@firebase/component';
import { FirebaseAnalyticsInternalName } from '@firebase/analytics-interop-types';
import { RealtimeHandler } from '../src/client/realtime_handler';

describe('RemoteConfig', () => {
const ACTIVE_CONFIG = {
Expand All @@ -71,6 +73,7 @@ describe('RemoteConfig', () => {
let logger: Logger;
let realtimeHandler: RealtimeHandler;
let rc: RemoteConfigType;
let analyticsProvider: Provider<FirebaseAnalyticsInternalName>;

let getActiveConfigStub: sinon.SinonStub;
let loggerDebugSpy: sinon.SinonSpy;
Expand All @@ -82,6 +85,7 @@ describe('RemoteConfig', () => {
client = {} as RemoteConfigFetchClient;
storageCache = {} as StorageCache;
storage = {} as Storage;
analyticsProvider = {} as Provider<FirebaseAnalyticsInternalName>;
realtimeHandler = {} as RealtimeHandler;
logger = new Logger('package-name');
getActiveConfigStub = sinon.stub().returns(undefined);
Expand All @@ -94,7 +98,8 @@ describe('RemoteConfig', () => {
storageCache,
storage,
logger,
realtimeHandler
realtimeHandler,
analyticsProvider
);
});

Expand Down Expand Up @@ -439,6 +444,10 @@ describe('RemoteConfig', () => {
sandbox.restore();
});

afterEach(() => {
sandbox.restore();
});

it('does not activate if last successful fetch response is undefined', async () => {
getLastSuccessfulFetchResponseStub.returns(Promise.resolve());
getActiveConfigEtagStub.returns(Promise.resolve(ETAG));
Expand Down
Loading