Skip to content

Make initialize methods for each product idempotent #5272

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

Merged
merged 18 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/fast-buses-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/component': patch
---

Store instance initialization options on the Provider.
4 changes: 4 additions & 0 deletions common/api-review/app-check-exp.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export class CustomProvider implements AppCheckProvider {
getToken(): Promise<AppCheckTokenInternal>;
// @internal (undocumented)
initialize(app: FirebaseApp): void;
// @internal (undocumented)
isEqual(otherProvider: unknown): boolean;
}

// @public
Expand Down Expand Up @@ -79,6 +81,8 @@ export class ReCaptchaV3Provider implements AppCheckProvider {
getToken(): Promise<AppCheckTokenInternal>;
// @internal (undocumented)
initialize(app: FirebaseApp): void;
// @internal (undocumented)
isEqual(otherProvider: unknown): boolean;
}

// @public
Expand Down
97 changes: 97 additions & 0 deletions packages-exp/analytics-exp/src/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @license
* Copyright 2019 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 { expect } from 'chai';
import { SinonStub, stub } from 'sinon';
import '../testing/setup';
import { getFullApp } from '../testing/get-fake-firebase-services';
import { getAnalytics, initializeAnalytics } from './api';
import { FirebaseApp, deleteApp } from '@firebase/app-exp';
import { AnalyticsError } from './errors';
import * as init from './initialize-analytics';
const fakeAppParams = { appId: 'abcdefgh12345:23405', apiKey: 'AAbbCCdd12345' };

describe('FirebaseAnalytics API tests', () => {
let initStub: SinonStub = stub();
let app: FirebaseApp;

beforeEach(() => {
initStub = stub(init, '_initializeAnalytics').resolves(
'FAKE_MEASUREMENT_ID'
);
});

afterEach(async () => {
await initStub();
initStub.restore();
if (app) {
return deleteApp(app);
}
});

after(() => {
delete window['gtag'];
delete window['dataLayer'];
});

it('initializeAnalytics() with same (no) options returns same instance', () => {
app = getFullApp(fakeAppParams);
const analyticsInstance = initializeAnalytics(app);
const newInstance = initializeAnalytics(app);
expect(analyticsInstance).to.equal(newInstance);
});
it('initializeAnalytics() with same options returns same instance', () => {
app = getFullApp(fakeAppParams);
const analyticsInstance = initializeAnalytics(app, {
config: { 'send_page_view': false }
});
const newInstance = initializeAnalytics(app, {
config: { 'send_page_view': false }
});
expect(analyticsInstance).to.equal(newInstance);
});
it('initializeAnalytics() with different options throws', () => {
app = getFullApp(fakeAppParams);
initializeAnalytics(app, {
config: { 'send_page_view': false }
});
expect(() =>
initializeAnalytics(app, {
config: { 'send_page_view': true }
})
).to.throw(AnalyticsError.ALREADY_INITIALIZED);
});
it('initializeAnalytics() with different options (one undefined) throws', () => {
app = getFullApp(fakeAppParams);
initializeAnalytics(app);
expect(() =>
initializeAnalytics(app, {
config: { 'send_page_view': true }
})
).to.throw(AnalyticsError.ALREADY_INITIALIZED);
});
it('getAnalytics() returns same instance created by previous getAnalytics()', () => {
app = getFullApp(fakeAppParams);
const analyticsInstance = getAnalytics(app);
expect(getAnalytics(app)).to.equal(analyticsInstance);
});
it('getAnalytics() returns same instance created by initializeAnalytics()', () => {
app = getFullApp(fakeAppParams);
const analyticsInstance = initializeAnalytics(app);
expect(getAnalytics(app)).to.equal(analyticsInstance);
});
});
10 changes: 8 additions & 2 deletions packages-exp/analytics-exp/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import {
validateIndexedDBOpenable,
areCookiesEnabled,
isBrowserExtension,
getModularInstance
getModularInstance,
deepEqual
} from '@firebase/util';
import { ANALYTICS_TYPE } from './constants';
import {
Expand Down Expand Up @@ -97,7 +98,12 @@ export function initializeAnalytics(
ANALYTICS_TYPE
);
if (analyticsProvider.isInitialized()) {
throw ERROR_FACTORY.create(AnalyticsError.ALREADY_INITIALIZED);
const existingInstance = analyticsProvider.getImmediate();
if (deepEqual(options, analyticsProvider.getOptions())) {
return existingInstance;
} else {
throw ERROR_FACTORY.create(AnalyticsError.ALREADY_INITIALIZED);
}
}
const analyticsInstance = analyticsProvider.initialize({ options });
return analyticsInstance;
Expand Down
5 changes: 3 additions & 2 deletions packages-exp/analytics-exp/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ const ERRORS: ErrorMap<AnalyticsError> = {
' already exists. ' +
'Only one Firebase Analytics instance can be created for each appId.',
[AnalyticsError.ALREADY_INITIALIZED]:
'Firebase Analytics has already been initialized. ' +
'initializeAnalytics() must only be called once. getAnalytics() can be used ' +
'initializeAnalytics() cannot be called again with different options than those ' +
'it was initially called with. It can be called again with the same options to ' +
'return the existing instance, or getAnalytics() can be used ' +
'to get a reference to the already-intialized instance.',
[AnalyticsError.ALREADY_INITIALIZED_SETTINGS]:
'Firebase Analytics has already been initialized.' +
Expand Down
4 changes: 2 additions & 2 deletions packages-exp/analytics-exp/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { getOrCreateDataLayer, wrapOrCreateGtag } from './helpers';
import { AnalyticsError, ERROR_FACTORY } from './errors';
import { _FirebaseInstallationsInternal } from '@firebase/installations-exp';
import { areCookiesEnabled, isBrowserExtension } from '@firebase/util';
import { initializeAnalytics } from './initialize-analytics';
import { _initializeAnalytics } from './initialize-analytics';
import { logger } from './logger';
import { FirebaseApp, _FirebaseService } from '@firebase/app-exp';

Expand Down Expand Up @@ -221,7 +221,7 @@ export function factory(
}
// Async but non-blocking.
// This map reflects the completion state of all promises for each appId.
initializationPromisesMap[appId] = initializeAnalytics(
initializationPromisesMap[appId] = _initializeAnalytics(
app,
dynamicConfigPromisesList,
measurementIdToAppId,
Expand Down
10 changes: 2 additions & 8 deletions packages-exp/analytics-exp/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ let clock: sinon.SinonFakeTimers;
let fakeInstallations: _FirebaseInstallationsInternal;

// Fake indexedDB.open() request
let fakeRequest = {
const fakeRequest = {
onsuccess: () => {},
result: {
close: () => {}
Expand All @@ -67,13 +67,7 @@ function stubFetch(status: number, body: object): void {
// Stub indexedDB.open() because sinon's clock does not know
// how to wait for the real indexedDB callbacks to resolve.
function stubIdbOpen(): void {
(fakeRequest = {
onsuccess: () => {},
result: {
close: () => {}
}
}),
(idbOpenStub = stub(indexedDB, 'open').returns(fakeRequest as any));
idbOpenStub = stub(indexedDB, 'open').returns(fakeRequest as any);
}

describe('FirebaseAnalytics instance tests', () => {
Expand Down
12 changes: 6 additions & 6 deletions packages-exp/analytics-exp/src/initialize-analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import { expect } from 'chai';
import { SinonStub, stub } from 'sinon';
import '../testing/setup';
import { initializeAnalytics } from './initialize-analytics';
import { _initializeAnalytics } from './initialize-analytics';
import {
getFakeApp,
getFakeInstallations
Expand Down Expand Up @@ -65,7 +65,7 @@ describe('initializeAnalytics()', () => {
});
it('gets FID and measurement ID and calls gtag config with them', async () => {
stubFetch();
await initializeAnalytics(
await _initializeAnalytics(
app,
dynamicPromisesList,
measurementIdToAppId,
Expand All @@ -81,7 +81,7 @@ describe('initializeAnalytics()', () => {
});
it('calls gtag config with options if provided', async () => {
stubFetch();
await initializeAnalytics(
await _initializeAnalytics(
app,
dynamicPromisesList,
measurementIdToAppId,
Expand All @@ -99,7 +99,7 @@ describe('initializeAnalytics()', () => {
});
it('puts dynamic fetch promise into dynamic promises list', async () => {
stubFetch();
await initializeAnalytics(
await _initializeAnalytics(
app,
dynamicPromisesList,
measurementIdToAppId,
Expand All @@ -113,7 +113,7 @@ describe('initializeAnalytics()', () => {
});
it('puts dynamically fetched measurementId into lookup table', async () => {
stubFetch();
await initializeAnalytics(
await _initializeAnalytics(
app,
dynamicPromisesList,
measurementIdToAppId,
Expand All @@ -126,7 +126,7 @@ describe('initializeAnalytics()', () => {
it('warns on local/fetched measurement ID mismatch', async () => {
stubFetch();
const consoleStub = stub(console, 'warn');
await initializeAnalytics(
await _initializeAnalytics(
getFakeApp({ ...fakeAppParams, measurementId: 'old-measurement-id' }),
dynamicPromisesList,
measurementIdToAppId,
Expand Down
2 changes: 1 addition & 1 deletion packages-exp/analytics-exp/src/initialize-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async function validateIndexedDB(): Promise<boolean> {
*
* @returns Measurement ID.
*/
export async function initializeAnalytics(
export async function _initializeAnalytics(
app: FirebaseApp,
dynamicConfigPromisesList: Array<
Promise<DynamicConfig | MinimalDynamicConfig>
Expand Down
4 changes: 2 additions & 2 deletions packages-exp/analytics-exp/src/public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export interface Promotion {
* Standard gtag.js control parameters.
* For more information, see
* {@link https://developers.google.com/gtagjs/reference/ga4-events
* the GA4 reference documentation}.
* | the GA4 reference documentation}.
* @public
*/
export interface ControlParams {
Expand All @@ -242,7 +242,7 @@ export interface ControlParams {
* Standard gtag.js event parameters.
* For more information, see
* {@link https://developers.google.com/gtagjs/reference/ga4-events
* the GA4 reference documentation}.
* | the GA4 reference documentation}.
* @public
*/
export interface EventParams {
Expand Down
54 changes: 43 additions & 11 deletions packages-exp/analytics-exp/testing/get-fake-firebase-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,22 @@
* limitations under the License.
*/

import { FirebaseApp } from '@firebase/app-exp';
import {
FirebaseApp,
initializeApp,
_registerComponent
} from '@firebase/app-exp';
import { Component, ComponentType } from '@firebase/component';
import { _FirebaseInstallationsInternal } from '@firebase/installations-exp';
import { AnalyticsService } from '../src/factory';

const fakeConfig = {
projectId: 'projectId',
authDomain: 'authDomain',
messagingSenderId: 'messagingSenderId',
databaseURL: 'databaseUrl',
storageBucket: 'storageBucket'
};

export function getFakeApp(fakeAppParams?: {
appId?: string;
Expand All @@ -25,16 +39,7 @@ export function getFakeApp(fakeAppParams?: {
}): FirebaseApp {
return {
name: 'appName',
options: {
apiKey: fakeAppParams?.apiKey,
projectId: 'projectId',
authDomain: 'authDomain',
messagingSenderId: 'messagingSenderId',
databaseURL: 'databaseUrl',
storageBucket: 'storageBucket',
appId: fakeAppParams?.appId,
measurementId: fakeAppParams?.measurementId
},
options: { ...fakeConfig, ...fakeAppParams },
automaticDataCollectionEnabled: true
};
}
Expand All @@ -52,3 +57,30 @@ export function getFakeInstallations(
getToken: async () => 'authToken'
};
}

export function getFullApp(fakeAppParams?: {
appId?: string;
apiKey?: string;
measurementId?: string;
}): FirebaseApp {
_registerComponent(
new Component(
'installations-exp-internal',
() => {
return {} as _FirebaseInstallationsInternal;
},
ComponentType.PUBLIC
)
);
_registerComponent(
new Component(
'analytics-exp',
() => {
return {} as AnalyticsService;
},
ComponentType.PUBLIC
)
);
const app = initializeApp({ ...fakeConfig, ...fakeAppParams });
return app;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { getAnalytics, initializeAnalytics, logEvent } from '../../src/index';
import '../setup';
import { expect } from 'chai';
import { stub } from 'sinon';
import { AnalyticsError } from '../../src/errors';

let config: Record<string, string>;
try {
Expand Down Expand Up @@ -86,15 +85,5 @@ describe('FirebaseAnalytics Integration Smoke Tests', () => {
expect(eventCalls.length).to.equal(1);
expect(eventCalls[0].name).to.include('method=email');
});
it('getAnalytics() does not throw if called after initializeAnalytics().', async () => {
const analyticsInstance = getAnalytics(app);
expect(analyticsInstance.app).to.equal(app);
});
it('initializeAnalytics() throws if called more than once.', async () => {
expect(() => initializeAnalytics(app)).to.throw(
AnalyticsError.ALREADY_INITIALIZED
);
await deleteApp(app);
});
});
});
Loading