Skip to content
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

Add implementation of getGoogleAnalyticsClientId #7158

Merged
merged 24 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7071d2b
Add initial implementation of getGoogleAnalyticsClientId
dwyfrequency Mar 26, 2023
156137e
Update docs devsite
dwyfrequency Mar 26, 2023
510a89a
Add checkset
dwyfrequency Mar 26, 2023
41ef1bf
Update changeset description
dwyfrequency Mar 27, 2023
8f6ac07
Add link to client_id in docstring
dwyfrequency Mar 31, 2023
3e401cb
Update gtagWrapper to take variable number of args for potential fall…
dwyfrequency Apr 8, 2023
4c93e53
Add API test for getGoogleAnalyticsClientId
dwyfrequency Apr 10, 2023
c4b9792
Move API functionality to internal function
dwyfrequency Apr 11, 2023
54d83aa
Update docs for devsite
dwyfrequency Apr 11, 2023
2ecd32d
Removed unused function in test
dwyfrequency Apr 11, 2023
9f5cfb5
Update public api with async keyword
dwyfrequency Apr 11, 2023
c6d4109
Remove comment
dwyfrequency Apr 12, 2023
5516b49
Update doc string
dwyfrequency Apr 12, 2023
6eef67c
Update grammar of changeset
dwyfrequency Apr 12, 2023
eca9407
Remove console.log
dwyfrequency Apr 12, 2023
0e3b900
Update variable name from targetId to measurementId
dwyfrequency Apr 12, 2023
0ec1f27
Removed check for blank measurementId
dwyfrequency Apr 12, 2023
a585a16
Add ERROR_FACTORY for promise rejection
dwyfrequency Apr 13, 2023
953e499
Change fieldName from clientId
dwyfrequency Apr 13, 2023
7149472
Update AnalyticsError.NO_CLIENT_ID message
dwyfrequency Apr 13, 2023
93ed844
Merge branch 'master' into jd-getclientid-analytics-v2
dwyfrequency Apr 14, 2023
8332162
Testing adding comment
dwyfrequency Apr 18, 2023
888aba4
remove test comment
dwyfrequency Apr 18, 2023
9357f51
Remove comments
dwyfrequency Apr 18, 2023
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
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()
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved

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.measurementId!]
);
}

/**
* 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);
});
});
29 changes: 29 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,34 @@ 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}.
*
* @public
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved
*
* @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 @@ -331,6 +331,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
27 changes: 20 additions & 7 deletions packages/analytics/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,37 +277,50 @@ 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) {
const [gtagParams] = args;
// If CONFIG, second arg must be measurementId.
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved
gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings);
} else {
} else if (command === GtagCommand.GET) {
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved
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[];