Skip to content

Commit

Permalink
Merge 9357f51 into 253b998
Browse files Browse the repository at this point in the history
  • Loading branch information
dwyfrequency authored Apr 18, 2023
2 parents 253b998 + 9357f51 commit 248c028
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changeset/silent-islands-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/analytics': minor
'firebase': minor
---

Add method `getGoogleAnalyticsClientId()` to retrieve an unique identifier for a web client. This allows users to log purchase and other events from their backends using Google Analytics 4 Measurement Protocol and to have those events be connected to actions taken on the client within their Firebase web app. `getGoogleAnalyticsClientId()` will simplify this event recording process.
3 changes: 3 additions & 0 deletions common/api-review/analytics.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ export interface EventParams {
// @public
export function getAnalytics(app?: FirebaseApp): Analytics;

// @public
export function getGoogleAnalyticsClientId(analyticsInstance: Analytics): Promise<string>;

// @public
export interface GtagConfigParams {
// (undocumented)
Expand Down
21 changes: 21 additions & 0 deletions docs-devsite/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Firebase Analytics
| [getAnalytics(app)](./analytics.md#getanalytics) | Returns an [Analytics](./analytics.analytics.md#analytics_interface) instance for the given app. |
| [initializeAnalytics(app, options)](./analytics.md#initializeanalytics) | Returns an [Analytics](./analytics.analytics.md#analytics_interface) instance for the given app. |
| <b>function(analyticsInstance...)</b> |
| [getGoogleAnalyticsClientId(analyticsInstance)](./analytics.md#getgoogleanalyticsclientid) | Retrieves a unique Google Analytics identifier for the web client. See [client\_id](https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id)<!-- -->. |
| [logEvent(analyticsInstance, eventName, eventParams, options)](./analytics.md#logevent) | Sends a Google Analytics event with given <code>eventParams</code>. This method automatically associates this logged event with this Firebase web app instance on this device.<!-- -->List of recommended event parameters can be found in [the GA4 reference documentation](https://developers.google.com/gtagjs/reference/ga4-events)<!-- -->. |
| [logEvent(analyticsInstance, eventName, eventParams, options)](./analytics.md#logevent) | Sends a Google Analytics event with given <code>eventParams</code>. This method automatically associates this logged event with this Firebase web app instance on this device.<!-- -->List of recommended event parameters can be found in [the GA4 reference documentation](https://developers.google.com/gtagjs/reference/ga4-events)<!-- -->. |
| [logEvent(analyticsInstance, eventName, eventParams, options)](./analytics.md#logevent) | Sends a Google Analytics event with given <code>eventParams</code>. This method automatically associates this logged event with this Firebase web app instance on this device.<!-- -->See [Track Screenviews](https://firebase.google.com/docs/analytics/screenviews)<!-- -->. |
Expand Down Expand Up @@ -121,6 +122,26 @@ export declare function initializeAnalytics(app: FirebaseApp, options?: Analytic

[Analytics](./analytics.analytics.md#analytics_interface)

## getGoogleAnalyticsClientId()

Retrieves a unique Google Analytics identifier for the web client. See [client\_id](https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id)<!-- -->.

<b>Signature:</b>

```typescript
export declare function getGoogleAnalyticsClientId(analyticsInstance: Analytics): Promise<string>;
```

### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| analyticsInstance | [Analytics](./analytics.analytics.md#analytics_interface) | |

<b>Returns:</b>

Promise&lt;string&gt;

## logEvent()

Sends a Google Analytics event with given `eventParams`<!-- -->. This method automatically associates this logged event with this Firebase web app instance on this device.
Expand Down
21 changes: 20 additions & 1 deletion packages/analytics/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ import {
setUserProperties as internalSetUserProperties,
setAnalyticsCollectionEnabled as internalSetAnalyticsCollectionEnabled,
_setConsentDefaultForInit,
_setDefaultEventParametersForInit
_setDefaultEventParametersForInit,
internalGetGoogleAnalyticsClientId
} from './functions';
import { ERROR_FACTORY, AnalyticsError } from './errors';

Expand Down Expand Up @@ -167,6 +168,24 @@ export function setCurrentScreen(
).catch(e => logger.error(e));
}

/**
* Retrieves a unique Google Analytics identifier for the web client.
* See {@link https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id | client_id}.
*
* @public
*
* @param app - The {@link @firebase/app#FirebaseApp} to use.
*/
export async function getGoogleAnalyticsClientId(
analyticsInstance: Analytics
): Promise<string> {
analyticsInstance = getModularInstance(analyticsInstance);
return internalGetGoogleAnalyticsClientId(
wrappedGtagFunction,
initializationPromisesMap[analyticsInstance.app.options.appId!]
);
}

/**
* Use gtag `config` command to set `user_id`.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/analytics/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ export const enum GtagCommand {
EVENT = 'event',
SET = 'set',
CONFIG = 'config',
CONSENT = 'consent'
CONSENT = 'consent',
GET = 'get'
}
2 changes: 2 additions & 0 deletions packages/analytics/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const enum AnalyticsError {
CONFIG_FETCH_FAILED = 'config-fetch-failed',
NO_API_KEY = 'no-api-key',
NO_APP_ID = 'no-app-id',
NO_CLIENT_ID = 'no-client-id',
INVALID_GTAG_RESOURCE = 'invalid-gtag-resource'
}

Expand Down Expand Up @@ -66,6 +67,7 @@ const ERRORS: ErrorMap<AnalyticsError> = {
[AnalyticsError.NO_APP_ID]:
'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' +
'contain a valid app ID.',
[AnalyticsError.NO_CLIENT_ID]: 'The "client_id" field is empty.',
[AnalyticsError.INVALID_GTAG_RESOURCE]:
'Trusted Types detected an invalid gtag resource: {$gtagURL}.'
};
Expand Down
35 changes: 34 additions & 1 deletion packages/analytics/src/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ import {
defaultEventParametersForInit,
_setDefaultEventParametersForInit,
_setConsentDefaultForInit,
defaultConsentSettingsForInit
defaultConsentSettingsForInit,
internalGetGoogleAnalyticsClientId
} from './functions';
import { GtagCommand } from './constants';
import { ConsentSettings } from './public-types';
import { Gtag } from './types';
import { AnalyticsError } from './errors';

const fakeMeasurementId = 'abcd-efgh-ijkl';
const fakeInitializationPromise = Promise.resolve(fakeMeasurementId);
Expand Down Expand Up @@ -238,4 +241,34 @@ describe('FirebaseAnalytics methods', () => {
...additionalParams
});
});
it('internalGetGoogleAnalyticsClientId() rejects when no client_id is available', async () => {
await expect(
internalGetGoogleAnalyticsClientId(
function fakeWrappedGtag(
unused1: unknown,
unused2: unknown,
unused3: unknown,
callBackStub: (clientId: string) => {}
): void {
callBackStub('');
} as Gtag,
fakeInitializationPromise
)
).to.be.rejectedWith(AnalyticsError.NO_CLIENT_ID);
});
it('internalGetGoogleAnalyticsClientId() returns client_id when available', async () => {
const CLIENT_ID = 'clientId1234';
const id = await internalGetGoogleAnalyticsClientId(
function fakeWrappedGtag(
unused1: unknown,
unused2: unknown,
unused3: unknown,
callBackStub: (clientId: string) => {}
): void {
callBackStub(CLIENT_ID);
} as Gtag,
fakeInitializationPromise
);
expect(id).to.equal(CLIENT_ID);
});
});
27 changes: 27 additions & 0 deletions packages/analytics/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from './public-types';
import { Gtag } from './types';
import { GtagCommand } from './constants';
import { AnalyticsError, ERROR_FACTORY } from './errors';

/**
* Event parameters to set on 'gtag' during initialization.
Expand Down Expand Up @@ -137,6 +138,32 @@ export async function setUserProperties(
}
}

/**
* Retrieves a unique Google Analytics identifier for the web client.
* See {@link https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id | client_id}.
*
* @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event
*/
export async function internalGetGoogleAnalyticsClientId(
gtagFunction: Gtag,
initializationPromise: Promise<string>
): Promise<string> {
const measurementId = await initializationPromise;
return new Promise((resolve, reject) => {
gtagFunction(
GtagCommand.GET,
measurementId,
'client_id',
(clientId: string) => {
if (!clientId) {
reject(ERROR_FACTORY.create(AnalyticsError.NO_CLIENT_ID));
}
resolve(clientId);
}
);
});
}

/**
* Set whether collection is enabled for this ID.
*
Expand Down
31 changes: 31 additions & 0 deletions packages/analytics/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,37 @@ describe('Gtag wrapping functions', () => {
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
});

it('new window.gtag function does not wait when sending "get" calls', async () => {
wrapOrCreateGtag(
{ [fakeAppId]: Promise.resolve(fakeMeasurementId) },
fakeDynamicConfigPromises,
{},
'dataLayer',
'gtag'
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)(
GtagCommand.GET,
fakeMeasurementId,
'client_id',
clientId => console.log(clientId)
);
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
});

it('new window.gtag function does not wait when sending an unknown command', async () => {
wrapOrCreateGtag(
{ [fakeAppId]: Promise.resolve(fakeMeasurementId) },
fakeDynamicConfigPromises,
{},
'dataLayer',
'gtag'
);
window['dataLayer'] = [];
(window['gtag'] as Gtag)('new-command-from-gtag-team', fakeMeasurementId);
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
});

it('new window.gtag function waits for initialization promise when sending "config" calls', async () => {
const initPromise1 = new Deferred<string>();
wrapOrCreateGtag(
Expand Down
28 changes: 20 additions & 8 deletions packages/analytics/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,37 +277,49 @@ function wrapGtag(
* @param gtagParams Params if event is EVENT/CONFIG.
*/
async function gtagWrapper(
command: 'config' | 'set' | 'event' | 'consent',
idOrNameOrParams: string | ControlParams,
gtagParams?: GtagConfigOrEventParams | ConsentSettings
command: 'config' | 'set' | 'event' | 'consent' | 'get' | string,
...args: unknown[]
): Promise<void> {
try {
// If event, check that relevant initialization promises have completed.
if (command === GtagCommand.EVENT) {
const [measurementId, gtagParams] = args;
// If EVENT, second arg must be measurementId.
await gtagOnEvent(
gtagCore,
initializationPromisesMap,
dynamicConfigPromisesList,
idOrNameOrParams as string,
measurementId as string,
gtagParams as GtagConfigOrEventParams
);
} else if (command === GtagCommand.CONFIG) {
const [measurementId, gtagParams] = args;
// If CONFIG, second arg must be measurementId.
await gtagOnConfig(
gtagCore,
initializationPromisesMap,
dynamicConfigPromisesList,
measurementIdToAppId,
idOrNameOrParams as string,
measurementId as string,
gtagParams as GtagConfigOrEventParams
);
} else if (command === GtagCommand.CONSENT) {
// If CONFIG, second arg must be measurementId.
const [gtagParams] = args;
gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings);
} else {
} else if (command === GtagCommand.GET) {
const [measurementId, fieldName, callback] = args;
gtagCore(
GtagCommand.GET,
measurementId as string,
fieldName as string,
callback as (...args: unknown[]) => void
);
} else if (command === GtagCommand.SET) {
const [customParams] = args;
// If SET, second arg must be params.
gtagCore(GtagCommand.SET, idOrNameOrParams as CustomParams);
gtagCore(GtagCommand.SET, customParams as CustomParams);
} else {
gtagCore(command, ...args);
}
} catch (e) {
logger.error(e);
Expand Down
7 changes: 7 additions & 0 deletions packages/analytics/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ export interface Gtag {
subCommand: 'default' | 'update',
consentSettings: ConsentSettings
): void;
(
command: 'get',
measurementId: string,
fieldName: string,
callback: (...args: unknown[]) => void
): void;
(command: string, ...args: unknown[]): void;
}

export type DataLayer = IArguments[];

0 comments on commit 248c028

Please sign in to comment.