From 7071d2b1fc9aa33358ee7eac490fa979e8cc68bc Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Sun, 26 Mar 2023 19:14:55 -0400 Subject: [PATCH 01/23] Add initial implementation of getGoogleAnalyticsClientId --- common/api-review/analytics.api.md | 3 +++ packages/analytics/src/api.ts | 34 ++++++++++++++++++++++++++ packages/analytics/src/constants.ts | 3 ++- packages/analytics/src/helpers.test.ts | 18 ++++++++++++++ packages/analytics/src/helpers.ts | 12 +++++++-- packages/analytics/src/types.ts | 6 +++++ 6 files changed, 73 insertions(+), 3 deletions(-) diff --git a/common/api-review/analytics.api.md b/common/api-review/analytics.api.md index af301eebfa0..7b6e9cfb182 100644 --- a/common/api-review/analytics.api.md +++ b/common/api-review/analytics.api.md @@ -133,6 +133,9 @@ export interface EventParams { // @public export function getAnalytics(app?: FirebaseApp): Analytics; +// @public +export function getGoogleAnalyticsClientId(analyticsInstance: Analytics): Promise; + // @public export interface GtagConfigParams { // (undocumented) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index 3e3119fcb78..5a7867288e7 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -167,6 +167,40 @@ export function setCurrentScreen( ).catch(e => logger.error(e)); } +/** + * Retrieves a unique identifier for the web client. + * + * @public + * + * @param app - The {@link @firebase/app#FirebaseApp} to use. + */ +export async function getGoogleAnalyticsClientId( + analyticsInstance: Analytics +): Promise { + analyticsInstance = getModularInstance(analyticsInstance); + const measurementId = analyticsInstance.app.options.measurementId; + let clientId = ''; + if (!measurementId) { + logger.error('The app has no recognizable measurement ID.'); + } else { + clientId = await new Promise((resolve, reject) => { + wrappedGtagFunction( + GtagCommand.GET, + measurementId, + 'client_id', + fieldName => { + if (!fieldName) { + reject('There was an issue retrieving the `client_id`'); + } + resolve(fieldName); + } + ); + }); + } + + return clientId; +} + /** * Use gtag `config` command to set `user_id`. * diff --git a/packages/analytics/src/constants.ts b/packages/analytics/src/constants.ts index 7f3da181288..e95e76e0ace 100644 --- a/packages/analytics/src/constants.ts +++ b/packages/analytics/src/constants.ts @@ -35,5 +35,6 @@ export const enum GtagCommand { EVENT = 'event', SET = 'set', CONFIG = 'config', - CONSENT = 'consent' + CONSENT = 'consent', + GET = 'get' } diff --git a/packages/analytics/src/helpers.test.ts b/packages/analytics/src/helpers.test.ts index 1175ff0f61e..7b17dfa9d46 100644 --- a/packages/analytics/src/helpers.test.ts +++ b/packages/analytics/src/helpers.test.ts @@ -263,6 +263,24 @@ 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', + fieldName => console.log(fieldName) + ); + 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(); wrapOrCreateGtag( diff --git a/packages/analytics/src/helpers.ts b/packages/analytics/src/helpers.ts index 1fb8a38319c..e21e3744bde 100644 --- a/packages/analytics/src/helpers.ts +++ b/packages/analytics/src/helpers.ts @@ -227,9 +227,10 @@ function wrapGtag( * @param gtagParams Params if event is EVENT/CONFIG. */ async function gtagWrapper( - command: 'config' | 'set' | 'event' | 'consent', + command: 'config' | 'set' | 'event' | 'consent' | 'get', idOrNameOrParams: string | ControlParams, - gtagParams?: GtagConfigOrEventParams | ConsentSettings + gtagParams?: GtagConfigOrEventParams | ConsentSettings | string, + callback?: (s: string) => void ): Promise { try { // If event, check that relevant initialization promises have completed. @@ -255,6 +256,13 @@ function wrapGtag( } else if (command === GtagCommand.CONSENT) { // If CONFIG, second arg must be measurementId. gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings); + } else if (command === GtagCommand.GET) { + gtagCore( + GtagCommand.GET, + idOrNameOrParams as string, + gtagParams as string, + callback as (fieldName: string) => void + ); } else { // If SET, second arg must be params. gtagCore(GtagCommand.SET, idOrNameOrParams as CustomParams); diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts index 9ec0a9e79ab..e1123b49881 100644 --- a/packages/analytics/src/types.ts +++ b/packages/analytics/src/types.ts @@ -73,6 +73,12 @@ export interface Gtag { subCommand: 'default' | 'update', consentSettings: ConsentSettings ): void; + ( + command: 'get', + targetId: string, + fieldName: string, + callback: (s: string) => void + ): void; } export type DataLayer = IArguments[]; From 156137e439c643c4d0fad2b29415a5c37d64473f Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Sun, 26 Mar 2023 19:34:29 -0400 Subject: [PATCH 02/23] Update docs devsite --- docs-devsite/analytics.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs-devsite/analytics.md b/docs-devsite/analytics.md index bd6e4b69351..6b72ce80efe 100644 --- a/docs-devsite/analytics.md +++ b/docs-devsite/analytics.md @@ -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. | | function(analyticsInstance...) | +| [getGoogleAnalyticsClientId(analyticsInstance)](./analytics.md#getgoogleanalyticsclientid) | Retrieves a unique identifier for the web client. | | [logEvent(analyticsInstance, eventName, eventParams, options)](./analytics.md#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.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 eventParams. 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 eventParams. 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 [Analytics](./analytics.analytics.md#analytics_interface) +## getGoogleAnalyticsClientId() + +Retrieves a unique identifier for the web client. + +Signature: + +```typescript +export declare function getGoogleAnalyticsClientId(analyticsInstance: Analytics): Promise; +``` + +### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| analyticsInstance | [Analytics](./analytics.analytics.md#analytics_interface) | | + +Returns: + +Promise<string> + ## 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. From 510a89ae29a2f68a660c7c1cf11915dde6424078 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Sun, 26 Mar 2023 19:45:39 -0400 Subject: [PATCH 03/23] Add checkset --- .changeset/silent-islands-fix.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/silent-islands-fix.md diff --git a/.changeset/silent-islands-fix.md b/.changeset/silent-islands-fix.md new file mode 100644 index 00000000000..91f64abc6bc --- /dev/null +++ b/.changeset/silent-islands-fix.md @@ -0,0 +1,6 @@ +--- +'@firebase/analytics': minor +'firebase': minor +--- + +Add method `getGoogleAnalyticsClientId()` to retrieve an unique identifier for a web client. Users wanted 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 simply this event recording process. From 41ef1bf79846d9960a8f727730d4f47c727f1148 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Mon, 27 Mar 2023 18:12:59 -0400 Subject: [PATCH 04/23] Update changeset description --- .changeset/silent-islands-fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silent-islands-fix.md b/.changeset/silent-islands-fix.md index 91f64abc6bc..5737bfd3ac0 100644 --- a/.changeset/silent-islands-fix.md +++ b/.changeset/silent-islands-fix.md @@ -3,4 +3,4 @@ 'firebase': minor --- -Add method `getGoogleAnalyticsClientId()` to retrieve an unique identifier for a web client. Users wanted 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 simply this event recording process. +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 simply this event recording process. From 8f6ac073f404e408831f2b18c3f51edc99021b22 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Fri, 31 Mar 2023 12:21:35 -0400 Subject: [PATCH 05/23] Add link to client_id in docstring --- packages/analytics/src/api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index 5a7867288e7..da914394c46 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -168,7 +168,8 @@ export function setCurrentScreen( } /** - * Retrieves a unique identifier for the web client. + * 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 * From 3e401cb1a272a19a4fcba217dc3b4f7d75cca49c Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Sat, 8 Apr 2023 16:56:24 -0400 Subject: [PATCH 06/23] Update gtagWrapper to take variable number of args for potential fallthrough case --- packages/analytics/src/api.ts | 2 +- packages/analytics/src/helpers.test.ts | 13 +++++++++++++ packages/analytics/src/helpers.ts | 27 +++++++++++++++----------- packages/analytics/src/types.ts | 4 +++- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index da914394c46..faff2a765a2 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -189,7 +189,7 @@ export async function getGoogleAnalyticsClientId( GtagCommand.GET, measurementId, 'client_id', - fieldName => { + (fieldName: string) => { if (!fieldName) { reject('There was an issue retrieving the `client_id`'); } diff --git a/packages/analytics/src/helpers.test.ts b/packages/analytics/src/helpers.test.ts index 7b17dfa9d46..dafe9272ad0 100644 --- a/packages/analytics/src/helpers.test.ts +++ b/packages/analytics/src/helpers.test.ts @@ -281,6 +281,19 @@ describe('Gtag wrapping functions', () => { 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(); wrapOrCreateGtag( diff --git a/packages/analytics/src/helpers.ts b/packages/analytics/src/helpers.ts index e21e3744bde..7e35fd7720d 100644 --- a/packages/analytics/src/helpers.ts +++ b/packages/analytics/src/helpers.ts @@ -227,45 +227,50 @@ function wrapGtag( * @param gtagParams Params if event is EVENT/CONFIG. */ async function gtagWrapper( - command: 'config' | 'set' | 'event' | 'consent' | 'get', - idOrNameOrParams: string | ControlParams, - gtagParams?: GtagConfigOrEventParams | ConsentSettings | string, - callback?: (s: string) => void + command: 'config' | 'set' | 'event' | 'consent' | 'get' | string, + ...args: unknown[] ): Promise { 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. gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings); } else if (command === GtagCommand.GET) { + const [targetId, fieldName, callback] = args; gtagCore( GtagCommand.GET, - idOrNameOrParams as string, - gtagParams as string, - callback as (fieldName: string) => void + targetId as string, + fieldName as string, + callback as (...args: unknown[]) => void ); - } else { + } 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); diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts index e1123b49881..ccc4f2721b1 100644 --- a/packages/analytics/src/types.ts +++ b/packages/analytics/src/types.ts @@ -53,6 +53,7 @@ export interface MinimalDynamicConfig { measurementId: string; } +// We can kill this interface as it'll all be unknown /** * Standard `gtag` function provided by gtag.js. */ @@ -77,8 +78,9 @@ export interface Gtag { command: 'get', targetId: string, fieldName: string, - callback: (s: string) => void + callback: (...args: unknown[]) => void ): void; + (command: string, ...args: unknown[]): void; } export type DataLayer = IArguments[]; From 4c93e5325e35aab6d5fbd0b8517374a03fa34102 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Mon, 10 Apr 2023 15:11:56 -0400 Subject: [PATCH 07/23] Add API test for getGoogleAnalyticsClientId --- packages/analytics/src/api.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/analytics/src/api.test.ts b/packages/analytics/src/api.test.ts index eacaf3ee811..66ad699dc21 100644 --- a/packages/analytics/src/api.test.ts +++ b/packages/analytics/src/api.test.ts @@ -21,6 +21,7 @@ import '../testing/setup'; import { getFullApp } from '../testing/get-fake-firebase-services'; import { getAnalytics, + getGoogleAnalyticsClientId, initializeAnalytics, setConsent, setDefaultEventParameters @@ -35,6 +36,8 @@ import { defaultEventParametersForInit } from './functions'; import { ConsentSettings } from './public-types'; +import { GtagCommand } from './constants'; +import * as util from '@firebase/util'; describe('FirebaseAnalytics API tests', () => { let initStub: SinonStub = stub(); @@ -154,4 +157,19 @@ describe('FirebaseAnalytics API tests', () => { consentParametersForInit ); }); + it('getGoogleAnalyticsClientId() calls gtag "get" through wrappedGtagFunction', () => { + stub(factory, 'wrappedGtagFunction').get(() => wrappedGtag); + stub(util, 'getModularInstance').returns({ + app: { options: { measurementId: 'measurement-id-1' } } + }); + app = getFullApp({ ...fakeAppParams, measurementId: 'measurement-id-1' }); + const analyticsInstance = initializeAnalytics(app); + const id = getGoogleAnalyticsClientId(analyticsInstance); + console.log({ didWeGetTheId: id }); + expect(wrappedGtag).to.have.been.calledWith( + GtagCommand.GET, + 'measurement-id-1', + 'client_id' + ); + }); }); From c4b9792836e5adc8928cec26fafb71bf66f44114 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Mon, 10 Apr 2023 20:45:55 -0400 Subject: [PATCH 08/23] Move API functionality to internal function --- packages/analytics/src/api.test.ts | 17 ----------- packages/analytics/src/api.ts | 30 +++++-------------- packages/analytics/src/functions.test.ts | 34 +++++++++++++++++++++- packages/analytics/src/functions.ts | 37 ++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 41 deletions(-) diff --git a/packages/analytics/src/api.test.ts b/packages/analytics/src/api.test.ts index 66ad699dc21..36c90776fec 100644 --- a/packages/analytics/src/api.test.ts +++ b/packages/analytics/src/api.test.ts @@ -36,8 +36,6 @@ import { defaultEventParametersForInit } from './functions'; import { ConsentSettings } from './public-types'; -import { GtagCommand } from './constants'; -import * as util from '@firebase/util'; describe('FirebaseAnalytics API tests', () => { let initStub: SinonStub = stub(); @@ -157,19 +155,4 @@ describe('FirebaseAnalytics API tests', () => { consentParametersForInit ); }); - it('getGoogleAnalyticsClientId() calls gtag "get" through wrappedGtagFunction', () => { - stub(factory, 'wrappedGtagFunction').get(() => wrappedGtag); - stub(util, 'getModularInstance').returns({ - app: { options: { measurementId: 'measurement-id-1' } } - }); - app = getFullApp({ ...fakeAppParams, measurementId: 'measurement-id-1' }); - const analyticsInstance = initializeAnalytics(app); - const id = getGoogleAnalyticsClientId(analyticsInstance); - console.log({ didWeGetTheId: id }); - expect(wrappedGtag).to.have.been.calledWith( - GtagCommand.GET, - 'measurement-id-1', - 'client_id' - ); - }); }); diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index faff2a765a2..4de2df565f4 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -50,7 +50,8 @@ import { setUserProperties as internalSetUserProperties, setAnalyticsCollectionEnabled as internalSetAnalyticsCollectionEnabled, _setConsentDefaultForInit, - _setDefaultEventParametersForInit + _setDefaultEventParametersForInit, + internalGetGoogleAnalyticsClientId } from './functions'; import { ERROR_FACTORY, AnalyticsError } from './errors'; @@ -175,31 +176,14 @@ export function setCurrentScreen( * * @param app - The {@link @firebase/app#FirebaseApp} to use. */ -export async function getGoogleAnalyticsClientId( +export function getGoogleAnalyticsClientId( analyticsInstance: Analytics ): Promise { analyticsInstance = getModularInstance(analyticsInstance); - const measurementId = analyticsInstance.app.options.measurementId; - let clientId = ''; - if (!measurementId) { - logger.error('The app has no recognizable measurement ID.'); - } else { - clientId = await new Promise((resolve, reject) => { - wrappedGtagFunction( - GtagCommand.GET, - measurementId, - 'client_id', - (fieldName: string) => { - if (!fieldName) { - reject('There was an issue retrieving the `client_id`'); - } - resolve(fieldName); - } - ); - }); - } - - return clientId; + return internalGetGoogleAnalyticsClientId( + wrappedGtagFunction, + initializationPromisesMap[analyticsInstance.app.options.measurementId!] + ); } /** diff --git a/packages/analytics/src/functions.test.ts b/packages/analytics/src/functions.test.ts index 4deca1a9401..6e53e8911c8 100644 --- a/packages/analytics/src/functions.test.ts +++ b/packages/analytics/src/functions.test.ts @@ -27,10 +27,12 @@ import { defaultEventParametersForInit, _setDefaultEventParametersForInit, _setConsentDefaultForInit, - defaultConsentSettingsForInit + defaultConsentSettingsForInit, + internalGetGoogleAnalyticsClientId } from './functions'; import { GtagCommand } from './constants'; import { ConsentSettings } from './public-types'; +import { Gtag } from './types'; const fakeMeasurementId = 'abcd-efgh-ijkl'; const fakeInitializationPromise = Promise.resolve(fakeMeasurementId); @@ -238,4 +240,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: (fieldName: string) => {} + ): void { + callBackStub(''); + } as Gtag, + fakeInitializationPromise + ) + ).to.be.rejectedWith('There was an issue retrieving the `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: (fieldName: string) => {} + ): void { + callBackStub(CLIENT_ID); + } as Gtag, + fakeInitializationPromise + ); + expect(id).to.equal(CLIENT_ID); + }); }); diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index 0b3b15f68c1..08ba98e4fe8 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -24,6 +24,7 @@ import { } from './public-types'; import { Gtag } from './types'; import { GtagCommand } from './constants'; +import { logger } from './logger'; /** * Event parameters to set on 'gtag' during initialization. @@ -137,6 +138,42 @@ 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 + * + * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event + * @param properties Map of user properties to set + */ +export async function internalGetGoogleAnalyticsClientId( + gtagFunction: Gtag, + initializationPromise: Promise +): Promise { + const measurementId = await initializationPromise; + let clientId = ''; + if (!measurementId) { + logger.error('The app has no recognizable measurement ID.'); + } else { + clientId = await new Promise((resolve, reject) => { + gtagFunction( + GtagCommand.GET, + measurementId, + 'client_id', + (fieldName: string) => { + console.log('inside the callback'); + if (!fieldName) { + reject('There was an issue retrieving the `client_id`'); + } + resolve(fieldName); + } + ); + }); + } + return clientId; +} + /** * Set whether collection is enabled for this ID. * From 54d83aa18cb0678e15586ac3b2cf7c64b3a119ce Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Tue, 11 Apr 2023 00:31:32 -0400 Subject: [PATCH 09/23] Update docs for devsite --- docs-devsite/analytics.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs-devsite/analytics.md b/docs-devsite/analytics.md index 6b72ce80efe..a79e6f005d9 100644 --- a/docs-devsite/analytics.md +++ b/docs-devsite/analytics.md @@ -20,7 +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. | | function(analyticsInstance...) | -| [getGoogleAnalyticsClientId(analyticsInstance)](./analytics.md#getgoogleanalyticsclientid) | Retrieves a unique identifier for the web client. | +| [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 eventParams. 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 eventParams. 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 eventParams. 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). | @@ -124,7 +124,7 @@ export declare function initializeAnalytics(app: FirebaseApp, options?: Analytic ## getGoogleAnalyticsClientId() -Retrieves a unique identifier for the web client. +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). Signature: From 2ecd32db44ee15ce67ca2ddba0cb4672d6f86870 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Tue, 11 Apr 2023 13:28:51 -0400 Subject: [PATCH 10/23] Removed unused function in test --- packages/analytics/src/api.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics/src/api.test.ts b/packages/analytics/src/api.test.ts index 36c90776fec..eacaf3ee811 100644 --- a/packages/analytics/src/api.test.ts +++ b/packages/analytics/src/api.test.ts @@ -21,7 +21,6 @@ import '../testing/setup'; import { getFullApp } from '../testing/get-fake-firebase-services'; import { getAnalytics, - getGoogleAnalyticsClientId, initializeAnalytics, setConsent, setDefaultEventParameters From 9f5cfb5bbbb5f8421099a883855b3fcce8c4c674 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Tue, 11 Apr 2023 13:41:40 -0400 Subject: [PATCH 11/23] Update public api with async keyword --- packages/analytics/src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index 4de2df565f4..683e1d18862 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -176,7 +176,7 @@ export function setCurrentScreen( * * @param app - The {@link @firebase/app#FirebaseApp} to use. */ -export function getGoogleAnalyticsClientId( +export async function getGoogleAnalyticsClientId( analyticsInstance: Analytics ): Promise { analyticsInstance = getModularInstance(analyticsInstance); From c6d410911b19f37b25335430bf8d4b6bc2432a3e Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Tue, 11 Apr 2023 23:31:02 -0400 Subject: [PATCH 12/23] Remove comment --- packages/analytics/src/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts index ccc4f2721b1..5d4d34b0357 100644 --- a/packages/analytics/src/types.ts +++ b/packages/analytics/src/types.ts @@ -53,7 +53,6 @@ export interface MinimalDynamicConfig { measurementId: string; } -// We can kill this interface as it'll all be unknown /** * Standard `gtag` function provided by gtag.js. */ From 5516b49d19f45b5453ad27a3b29883053a085d42 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 12 Apr 2023 13:50:25 -0400 Subject: [PATCH 13/23] Update doc string --- packages/analytics/src/functions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index 08ba98e4fe8..49628b0a2d4 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -145,7 +145,6 @@ export async function setUserProperties( * @public * * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event - * @param properties Map of user properties to set */ export async function internalGetGoogleAnalyticsClientId( gtagFunction: Gtag, From 6eef67cc83b366253438025790f20bfcdbc3867f Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 12 Apr 2023 14:04:46 -0400 Subject: [PATCH 14/23] Update grammar of changeset --- .changeset/silent-islands-fix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silent-islands-fix.md b/.changeset/silent-islands-fix.md index 5737bfd3ac0..b7403ef1284 100644 --- a/.changeset/silent-islands-fix.md +++ b/.changeset/silent-islands-fix.md @@ -3,4 +3,4 @@ '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 simply this event recording process. +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. From eca9407a3476233d40893b502cb446f08cdee07d Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 12 Apr 2023 14:07:16 -0400 Subject: [PATCH 15/23] Remove console.log --- packages/analytics/src/functions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index 49628b0a2d4..36095a9bd03 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -161,7 +161,6 @@ export async function internalGetGoogleAnalyticsClientId( measurementId, 'client_id', (fieldName: string) => { - console.log('inside the callback'); if (!fieldName) { reject('There was an issue retrieving the `client_id`'); } From 0e3b90080470829eb262e21daad4e3d4bdb65e40 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 12 Apr 2023 14:43:11 -0400 Subject: [PATCH 16/23] Update variable name from targetId to measurementId --- packages/analytics/src/helpers.ts | 4 ++-- packages/analytics/src/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/analytics/src/helpers.ts b/packages/analytics/src/helpers.ts index 7e35fd7720d..fa099a4c454 100644 --- a/packages/analytics/src/helpers.ts +++ b/packages/analytics/src/helpers.ts @@ -258,10 +258,10 @@ function wrapGtag( // If CONFIG, second arg must be measurementId. gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings); } else if (command === GtagCommand.GET) { - const [targetId, fieldName, callback] = args; + const [measurementId, fieldName, callback] = args; gtagCore( GtagCommand.GET, - targetId as string, + measurementId as string, fieldName as string, callback as (...args: unknown[]) => void ); diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts index 5d4d34b0357..9d3db8112f0 100644 --- a/packages/analytics/src/types.ts +++ b/packages/analytics/src/types.ts @@ -75,7 +75,7 @@ export interface Gtag { ): void; ( command: 'get', - targetId: string, + measurementId: string, fieldName: string, callback: (...args: unknown[]) => void ): void; From 0ec1f27e55b12e146b54530cd0be1a302deec7c5 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 12 Apr 2023 14:47:40 -0400 Subject: [PATCH 17/23] Removed check for blank measurementId --- packages/analytics/src/functions.ts | 31 +++++++++++------------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index 36095a9bd03..5478f1587e4 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -24,7 +24,6 @@ import { } from './public-types'; import { Gtag } from './types'; import { GtagCommand } from './constants'; -import { logger } from './logger'; /** * Event parameters to set on 'gtag' during initialization. @@ -151,25 +150,19 @@ export async function internalGetGoogleAnalyticsClientId( initializationPromise: Promise ): Promise { const measurementId = await initializationPromise; - let clientId = ''; - if (!measurementId) { - logger.error('The app has no recognizable measurement ID.'); - } else { - clientId = await new Promise((resolve, reject) => { - gtagFunction( - GtagCommand.GET, - measurementId, - 'client_id', - (fieldName: string) => { - if (!fieldName) { - reject('There was an issue retrieving the `client_id`'); - } - resolve(fieldName); + return new Promise((resolve, reject) => { + gtagFunction( + GtagCommand.GET, + measurementId, + 'client_id', + (fieldName: string) => { + if (!fieldName) { + reject('There was an issue retrieving the `client_id`'); } - ); - }); - } - return clientId; + resolve(fieldName); + } + ); + }); } /** From a585a16177848cccf2661fac966f80ff73982e60 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Thu, 13 Apr 2023 13:30:31 -0400 Subject: [PATCH 18/23] Add ERROR_FACTORY for promise rejection --- packages/analytics/src/errors.ts | 6 ++++-- packages/analytics/src/functions.test.ts | 3 ++- packages/analytics/src/functions.ts | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/analytics/src/errors.ts b/packages/analytics/src/errors.ts index 98293447c65..80443061357 100644 --- a/packages/analytics/src/errors.ts +++ b/packages/analytics/src/errors.ts @@ -27,7 +27,8 @@ export const enum AnalyticsError { FETCH_THROTTLE = 'fetch-throttle', CONFIG_FETCH_FAILED = 'config-fetch-failed', NO_API_KEY = 'no-api-key', - NO_APP_ID = 'no-app-id' + NO_APP_ID = 'no-app-id', + NO_CLIENT_ID = 'no-client-id' } const ERRORS: ErrorMap = { @@ -64,7 +65,8 @@ const ERRORS: ErrorMap = { 'contain a valid API key.', [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.' + 'contain a valid app ID.', + [AnalyticsError.NO_CLIENT_ID]: 'The "client_id" field is empty or falsy.' }; interface ErrorParams { diff --git a/packages/analytics/src/functions.test.ts b/packages/analytics/src/functions.test.ts index 6e53e8911c8..d989f4c2ceb 100644 --- a/packages/analytics/src/functions.test.ts +++ b/packages/analytics/src/functions.test.ts @@ -33,6 +33,7 @@ import { 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); @@ -253,7 +254,7 @@ describe('FirebaseAnalytics methods', () => { } as Gtag, fakeInitializationPromise ) - ).to.be.rejectedWith('There was an issue retrieving the `client_id`'); + ).to.be.rejectedWith(AnalyticsError.NO_CLIENT_ID); }); it('internalGetGoogleAnalyticsClientId() returns client_id when available', async () => { const CLIENT_ID = 'clientId1234'; diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index 5478f1587e4..d790ef44e15 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -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. @@ -157,7 +158,7 @@ export async function internalGetGoogleAnalyticsClientId( 'client_id', (fieldName: string) => { if (!fieldName) { - reject('There was an issue retrieving the `client_id`'); + reject(ERROR_FACTORY.create(AnalyticsError.NO_CLIENT_ID)); } resolve(fieldName); } From 953e499872f7f017ed32d3eb14c5347f9520aa22 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Thu, 13 Apr 2023 15:27:11 -0400 Subject: [PATCH 19/23] Change fieldName from clientId --- packages/analytics/src/functions.test.ts | 4 ++-- packages/analytics/src/functions.ts | 6 +++--- packages/analytics/src/helpers.test.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/analytics/src/functions.test.ts b/packages/analytics/src/functions.test.ts index d989f4c2ceb..1e4ee4a569f 100644 --- a/packages/analytics/src/functions.test.ts +++ b/packages/analytics/src/functions.test.ts @@ -248,7 +248,7 @@ describe('FirebaseAnalytics methods', () => { unused1: unknown, unused2: unknown, unused3: unknown, - callBackStub: (fieldName: string) => {} + callBackStub: (clientId: string) => {} ): void { callBackStub(''); } as Gtag, @@ -263,7 +263,7 @@ describe('FirebaseAnalytics methods', () => { unused1: unknown, unused2: unknown, unused3: unknown, - callBackStub: (fieldName: string) => {} + callBackStub: (clientId: string) => {} ): void { callBackStub(CLIENT_ID); } as Gtag, diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index d790ef44e15..7f5d3b337ed 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -156,11 +156,11 @@ export async function internalGetGoogleAnalyticsClientId( GtagCommand.GET, measurementId, 'client_id', - (fieldName: string) => { - if (!fieldName) { + (clientId: string) => { + if (!clientId) { reject(ERROR_FACTORY.create(AnalyticsError.NO_CLIENT_ID)); } - resolve(fieldName); + resolve(clientId); } ); }); diff --git a/packages/analytics/src/helpers.test.ts b/packages/analytics/src/helpers.test.ts index dafe9272ad0..855127590db 100644 --- a/packages/analytics/src/helpers.test.ts +++ b/packages/analytics/src/helpers.test.ts @@ -276,7 +276,7 @@ describe('Gtag wrapping functions', () => { GtagCommand.GET, fakeMeasurementId, 'client_id', - fieldName => console.log(fieldName) + clientId => console.log(clientId) ); expect((window['dataLayer'] as DataLayer).length).to.equal(1); }); From 7149472d36e7be797221e36d91eeedadc39a7443 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Thu, 13 Apr 2023 15:59:22 -0400 Subject: [PATCH 20/23] Update AnalyticsError.NO_CLIENT_ID message --- packages/analytics/src/errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/analytics/src/errors.ts b/packages/analytics/src/errors.ts index 80443061357..5c4d4bf0ca9 100644 --- a/packages/analytics/src/errors.ts +++ b/packages/analytics/src/errors.ts @@ -66,7 +66,7 @@ const ERRORS: ErrorMap = { [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 or falsy.' + [AnalyticsError.NO_CLIENT_ID]: 'The "client_id" field is empty.' }; interface ErrorParams { From 833216262ccb11c975cbfecf8cae6e81f4ccec81 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Tue, 18 Apr 2023 13:27:31 -0400 Subject: [PATCH 21/23] Testing adding comment --- packages/analytics/src/api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index 683e1d18862..2a98176fbd2 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -182,7 +182,8 @@ export async function getGoogleAnalyticsClientId( analyticsInstance = getModularInstance(analyticsInstance); return internalGetGoogleAnalyticsClientId( wrappedGtagFunction, - initializationPromisesMap[analyticsInstance.app.options.measurementId!] + // test + initializationPromisesMap[analyticsInstance.app.options.appId!] ); } From 888aba41150885f0bd1a7dfb872762af8307da09 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Tue, 18 Apr 2023 13:28:57 -0400 Subject: [PATCH 22/23] remove test comment --- packages/analytics/src/api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index 2a98176fbd2..f44cbad2de9 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -182,7 +182,6 @@ export async function getGoogleAnalyticsClientId( analyticsInstance = getModularInstance(analyticsInstance); return internalGetGoogleAnalyticsClientId( wrappedGtagFunction, - // test initializationPromisesMap[analyticsInstance.app.options.appId!] ); } From 9357f51218c4fa206b7932b23aa7a2d219399560 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Tue, 18 Apr 2023 15:02:27 -0400 Subject: [PATCH 23/23] Remove comments --- packages/analytics/src/functions.ts | 2 -- packages/analytics/src/helpers.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index 7f5d3b337ed..dd5062aa548 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -142,8 +142,6 @@ 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 - * * @param gtagFunction Wrapped gtag function that waits for fid to be set before sending an event */ export async function internalGetGoogleAnalyticsClientId( diff --git a/packages/analytics/src/helpers.ts b/packages/analytics/src/helpers.ts index caef1f11782..7f9582900c2 100644 --- a/packages/analytics/src/helpers.ts +++ b/packages/analytics/src/helpers.ts @@ -305,7 +305,6 @@ function wrapGtag( ); } else if (command === GtagCommand.CONSENT) { const [gtagParams] = args; - // If CONFIG, second arg must be measurementId. gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings); } else if (command === GtagCommand.GET) { const [measurementId, fieldName, callback] = args;