Skip to content

Commit 0a27d2f

Browse files
authored
Add implementation of getGoogleAnalyticsClientId (#7158)
* Add initial implementation of getGoogleAnalyticsClientId * Update docs devsite * Add checkset * Update changeset description * Add link to client_id in docstring * Update gtagWrapper to take variable number of args for potential fallthrough case * Add API test for getGoogleAnalyticsClientId * Move API functionality to internal function * Update docs for devsite * Removed unused function in test * Update public api with async keyword * Remove comment * Update doc string * Update grammar of changeset * Remove console.log * Update variable name from targetId to measurementId * Removed check for blank measurementId * Add ERROR_FACTORY for promise rejection * Change fieldName from clientId * Update AnalyticsError.NO_CLIENT_ID message * Testing adding comment * remove test comment * Remove comments
1 parent 195e82e commit 0a27d2f

File tree

11 files changed

+173
-11
lines changed

11 files changed

+173
-11
lines changed

.changeset/silent-islands-fix.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/analytics': minor
3+
'firebase': minor
4+
---
5+
6+
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.

common/api-review/analytics.api.md

+3
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ export interface EventParams {
133133
// @public
134134
export function getAnalytics(app?: FirebaseApp): Analytics;
135135

136+
// @public
137+
export function getGoogleAnalyticsClientId(analyticsInstance: Analytics): Promise<string>;
138+
136139
// @public
137140
export interface GtagConfigParams {
138141
// (undocumented)

docs-devsite/analytics.md

+21
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Firebase Analytics
2020
| [getAnalytics(app)](./analytics.md#getanalytics) | Returns an [Analytics](./analytics.analytics.md#analytics_interface) instance for the given app. |
2121
| [initializeAnalytics(app, options)](./analytics.md#initializeanalytics) | Returns an [Analytics](./analytics.analytics.md#analytics_interface) instance for the given app. |
2222
| <b>function(analyticsInstance...)</b> |
23+
| [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)<!-- -->. |
2324
| [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)<!-- -->. |
2425
| [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)<!-- -->. |
2526
| [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)<!-- -->. |
@@ -121,6 +122,26 @@ export declare function initializeAnalytics(app: FirebaseApp, options?: Analytic
121122

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

125+
## getGoogleAnalyticsClientId()
126+
127+
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)<!-- -->.
128+
129+
<b>Signature:</b>
130+
131+
```typescript
132+
export declare function getGoogleAnalyticsClientId(analyticsInstance: Analytics): Promise<string>;
133+
```
134+
135+
### Parameters
136+
137+
| Parameter | Type | Description |
138+
| --- | --- | --- |
139+
| analyticsInstance | [Analytics](./analytics.analytics.md#analytics_interface) | |
140+
141+
<b>Returns:</b>
142+
143+
Promise&lt;string&gt;
144+
124145
## logEvent()
125146

126147
Sends a Google Analytics event with given `eventParams`<!-- -->. This method automatically associates this logged event with this Firebase web app instance on this device.

packages/analytics/src/api.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ import {
5050
setUserProperties as internalSetUserProperties,
5151
setAnalyticsCollectionEnabled as internalSetAnalyticsCollectionEnabled,
5252
_setConsentDefaultForInit,
53-
_setDefaultEventParametersForInit
53+
_setDefaultEventParametersForInit,
54+
internalGetGoogleAnalyticsClientId
5455
} from './functions';
5556
import { ERROR_FACTORY, AnalyticsError } from './errors';
5657

@@ -167,6 +168,24 @@ export function setCurrentScreen(
167168
).catch(e => logger.error(e));
168169
}
169170

171+
/**
172+
* Retrieves a unique Google Analytics identifier for the web client.
173+
* See {@link https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id | client_id}.
174+
*
175+
* @public
176+
*
177+
* @param app - The {@link @firebase/app#FirebaseApp} to use.
178+
*/
179+
export async function getGoogleAnalyticsClientId(
180+
analyticsInstance: Analytics
181+
): Promise<string> {
182+
analyticsInstance = getModularInstance(analyticsInstance);
183+
return internalGetGoogleAnalyticsClientId(
184+
wrappedGtagFunction,
185+
initializationPromisesMap[analyticsInstance.app.options.appId!]
186+
);
187+
}
188+
170189
/**
171190
* Use gtag `config` command to set `user_id`.
172191
*

packages/analytics/src/constants.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@ export const enum GtagCommand {
3535
EVENT = 'event',
3636
SET = 'set',
3737
CONFIG = 'config',
38-
CONSENT = 'consent'
38+
CONSENT = 'consent',
39+
GET = 'get'
3940
}

packages/analytics/src/errors.ts

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const enum AnalyticsError {
2828
CONFIG_FETCH_FAILED = 'config-fetch-failed',
2929
NO_API_KEY = 'no-api-key',
3030
NO_APP_ID = 'no-app-id',
31+
NO_CLIENT_ID = 'no-client-id',
3132
INVALID_GTAG_RESOURCE = 'invalid-gtag-resource'
3233
}
3334

@@ -66,6 +67,7 @@ const ERRORS: ErrorMap<AnalyticsError> = {
6667
[AnalyticsError.NO_APP_ID]:
6768
'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' +
6869
'contain a valid app ID.',
70+
[AnalyticsError.NO_CLIENT_ID]: 'The "client_id" field is empty.',
6971
[AnalyticsError.INVALID_GTAG_RESOURCE]:
7072
'Trusted Types detected an invalid gtag resource: {$gtagURL}.'
7173
};

packages/analytics/src/functions.test.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ import {
2727
defaultEventParametersForInit,
2828
_setDefaultEventParametersForInit,
2929
_setConsentDefaultForInit,
30-
defaultConsentSettingsForInit
30+
defaultConsentSettingsForInit,
31+
internalGetGoogleAnalyticsClientId
3132
} from './functions';
3233
import { GtagCommand } from './constants';
3334
import { ConsentSettings } from './public-types';
35+
import { Gtag } from './types';
36+
import { AnalyticsError } from './errors';
3437

3538
const fakeMeasurementId = 'abcd-efgh-ijkl';
3639
const fakeInitializationPromise = Promise.resolve(fakeMeasurementId);
@@ -238,4 +241,34 @@ describe('FirebaseAnalytics methods', () => {
238241
...additionalParams
239242
});
240243
});
244+
it('internalGetGoogleAnalyticsClientId() rejects when no client_id is available', async () => {
245+
await expect(
246+
internalGetGoogleAnalyticsClientId(
247+
function fakeWrappedGtag(
248+
unused1: unknown,
249+
unused2: unknown,
250+
unused3: unknown,
251+
callBackStub: (clientId: string) => {}
252+
): void {
253+
callBackStub('');
254+
} as Gtag,
255+
fakeInitializationPromise
256+
)
257+
).to.be.rejectedWith(AnalyticsError.NO_CLIENT_ID);
258+
});
259+
it('internalGetGoogleAnalyticsClientId() returns client_id when available', async () => {
260+
const CLIENT_ID = 'clientId1234';
261+
const id = await internalGetGoogleAnalyticsClientId(
262+
function fakeWrappedGtag(
263+
unused1: unknown,
264+
unused2: unknown,
265+
unused3: unknown,
266+
callBackStub: (clientId: string) => {}
267+
): void {
268+
callBackStub(CLIENT_ID);
269+
} as Gtag,
270+
fakeInitializationPromise
271+
);
272+
expect(id).to.equal(CLIENT_ID);
273+
});
241274
});

packages/analytics/src/functions.ts

+27
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from './public-types';
2525
import { Gtag } from './types';
2626
import { GtagCommand } from './constants';
27+
import { AnalyticsError, ERROR_FACTORY } from './errors';
2728

2829
/**
2930
* Event parameters to set on 'gtag' during initialization.
@@ -137,6 +138,32 @@ export async function setUserProperties(
137138
}
138139
}
139140

141+
/**
142+
* Retrieves a unique Google Analytics identifier for the web client.
143+
* See {@link https://developers.google.com/analytics/devguides/collection/ga4/reference/config#client_id | client_id}.
144+
*
145+
* @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event
146+
*/
147+
export async function internalGetGoogleAnalyticsClientId(
148+
gtagFunction: Gtag,
149+
initializationPromise: Promise<string>
150+
): Promise<string> {
151+
const measurementId = await initializationPromise;
152+
return new Promise((resolve, reject) => {
153+
gtagFunction(
154+
GtagCommand.GET,
155+
measurementId,
156+
'client_id',
157+
(clientId: string) => {
158+
if (!clientId) {
159+
reject(ERROR_FACTORY.create(AnalyticsError.NO_CLIENT_ID));
160+
}
161+
resolve(clientId);
162+
}
163+
);
164+
});
165+
}
166+
140167
/**
141168
* Set whether collection is enabled for this ID.
142169
*

packages/analytics/src/helpers.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,37 @@ describe('Gtag wrapping functions', () => {
332332
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
333333
});
334334

335+
it('new window.gtag function does not wait when sending "get" calls', async () => {
336+
wrapOrCreateGtag(
337+
{ [fakeAppId]: Promise.resolve(fakeMeasurementId) },
338+
fakeDynamicConfigPromises,
339+
{},
340+
'dataLayer',
341+
'gtag'
342+
);
343+
window['dataLayer'] = [];
344+
(window['gtag'] as Gtag)(
345+
GtagCommand.GET,
346+
fakeMeasurementId,
347+
'client_id',
348+
clientId => console.log(clientId)
349+
);
350+
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
351+
});
352+
353+
it('new window.gtag function does not wait when sending an unknown command', async () => {
354+
wrapOrCreateGtag(
355+
{ [fakeAppId]: Promise.resolve(fakeMeasurementId) },
356+
fakeDynamicConfigPromises,
357+
{},
358+
'dataLayer',
359+
'gtag'
360+
);
361+
window['dataLayer'] = [];
362+
(window['gtag'] as Gtag)('new-command-from-gtag-team', fakeMeasurementId);
363+
expect((window['dataLayer'] as DataLayer).length).to.equal(1);
364+
});
365+
335366
it('new window.gtag function waits for initialization promise when sending "config" calls', async () => {
336367
const initPromise1 = new Deferred<string>();
337368
wrapOrCreateGtag(

packages/analytics/src/helpers.ts

+20-8
Original file line numberDiff line numberDiff line change
@@ -277,37 +277,49 @@ function wrapGtag(
277277
* @param gtagParams Params if event is EVENT/CONFIG.
278278
*/
279279
async function gtagWrapper(
280-
command: 'config' | 'set' | 'event' | 'consent',
281-
idOrNameOrParams: string | ControlParams,
282-
gtagParams?: GtagConfigOrEventParams | ConsentSettings
280+
command: 'config' | 'set' | 'event' | 'consent' | 'get' | string,
281+
...args: unknown[]
283282
): Promise<void> {
284283
try {
285284
// If event, check that relevant initialization promises have completed.
286285
if (command === GtagCommand.EVENT) {
286+
const [measurementId, gtagParams] = args;
287287
// If EVENT, second arg must be measurementId.
288288
await gtagOnEvent(
289289
gtagCore,
290290
initializationPromisesMap,
291291
dynamicConfigPromisesList,
292-
idOrNameOrParams as string,
292+
measurementId as string,
293293
gtagParams as GtagConfigOrEventParams
294294
);
295295
} else if (command === GtagCommand.CONFIG) {
296+
const [measurementId, gtagParams] = args;
296297
// If CONFIG, second arg must be measurementId.
297298
await gtagOnConfig(
298299
gtagCore,
299300
initializationPromisesMap,
300301
dynamicConfigPromisesList,
301302
measurementIdToAppId,
302-
idOrNameOrParams as string,
303+
measurementId as string,
303304
gtagParams as GtagConfigOrEventParams
304305
);
305306
} else if (command === GtagCommand.CONSENT) {
306-
// If CONFIG, second arg must be measurementId.
307+
const [gtagParams] = args;
307308
gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings);
308-
} else {
309+
} else if (command === GtagCommand.GET) {
310+
const [measurementId, fieldName, callback] = args;
311+
gtagCore(
312+
GtagCommand.GET,
313+
measurementId as string,
314+
fieldName as string,
315+
callback as (...args: unknown[]) => void
316+
);
317+
} else if (command === GtagCommand.SET) {
318+
const [customParams] = args;
309319
// If SET, second arg must be params.
310-
gtagCore(GtagCommand.SET, idOrNameOrParams as CustomParams);
320+
gtagCore(GtagCommand.SET, customParams as CustomParams);
321+
} else {
322+
gtagCore(command, ...args);
311323
}
312324
} catch (e) {
313325
logger.error(e);

packages/analytics/src/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ export interface Gtag {
7373
subCommand: 'default' | 'update',
7474
consentSettings: ConsentSettings
7575
): void;
76+
(
77+
command: 'get',
78+
measurementId: string,
79+
fieldName: string,
80+
callback: (...args: unknown[]) => void
81+
): void;
82+
(command: string, ...args: unknown[]): void;
7683
}
7784

7885
export type DataLayer = IArguments[];

0 commit comments

Comments
 (0)