diff --git a/packages/remote-config/src/abt/experiment.ts b/packages/remote-config/src/abt/experiment.ts index 07873beb373..85b309fbbc9 100644 --- a/packages/remote-config/src/abt/experiment.ts +++ b/packages/remote-config/src/abt/experiment.ts @@ -16,9 +16,22 @@ */ 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 { - constructor(private readonly storage: Storage) {} + private storage: Storage; + private logger: Logger; + private analyticsProvider: Provider; + + constructor(rc: RemoteConfig) { + this.storage = rc._storage; + this.logger = rc._logger; + this.analyticsProvider = rc._analyticsProvider; + } async updateActiveExperiments( latestExperiments: FirebaseExperimentDescription[] @@ -45,32 +58,47 @@ export class Experiment { currentActiveExperiments: Set, experimentInfoMap: Map ): void { + const customProperty: Record = {}; for (const [experimentId, experimentInfo] of experimentInfoMap.entries()) { if (!currentActiveExperiments.has(experimentId)) { - this.addExperimentToAnalytics(experimentId, experimentInfo.variantId); + customProperty[experimentId] = experimentInfo.variantId; } } + this.addExperimentToAnalytics(customProperty); } private removeInactiveExperiments( currentActiveExperiments: Set, experimentInfoMap: Map ): void { + const customProperty: Record = {}; for (const experimentId of currentActiveExperiments) { if (!experimentInfoMap.has(experimentId)) { - this.removeExperimentFromAnalytics(experimentId); + customProperty[experimentId] = null; } } + this.addExperimentToAnalytics(customProperty); } private addExperimentToAnalytics( - _experimentId: string, - _variantId: string + customProperty: Record ): void { - // TODO - } - - private removeExperimentFromAnalytics(_experimentId: string): void { - // TODO + if (Object.keys(customProperty).length === 0) { + return; + } + try { + const analytics = this.analyticsProvider.getImmediate({ optional: true }); + if (analytics) { + analytics.setUserProperties({ properties: customProperty }); + } else { + // TODO: Update warning message + this.logger.warn(`Analytics is not imported correctly`); + } + } catch (error) { + // TODO: Update error message + throw ERROR_FACTORY.create(ErrorCode.ANALYTICS_UNAVAILABLE, { + originalErrorMessage: (error as Error)?.message + }); + } } } diff --git a/packages/remote-config/src/api.ts b/packages/remote-config/src/api.ts index 39db9bae9f8..5533bf9129c 100644 --- a/packages/remote-config/src/api.ts +++ b/packages/remote-config/src/api.ts @@ -111,7 +111,7 @@ export async function activate(remoteConfig: RemoteConfig): Promise { // config. return false; } - const experiment = new Experiment(rc._storage); + const experiment = new Experiment(rc); const updateActiveExperiments = lastSuccessfulFetchResponse.experiments ? experiment.updateActiveExperiments( lastSuccessfulFetchResponse.experiments diff --git a/packages/remote-config/src/errors.ts b/packages/remote-config/src/errors.ts index dea9f43e922..7311b64bca8 100644 --- a/packages/remote-config/src/errors.ts +++ b/packages/remote-config/src/errors.ts @@ -37,7 +37,8 @@ export const enum ErrorCode { CONFIG_UPDATE_STREAM_ERROR = 'stream-error', CONFIG_UPDATE_UNAVAILABLE = 'realtime-unavailable', CONFIG_UPDATE_MESSAGE_INVALID = 'update-message-invalid', - CONFIG_UPDATE_NOT_FETCHED = 'update-not-fetched' + CONFIG_UPDATE_NOT_FETCHED = 'update-not-fetched', + ANALYTICS_UNAVAILABLE = 'analytics-unavailable' } const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { @@ -84,7 +85,9 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { [ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID]: 'The stream invalidation message was unparsable: {$originalErrorMessage}', [ErrorCode.CONFIG_UPDATE_NOT_FETCHED]: - 'Unable to fetch the latest config: {$originalErrorMessage}' + 'Unable to fetch the latest config: {$originalErrorMessage}', + [ErrorCode.ANALYTICS_UNAVAILABLE]: + 'Connection to firebase analytics failed: {$originalErrorMessage}' }; // Note this is effectively a type system binding a code to params. This approach overlaps with the @@ -108,6 +111,7 @@ interface ErrorParams { [ErrorCode.CONFIG_UPDATE_UNAVAILABLE]: { originalErrorMessage: string }; [ErrorCode.CONFIG_UPDATE_MESSAGE_INVALID]: { originalErrorMessage: string }; [ErrorCode.CONFIG_UPDATE_NOT_FETCHED]: { originalErrorMessage: string }; + [ErrorCode.ANALYTICS_UNAVAILABLE]: { originalErrorMessage: string }; } export const ERROR_FACTORY = new ErrorFactory( diff --git a/packages/remote-config/src/register.ts b/packages/remote-config/src/register.ts index eade371ca89..0a34024622f 100644 --- a/packages/remote-config/src/register.ts +++ b/packages/remote-config/src/register.ts @@ -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; @@ -127,7 +128,8 @@ export function registerRemoteConfig(): void { storageCache, storage, logger, - realtimeHandler + realtimeHandler, + analyticsProvider ); // Starts warming cache. diff --git a/packages/remote-config/src/remote_config.ts b/packages/remote-config/src/remote_config.ts index bd32c938304..aa9502262fd 100644 --- a/packages/remote-config/src/remote_config.ts +++ b/packages/remote-config/src/remote_config.ts @@ -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 @@ -88,6 +90,10 @@ export class RemoteConfig implements RemoteConfigType { /** * @internal */ - readonly _realtimeHandler: RealtimeHandler + readonly _realtimeHandler: RealtimeHandler, + /** + * @internal + */ + readonly _analyticsProvider: Provider ) {} } diff --git a/packages/remote-config/test/abt/experiment.test.ts b/packages/remote-config/test/abt/experiment.test.ts index 4dce38c7c3e..8e0b0cb5f13 100644 --- a/packages/remote-config/test/abt/experiment.test.ts +++ b/packages/remote-config/test/abt/experiment.test.ts @@ -20,18 +20,32 @@ 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'; +import { Logger } from '@firebase/logger'; +import { RemoteConfig } from '../../src/remote_config'; describe('Experiment', () => { const storage = {} as Storage; - const experiment = new Experiment(storage); + const analyticsProvider = {} as Provider; + const logger = {} as Logger; + const rc = { + _storage: storage, + _analyticsProvider: analyticsProvider, + _logger: logger + } as RemoteConfig; + const experiment = new Experiment(rc); describe('updateActiveExperiments', () => { beforeEach(() => { storage.getActiveExperiments = sinon.stub(); storage.setActiveExperiments = sinon.stub(); + analyticsProvider.getImmediate = sinon.stub().returns({ + setUserProperties: sinon.stub() + }); }); - it('adds mew experiments to storage', async () => { + it('adds new experiments to storage', async () => { const latestExperiments: FirebaseExperimentDescription[] = [ { experimentId: '_exp_3', @@ -59,12 +73,16 @@ describe('Experiment', () => { storage.getActiveExperiments = sinon .stub() .returns(new Set(['_exp_1', '_exp_2'])); + const analytics = analyticsProvider.getImmediate(); 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 () => { @@ -81,12 +99,16 @@ describe('Experiment', () => { storage.getActiveExperiments = sinon .stub() .returns(new Set(['_exp_1', '_exp_2'])); + const analytics = analyticsProvider.getImmediate(); await experiment.updateActiveExperiments(latestExperiments); expect(storage.setActiveExperiments).to.have.been.calledWith( expectedStoredExperiments ); + expect(analytics.setUserProperties).to.have.been.calledWith({ + properties: { '_exp_2': null } + }); }); }); }); diff --git a/packages/remote-config/test/remote_config.test.ts b/packages/remote-config/test/remote_config.test.ts index 1b5edb23b4a..de70e13dfc2 100644 --- a/packages/remote-config/test/remote_config.test.ts +++ b/packages/remote-config/test/remote_config.test.ts @@ -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 = { @@ -71,6 +73,7 @@ describe('RemoteConfig', () => { let logger: Logger; let realtimeHandler: RealtimeHandler; let rc: RemoteConfigType; + let analyticsProvider: Provider; let getActiveConfigStub: sinon.SinonStub; let loggerDebugSpy: sinon.SinonSpy; @@ -82,6 +85,7 @@ describe('RemoteConfig', () => { client = {} as RemoteConfigFetchClient; storageCache = {} as StorageCache; storage = {} as Storage; + analyticsProvider = {} as Provider; realtimeHandler = {} as RealtimeHandler; logger = new Logger('package-name'); getActiveConfigStub = sinon.stub().returns(undefined); @@ -94,7 +98,8 @@ describe('RemoteConfig', () => { storageCache, storage, logger, - realtimeHandler + realtimeHandler, + analyticsProvider ); }); @@ -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));