Skip to content

Add initializeAnalytics() to analytics-exp #4575

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 7 commits into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 32 additions & 0 deletions common/api-review/analytics-exp.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export interface AnalyticsCallOptions {
global: boolean;
}

// @public
export interface AnalyticsOptions {
config?: GtagConfigParams | EventParams;
}

// @public
export interface ControlParams {
// (undocumented)
Expand Down Expand Up @@ -45,6 +50,8 @@ export type EventNameString = 'add_payment_info' | 'add_shipping_info' | 'add_to

// @public
export interface EventParams {
// (undocumented)
[key: string]: unknown;
// (undocumented)
affiliation?: string;
// (undocumented)
Expand Down Expand Up @@ -110,6 +117,31 @@ export interface EventParams {
// @public
export function getAnalytics(app?: FirebaseApp): Analytics;

// @public
export interface GtagConfigParams {
'allow_google_signals?': boolean;
// (undocumented)
[key: string]: unknown;
'allow_ad_personalization_signals'?: boolean;
'anonymize_ip'?: boolean;
'cookie_domain'?: string;
'cookie_expires'?: number;
'cookie_flags'?: string;
'cookie_prefix'?: string;
'cookie_update'?: boolean;
'custom_map'?: {
[key: string]: unknown;
};
'link_attribution'?: boolean;
'page_location'?: string;
'page_path'?: string;
'page_title'?: string;
'send_page_view'?: boolean;
}

// @public
export function initializeAnalytics(app: FirebaseApp, options?: AnalyticsOptions): Analytics;

// @public
export function isSupported(): Promise<boolean>;

Expand Down
31 changes: 30 additions & 1 deletion packages-exp/analytics-exp/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { _getProvider, FirebaseApp, getApp } from '@firebase/app-exp';
import {
Analytics,
AnalyticsCallOptions,
AnalyticsOptions,
CustomParams,
EventNameString,
EventParams
Expand All @@ -47,6 +48,7 @@ import {
setUserProperties as internalSetUserProperties,
setAnalyticsCollectionEnabled as internalSetAnalyticsCollectionEnabled
} from './functions';
import { ERROR_FACTORY, AnalyticsError } from './errors';

export { settings } from './factory';

Expand All @@ -70,7 +72,34 @@ export function getAnalytics(app: FirebaseApp = getApp()): Analytics {
app,
ANALYTICS_TYPE
);
const analyticsInstance = analyticsProvider.getImmediate();

if (analyticsProvider.isInitialized()) {
return analyticsProvider.getImmediate();
}

return initializeAnalytics(app);
}

/**
* Returns a Firebase Analytics instance for the given app.
*
* @public
*
* @param app - The FirebaseApp to use.
*/
export function initializeAnalytics(
app: FirebaseApp,
options: AnalyticsOptions = {}
): Analytics {
// Dependencies
const analyticsProvider: Provider<'analytics-exp'> = _getProvider(
app,
ANALYTICS_TYPE
);
if (analyticsProvider.isInitialized()) {
throw ERROR_FACTORY.create(AnalyticsError.ALREADY_INITIALIZED);
}
const analyticsInstance = analyticsProvider.initialize({ options });
return analyticsInstance;
}

Expand Down
5 changes: 5 additions & 0 deletions packages-exp/analytics-exp/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ErrorFactory, ErrorMap } from '@firebase/util';
export const enum AnalyticsError {
ALREADY_EXISTS = 'already-exists',
ALREADY_INITIALIZED = 'already-initialized',
ALREADY_INITIALIZED_SETTINGS = 'already-initialized-settings',
INTEROP_COMPONENT_REG_FAILED = 'interop-component-reg-failed',
INVALID_ANALYTICS_CONTEXT = 'invalid-analytics-context',
INDEXEDDB_UNAVAILABLE = 'indexeddb-unavailable',
Expand All @@ -35,6 +36,10 @@ 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 ' +
'to get a reference to the already-intialized instance.',
[AnalyticsError.ALREADY_INITIALIZED_SETTINGS]:
'Firebase Analytics has already been initialized.' +
'settings() must be called before initializing any Analytics instance' +
'or it will have no effect.',
Expand Down
8 changes: 5 additions & 3 deletions packages-exp/analytics-exp/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* limitations under the License.
*/

import { SettingsOptions, Analytics } from './public-types';
import { SettingsOptions, Analytics, AnalyticsOptions } from './public-types';
import { Gtag, DynamicConfig, MinimalDynamicConfig } from './types';
import { getOrCreateDataLayer, wrapOrCreateGtag } from './helpers';
import { AnalyticsError, ERROR_FACTORY } from './errors';
Expand Down Expand Up @@ -176,7 +176,8 @@ function warnOnBrowserContextMismatch(): void {
*/
export function factory(
app: FirebaseApp,
installations: _FirebaseInstallationsInternal
installations: _FirebaseInstallationsInternal,
options?: AnalyticsOptions
): AnalyticsService {
warnOnBrowserContextMismatch();
const appId = app.options.appId;
Expand Down Expand Up @@ -226,7 +227,8 @@ export function factory(
measurementIdToAppId,
installations,
gtagCoreFunction,
dataLayerName
dataLayerName,
options
);

const analyticsInstance: AnalyticsService = new AnalyticsService(app);
Expand Down
7 changes: 4 additions & 3 deletions packages-exp/analytics-exp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import { ANALYTICS_TYPE } from './constants';
import {
Component,
ComponentType,
ComponentContainer
ComponentContainer,
InstanceFactoryOptions
} from '@firebase/component';
import { ERROR_FACTORY, AnalyticsError } from './errors';
import { logEvent } from './api';
Expand All @@ -46,14 +47,14 @@ function registerAnalytics(): void {
_registerComponent(
new Component(
ANALYTICS_TYPE,
container => {
(container, { options: analyticsOptions }: InstanceFactoryOptions) => {
// getImmediate for FirebaseApp will always succeed
const app = container.getProvider('app-exp').getImmediate();
const installations = container
.getProvider('installations-exp-internal')
.getImmediate();

return factory(app, installations);
return factory(app, installations, analyticsOptions);
},
ComponentType.PUBLIC
)
Expand Down
20 changes: 19 additions & 1 deletion packages-exp/analytics-exp/src/initialize-analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function stubFetch(): void {
fetchStub.returns(Promise.resolve(mockResponse));
}

describe('initializeIds()', () => {
describe('initializeAnalytics()', () => {
const gtagStub: SinonStub = stub();
const dynamicPromisesList: Array<Promise<DynamicConfig>> = [];
const measurementIdToAppId: { [key: string]: string } = {};
Expand Down Expand Up @@ -79,6 +79,24 @@ describe('initializeIds()', () => {
update: true
});
});
it('calls gtag config with options if provided', async () => {
stubFetch();
await initializeAnalytics(
app,
dynamicPromisesList,
measurementIdToAppId,
fakeInstallations,
gtagStub,
'dataLayer',
{ config: { 'send_page_view': false } }
);
expect(gtagStub).to.be.calledWith(GtagCommand.CONFIG, fakeMeasurementId, {
'firebase_id': fakeFid,
'origin': 'firebase',
update: true,
'send_page_view': false
});
});
it('puts dynamic fetch promise into dynamic promises list', async () => {
stubFetch();
await initializeAnalytics(
Expand Down
15 changes: 9 additions & 6 deletions packages-exp/analytics-exp/src/initialize-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from '@firebase/util';
import { ERROR_FACTORY, AnalyticsError } from './errors';
import { findGtagScriptOnPage, insertScriptTag } from './helpers';
import { AnalyticsOptions } from './public-types';

async function validateIndexedDB(): Promise<boolean> {
if (!isIndexedDBAvailable()) {
Expand Down Expand Up @@ -72,7 +73,8 @@ export async function initializeAnalytics(
measurementIdToAppId: { [key: string]: string },
installations: _FirebaseInstallationsInternal,
gtagCore: Gtag,
dataLayerName: string
dataLayerName: string,
options?: AnalyticsOptions
): Promise<string> {
const dynamicConfigPromise = fetchDynamicConfigWithRetry(app);
// Once fetched, map measurementIds to appId, for ease of lookup in wrapped gtag function.
Expand Down Expand Up @@ -121,12 +123,13 @@ export async function initializeAnalytics(
// We keep it together with other initialization logic for better code structure.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(gtagCore as any)('js', new Date());
// User config added first. We don't want users to accidentally overwrite
// base Firebase config properties.
const configProperties: Record<string, unknown> = options?.config ?? {};
Copy link
Member

Choose a reason for hiding this comment

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

nice! a lot of ?s


const configProperties: { [key: string]: string | boolean } = {
// guard against developers accidentally setting properties with prefix `firebase_`
[ORIGIN_KEY]: 'firebase',
update: true
};
// guard against developers accidentally setting properties with prefix `firebase_`
configProperties[ORIGIN_KEY] = 'firebase';
configProperties.update = true;

if (fid != null) {
configProperties[GA_FID_KEY] = fid;
Expand Down
103 changes: 98 additions & 5 deletions packages-exp/analytics-exp/src/public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,98 @@

import { FirebaseApp } from '@firebase/app-exp';

/**
* A set of common Analytics config settings recognized by
* gtag.
* @public
*/
export interface GtagConfigParams {
/**
* Whether or not a page view should be sent.
* If set to true (default), a page view is automatically sent upon initialization
* of analytics.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/pages
*/
'send_page_view'?: boolean;
/**
* The title of the page.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/pages
*/
'page_title'?: string;
/**
* The path to the page. If overridden, this value must start with a / character.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/pages
*/
'page_path'?: string;
/**
* The URL of the page.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/pages
*/
'page_location'?: string;
/**
* Defaults to `auto`.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id
*/
'cookie_domain'?: string;
/**
* Defaults to 63072000 (two years, in seconds).
* See https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id
*/
'cookie_expires'?: number;
/**
* Defaults to `_ga`.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id
*/
'cookie_prefix'?: string;
/**
* If set to true, will update cookies on each page load.
* Defaults to true.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id
*/
'cookie_update'?: boolean;
/**
* Appends additional flags to the cookie when set.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/cookies-user-id
*/
'cookie_flags'?: string;
/**
* If set to false, disables all advertising features with gtag.js.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/display-features
*/
'allow_google_signals?': boolean;
/**
* If set to false, disables all advertising personalization with gtag.js.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/display-features
*/
'allow_ad_personalization_signals'?: boolean;
/**
* See https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-link-attribution
*/
'link_attribution'?: boolean;
/**
* If set to true, anonymizes IP addresses for all events.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/ip-anonymization
*/
'anonymize_ip'?: boolean;
/**
* Custom dimensions and metrics.
* See https://developers.google.com/analytics/devguides/collection/gtagjs/custom-dims-mets
*/
'custom_map'?: { [key: string]: unknown };
[key: string]: unknown;
}

/**
* Analytics initialization options.
* @public
*/
export interface AnalyticsOptions {
/**
* Params to be passed in the initial gtag config call during analytics initialization.
*/
config?: GtagConfigParams | EventParams;
}

/**
* Additional options that can be passed to Firebase Analytics method
* calls such as `logEvent`, `setCurrentScreen`, etc.
Expand All @@ -31,13 +123,12 @@ export interface AnalyticsCallOptions {
}

/**
* The Firebase Analytics service interface.
*
* An instance of Firebase Analytics.
* @public
*/
export interface Analytics {
/**
* The FirebaseApp this Functions instance is associated with.
* The FirebaseApp this Analytics instance is associated with.
*/
app: FirebaseApp;
}
Expand All @@ -61,6 +152,7 @@ export interface SettingsOptions {
export interface CustomParams {
[key: string]: unknown;
}

/**
* Type for standard gtag.js event names. `logEvent` also accepts any
* custom string and interprets it as a custom event name.
Expand Down Expand Up @@ -96,14 +188,14 @@ export type EventNameString =
| 'view_search_results';

/**
* Currency field used by some Analytics events.
* Standard analytics currency type.
* @public
*/
export type Currency = string | number;

/* eslint-disable camelcase */
/**
* Item field used by some Analytics events.
* Standard analytics `Item` type.
* @public
*/
export interface Item {
Expand Down Expand Up @@ -204,5 +296,6 @@ export interface EventParams {
page_title?: string;
page_location?: string;
page_path?: string;
[key: string]: unknown;
}
/* eslint-enable camelcase */
Loading