From 1682a4176e40330f5624fc6822e9050c022db3c0 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 22 Jun 2022 12:04:26 -0400 Subject: [PATCH 01/11] Add initial draft of setConsent logic --- packages/analytics/src/api.ts | 24 +++++++++++++++-- packages/analytics/src/constants.ts | 3 ++- packages/analytics/src/functions.ts | 27 ++++++++++++++++++- .../analytics/src/initialize-analytics.ts | 6 +++++ packages/analytics/src/public-types.ts | 10 +++++++ packages/analytics/src/types.ts | 15 ++++++++++- 6 files changed, 80 insertions(+), 5 deletions(-) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index 41a5f2166a3..931936f5a1c 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -22,6 +22,7 @@ import { Analytics, AnalyticsCallOptions, AnalyticsSettings, + ConsentSettings, CustomParams, EventNameString, EventParams @@ -35,7 +36,7 @@ import { getModularInstance, deepEqual } from '@firebase/util'; -import { ANALYTICS_TYPE } from './constants'; +import { ANALYTICS_TYPE, GtagCommand } from './constants'; import { AnalyticsService, initializationPromisesMap, @@ -47,7 +48,8 @@ import { setCurrentScreen as internalSetCurrentScreen, setUserId as internalSetUserId, setUserProperties as internalSetUserProperties, - setAnalyticsCollectionEnabled as internalSetAnalyticsCollectionEnabled + setAnalyticsCollectionEnabled as internalSetAnalyticsCollectionEnabled, + _setConsentDefaultForInit } from './functions'; import { ERROR_FACTORY, AnalyticsError } from './errors'; @@ -716,3 +718,21 @@ export function logEvent( * @public */ export type CustomEventName = T extends EventNameString ? never : T; + +/** + * Sets the applicable end user consent state for this web app across all gtag references once + * Firebase Analytics is initialized. + * + * Use the {@link ConsentSettings} to specify individual consent type values. By default consent + * types are set to "granted". + * + * @param consentSettings Maps the applicable end user consent state for gtag.js. + */ +export function setConsent(consentSettings: ConsentSettings): void { + // Check if reference to existing gtag function on window object exists + if (wrappedGtagFunction) { + wrappedGtagFunction(GtagCommand.CONSENT, 'update', consentSettings); + } else { + _setConsentDefaultForInit(consentSettings); + } +} diff --git a/packages/analytics/src/constants.ts b/packages/analytics/src/constants.ts index 6697466c8aa..7f3da181288 100644 --- a/packages/analytics/src/constants.ts +++ b/packages/analytics/src/constants.ts @@ -34,5 +34,6 @@ export const GTAG_URL = 'https://www.googletagmanager.com/gtag/js'; export const enum GtagCommand { EVENT = 'event', SET = 'set', - CONFIG = 'config' + CONFIG = 'config', + CONSENT = 'consent' } diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index f35b61d98cd..2b891705723 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -19,7 +19,8 @@ import { AnalyticsCallOptions, CustomParams, ControlParams, - EventParams + EventParams, + ConsentSettings } from './public-types'; import { Gtag } from './types'; import { GtagCommand } from './constants'; @@ -142,3 +143,27 @@ export async function setAnalyticsCollectionEnabled( const measurementId = await initializationPromise; window[`ga-disable-${measurementId}`] = !enabled; } + +/** + * Consent parameters to default to during 'gtag' initialization. + */ +export let defaultConsentSettingsForInit: ConsentSettings | undefined; + +/** + * Sets the variable {@link defaultConsentSettingsForInit} for use in the initialization of + * analytics. + * + * @param consentSettings Maps the applicable end user consent state for gtag.js. + */ +export function _setConsentDefaultForInit( + consentSettings: ConsentSettings +): void { + if (defaultConsentSettingsForInit) { + defaultConsentSettingsForInit = { + ...defaultConsentSettingsForInit, + ...consentSettings + }; + } else { + defaultConsentSettingsForInit = consentSettings; + } +} diff --git a/packages/analytics/src/initialize-analytics.ts b/packages/analytics/src/initialize-analytics.ts index e38f939b3d7..02da3e7031e 100644 --- a/packages/analytics/src/initialize-analytics.ts +++ b/packages/analytics/src/initialize-analytics.ts @@ -28,6 +28,7 @@ import { import { ERROR_FACTORY, AnalyticsError } from './errors'; import { findGtagScriptOnPage, insertScriptTag } from './helpers'; import { AnalyticsSettings } from './public-types'; +import { defaultConsentSettingsForInit } from './functions'; async function validateIndexedDB(): Promise { if (!isIndexedDBAvailable()) { @@ -118,6 +119,11 @@ export async function _initializeAnalytics( insertScriptTag(dataLayerName, dynamicConfig.measurementId); } + // Detects if there are consent settings that need to be configured. + if (defaultConsentSettingsForInit) { + gtagCore(GtagCommand.CONSENT, 'default', defaultConsentSettingsForInit); + } + // This command initializes gtag.js and only needs to be called once for the entire web app, // but since it is idempotent, we can call it multiple times. // We keep it together with other initialization logic for better code structure. diff --git a/packages/analytics/src/public-types.ts b/packages/analytics/src/public-types.ts index 9e0f479a3a9..55c602f69a0 100644 --- a/packages/analytics/src/public-types.ts +++ b/packages/analytics/src/public-types.ts @@ -288,4 +288,14 @@ export interface EventParams { page_path?: string; [key: string]: unknown; } + +/** Maps the applicable end user consent state. */ +export interface ConsentSettings { + ad_storage?: ConsentStatusString; + analytics_storage?: ConsentStatusString; +} + /* eslint-enable camelcase */ + +/** Whether a particular consent type has been granted or denied. */ +export type ConsentStatusString = 'granted' | 'denied'; diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts index b3f6b8cf116..a88639a54c0 100644 --- a/packages/analytics/src/types.ts +++ b/packages/analytics/src/types.ts @@ -15,7 +15,12 @@ * limitations under the License. */ -import { ControlParams, EventParams, CustomParams } from './public-types'; +import { + ControlParams, + EventParams, + CustomParams, + ConsentSettings +} from './public-types'; /** * Encapsulates metadata concerning throttled fetch requests. @@ -63,6 +68,14 @@ export interface Gtag { eventName: string, eventParams?: ControlParams | EventParams | CustomParams ): void; + ( + command: 'consent', + // TODO(dwyfrequency) should I make these subCommands their own type + subCommand: 'default' | 'update', + // TODO(dwyfrequency) should this be optional like eventParams and config. + // Idk why we would want it as optional + consentSettings: ConsentSettings + ): void; } export type DataLayer = IArguments[]; From 58aff2e89e7dc27e52bd43434a52bba76328690c Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 22 Jun 2022 13:37:04 -0400 Subject: [PATCH 02/11] Update gtag wrappers to accommodate consent command --- packages/analytics/src/helpers.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/analytics/src/helpers.ts b/packages/analytics/src/helpers.ts index d8171f9c0f5..19ad70fd1cd 100644 --- a/packages/analytics/src/helpers.ts +++ b/packages/analytics/src/helpers.ts @@ -15,7 +15,12 @@ * limitations under the License. */ -import { CustomParams, ControlParams, EventParams } from './public-types'; +import { + CustomParams, + ControlParams, + EventParams, + ConsentSettings +} from './public-types'; import { DynamicConfig, DataLayer, Gtag, MinimalDynamicConfig } from './types'; import { GtagCommand, GTAG_URL } from './constants'; import { logger } from './logger'; @@ -219,9 +224,10 @@ function wrapGtag( * @param gtagParams Params if event is EVENT/CONFIG. */ async function gtagWrapper( - command: 'config' | 'set' | 'event', + command: 'config' | 'set' | 'event' | 'consent', idOrNameOrParams: string | ControlParams, - gtagParams?: ControlParams & EventParams & CustomParams + // TODO(dwyfrequency)Unsure if this is the best path. + gtagParams?: GtagSetConfigEventParams | ConsentSettings ): Promise { try { // If event, check that relevant initialization promises have completed. @@ -232,7 +238,7 @@ function wrapGtag( initializationPromisesMap, dynamicConfigPromisesList, idOrNameOrParams as string, - gtagParams + gtagParams as GtagSetConfigEventParams ); } else if (command === GtagCommand.CONFIG) { // If CONFIG, second arg must be measurementId. @@ -242,8 +248,11 @@ function wrapGtag( dynamicConfigPromisesList, measurementIdToAppId, idOrNameOrParams as string, - gtagParams + gtagParams as GtagSetConfigEventParams ); + } else if (command === GtagCommand.CONSENT) { + // If CONFIG, second arg must be measurementId. + gtagCore(GtagCommand.CONSENT, 'update', gtagParams as ConsentSettings); } else { // If SET, second arg must be params. gtagCore(GtagCommand.SET, idOrNameOrParams as CustomParams); @@ -254,6 +263,8 @@ function wrapGtag( } return gtagWrapper as Gtag; } +// TODO(dwyfrequency)Unsure if this is the best path and where it should go. Probably the type file +type GtagSetConfigEventParams = ControlParams & EventParams & CustomParams; /** * Creates global gtag function or wraps existing one if found. From d014600fa16ae8be91526b5d712be798a26491b2 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 22 Jun 2022 17:54:38 +0000 Subject: [PATCH 03/11] Update API reports --- common/api-review/analytics.api.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/common/api-review/analytics.api.md b/common/api-review/analytics.api.md index 00d2cac0ce0..d373176bda8 100644 --- a/common/api-review/analytics.api.md +++ b/common/api-review/analytics.api.md @@ -21,6 +21,17 @@ export interface AnalyticsSettings { config?: GtagConfigParams | EventParams; } +// @public +export interface ConsentSettings { + // (undocumented) + ad_storage?: ConsentStatusString; + // (undocumented) + analytics_storage?: ConsentStatusString; +} + +// @public +export type ConsentStatusString = 'granted' | 'denied'; + // @public export interface ControlParams { // (undocumented) @@ -388,6 +399,9 @@ export interface Promotion { // @public export function setAnalyticsCollectionEnabled(analyticsInstance: Analytics, enabled: boolean): void; +// @public +export function setConsent(consentSettings: ConsentSettings): void; + // @public @deprecated export function setCurrentScreen(analyticsInstance: Analytics, screenName: string, options?: AnalyticsCallOptions): void; From f41256c93995f656523ec6d291a54d73c29872a6 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 22 Jun 2022 14:45:37 -0400 Subject: [PATCH 04/11] Add changeset --- .changeset/shiny-bats-reflect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-bats-reflect.md diff --git a/.changeset/shiny-bats-reflect.md b/.changeset/shiny-bats-reflect.md new file mode 100644 index 00000000000..feb7a9a353f --- /dev/null +++ b/.changeset/shiny-bats-reflect.md @@ -0,0 +1,5 @@ +--- +'@firebase/analytics': minor +--- + +Add function `setConsent()` to set the applicable end user "consent" state. From e5a7b73f5891566eb520c970f6af99b0e64193ac Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Thu, 23 Jun 2022 14:55:53 -0400 Subject: [PATCH 05/11] Update types, documentation and functionality --- packages/analytics/src/api.ts | 2 +- packages/analytics/src/functions.ts | 11 ++-------- .../analytics/src/initialize-analytics.ts | 6 +++++- packages/analytics/src/public-types.ts | 20 +++++++++++++++++-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index 931936f5a1c..1be652ce3b3 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -725,7 +725,7 @@ export type CustomEventName = T extends EventNameString ? never : T; * * Use the {@link ConsentSettings} to specify individual consent type values. By default consent * types are set to "granted". - * + * @public * @param consentSettings Maps the applicable end user consent state for gtag.js. */ export function setConsent(consentSettings: ConsentSettings): void { diff --git a/packages/analytics/src/functions.ts b/packages/analytics/src/functions.ts index 2b891705723..e9cafa200a5 100644 --- a/packages/analytics/src/functions.ts +++ b/packages/analytics/src/functions.ts @@ -156,14 +156,7 @@ export let defaultConsentSettingsForInit: ConsentSettings | undefined; * @param consentSettings Maps the applicable end user consent state for gtag.js. */ export function _setConsentDefaultForInit( - consentSettings: ConsentSettings + consentSettings?: ConsentSettings ): void { - if (defaultConsentSettingsForInit) { - defaultConsentSettingsForInit = { - ...defaultConsentSettingsForInit, - ...consentSettings - }; - } else { - defaultConsentSettingsForInit = consentSettings; - } + defaultConsentSettingsForInit = consentSettings; } diff --git a/packages/analytics/src/initialize-analytics.ts b/packages/analytics/src/initialize-analytics.ts index 02da3e7031e..2062ca2ac92 100644 --- a/packages/analytics/src/initialize-analytics.ts +++ b/packages/analytics/src/initialize-analytics.ts @@ -28,7 +28,10 @@ import { import { ERROR_FACTORY, AnalyticsError } from './errors'; import { findGtagScriptOnPage, insertScriptTag } from './helpers'; import { AnalyticsSettings } from './public-types'; -import { defaultConsentSettingsForInit } from './functions'; +import { + defaultConsentSettingsForInit, + _setConsentDefaultForInit +} from './functions'; async function validateIndexedDB(): Promise { if (!isIndexedDBAvailable()) { @@ -122,6 +125,7 @@ export async function _initializeAnalytics( // Detects if there are consent settings that need to be configured. if (defaultConsentSettingsForInit) { gtagCore(GtagCommand.CONSENT, 'default', defaultConsentSettingsForInit); + _setConsentDefaultForInit(undefined); } // This command initializes gtag.js and only needs to be called once for the entire web app, diff --git a/packages/analytics/src/public-types.ts b/packages/analytics/src/public-types.ts index 55c602f69a0..9cb2a8ea5f0 100644 --- a/packages/analytics/src/public-types.ts +++ b/packages/analytics/src/public-types.ts @@ -289,13 +289,29 @@ export interface EventParams { [key: string]: unknown; } -/** Maps the applicable end user consent state. */ +/** + * Consent status settings for each consent type. + * @public + */ export interface ConsentSettings { + // Enables storage, such as cookies, related to advertising ad_storage?: ConsentStatusString; + // Enables storage, such as cookies, related to analytics (for example, visit duration) analytics_storage?: ConsentStatusString; + // Enables storage that supports the functionality of the website or app such as language settings + functionality_storage?: ConsentStatusString; + // Enables storage related to personalization such as video recommendations + personalization_storage?: ConsentStatusString; + // Enables storage related to security such as authentication functionality, fraud prevention, + // and other user protection + security_storage?: ConsentStatusString; + [key: string]: unknown; } /* eslint-enable camelcase */ -/** Whether a particular consent type has been granted or denied. */ +/** + * Whether a particular consent type has been granted or denied. + * @public + */ export type ConsentStatusString = 'granted' | 'denied'; From 069d0c87c5b1386ee155b08e26a0193b26e79b7a Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Thu, 23 Jun 2022 19:13:33 +0000 Subject: [PATCH 06/11] Update API reports --- common/api-review/analytics.api.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common/api-review/analytics.api.md b/common/api-review/analytics.api.md index d373176bda8..08bc08de542 100644 --- a/common/api-review/analytics.api.md +++ b/common/api-review/analytics.api.md @@ -23,10 +23,18 @@ export interface AnalyticsSettings { // @public export interface ConsentSettings { + // (undocumented) + [key: string]: unknown; // (undocumented) ad_storage?: ConsentStatusString; // (undocumented) analytics_storage?: ConsentStatusString; + // (undocumented) + functionality_storage?: ConsentStatusString; + // (undocumented) + personalization_storage?: ConsentStatusString; + // (undocumented) + security_storage?: ConsentStatusString; } // @public From 767421e9d149628b785b21d1ba965f651828c903 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Fri, 24 Jun 2022 11:27:36 -0400 Subject: [PATCH 07/11] Update comments and rename type --- packages/analytics/src/helpers.ts | 12 ++++++------ packages/analytics/src/types.ts | 3 --- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/analytics/src/helpers.ts b/packages/analytics/src/helpers.ts index 19ad70fd1cd..be713c2c8a0 100644 --- a/packages/analytics/src/helpers.ts +++ b/packages/analytics/src/helpers.ts @@ -25,6 +25,9 @@ import { DynamicConfig, DataLayer, Gtag, MinimalDynamicConfig } from './types'; import { GtagCommand, GTAG_URL } from './constants'; import { logger } from './logger'; +// Possible parameter types for gtag 'event' and 'config' commands +type GtagConfigOrEventParams = ControlParams & EventParams & CustomParams; + /** * Makeshift polyfill for Promise.allSettled(). Resolves when all promises * have either resolved or rejected. @@ -226,8 +229,7 @@ function wrapGtag( async function gtagWrapper( command: 'config' | 'set' | 'event' | 'consent', idOrNameOrParams: string | ControlParams, - // TODO(dwyfrequency)Unsure if this is the best path. - gtagParams?: GtagSetConfigEventParams | ConsentSettings + gtagParams?: GtagConfigOrEventParams | ConsentSettings ): Promise { try { // If event, check that relevant initialization promises have completed. @@ -238,7 +240,7 @@ function wrapGtag( initializationPromisesMap, dynamicConfigPromisesList, idOrNameOrParams as string, - gtagParams as GtagSetConfigEventParams + gtagParams as GtagConfigOrEventParams ); } else if (command === GtagCommand.CONFIG) { // If CONFIG, second arg must be measurementId. @@ -248,7 +250,7 @@ function wrapGtag( dynamicConfigPromisesList, measurementIdToAppId, idOrNameOrParams as string, - gtagParams as GtagSetConfigEventParams + gtagParams as GtagConfigOrEventParams ); } else if (command === GtagCommand.CONSENT) { // If CONFIG, second arg must be measurementId. @@ -263,8 +265,6 @@ function wrapGtag( } return gtagWrapper as Gtag; } -// TODO(dwyfrequency)Unsure if this is the best path and where it should go. Probably the type file -type GtagSetConfigEventParams = ControlParams & EventParams & CustomParams; /** * Creates global gtag function or wraps existing one if found. diff --git a/packages/analytics/src/types.ts b/packages/analytics/src/types.ts index a88639a54c0..3b615c1c011 100644 --- a/packages/analytics/src/types.ts +++ b/packages/analytics/src/types.ts @@ -70,10 +70,7 @@ export interface Gtag { ): void; ( command: 'consent', - // TODO(dwyfrequency) should I make these subCommands their own type subCommand: 'default' | 'update', - // TODO(dwyfrequency) should this be optional like eventParams and config. - // Idk why we would want it as optional consentSettings: ConsentSettings ): void; } From 572a151dd89d15d2175a89e85c52b9f1cc05a06f Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Mon, 27 Jun 2022 13:54:09 -0400 Subject: [PATCH 08/11] Add tests and update public type docs. --- packages/analytics/src/api.test.ts | 33 ++++++++++++++++++- packages/analytics/src/functions.test.ts | 27 ++++++++++++++- packages/analytics/src/helpers.test.ts | 13 ++++++-- .../src/initialize-analytics.test.ts | 30 ++++++++++++++++- packages/analytics/src/public-types.ts | 3 ++ 5 files changed, 101 insertions(+), 5 deletions(-) diff --git a/packages/analytics/src/api.test.ts b/packages/analytics/src/api.test.ts index a9a936ea373..eacaf3ee811 100644 --- a/packages/analytics/src/api.test.ts +++ b/packages/analytics/src/api.test.ts @@ -22,6 +22,7 @@ import { getFullApp } from '../testing/get-fake-firebase-services'; import { getAnalytics, initializeAnalytics, + setConsent, setDefaultEventParameters } from './api'; import { FirebaseApp, deleteApp } from '@firebase/app'; @@ -29,7 +30,11 @@ import { AnalyticsError } from './errors'; import * as init from './initialize-analytics'; const fakeAppParams = { appId: 'abcdefgh12345:23405', apiKey: 'AAbbCCdd12345' }; import * as factory from './factory'; -import { defaultEventParametersForInit } from './functions'; +import { + defaultConsentSettingsForInit, + defaultEventParametersForInit +} from './functions'; +import { ConsentSettings } from './public-types'; describe('FirebaseAnalytics API tests', () => { let initStub: SinonStub = stub(); @@ -123,4 +128,30 @@ describe('FirebaseAnalytics API tests', () => { eventParametersForInit ); }); + it('setConsent() updates defaultConsentSettingsForInit if gtag does not exist ', () => { + const consentParametersForInit: ConsentSettings = { + 'analytics_storage': 'granted', + 'functionality_storage': 'denied' + }; + stub(factory, 'wrappedGtagFunction').get(() => undefined); + app = getFullApp(fakeAppParams); + setConsent(consentParametersForInit); + expect(defaultConsentSettingsForInit).to.deep.equal( + consentParametersForInit + ); + }); + it('setConsent() calls gtag consent "update" if wrappedGtagFunction exists', () => { + const consentParametersForInit: ConsentSettings = { + 'analytics_storage': 'granted', + 'functionality_storage': 'denied' + }; + stub(factory, 'wrappedGtagFunction').get(() => wrappedGtag); + app = getFullApp(fakeAppParams); + setConsent(consentParametersForInit); + expect(wrappedGtag).to.have.been.calledWithExactly( + 'consent', + 'update', + consentParametersForInit + ); + }); }); diff --git a/packages/analytics/src/functions.test.ts b/packages/analytics/src/functions.test.ts index 0f806849f72..efce7145ce6 100644 --- a/packages/analytics/src/functions.test.ts +++ b/packages/analytics/src/functions.test.ts @@ -25,9 +25,12 @@ import { setUserProperties, setAnalyticsCollectionEnabled, defaultEventParametersForInit, - _setDefaultEventParametersForInit + _setDefaultEventParametersForInit, + _setConsentDefaultForInit, + defaultConsentSettingsForInit } from './functions'; import { GtagCommand } from './constants'; +import { ConsentSettings } from './public-types'; const fakeMeasurementId = 'abcd-efgh-ijkl'; const fakeInitializationPromise = Promise.resolve(fakeMeasurementId); @@ -192,4 +195,26 @@ describe('FirebaseAnalytics methods', () => { ...additionalParams }); }); + it('_setConsentDefaultForInit() stores individual params correctly', async () => { + const consentParametersForInit: ConsentSettings = { + 'analytics_storage': 'granted', + 'functionality_storage': 'denied' + }; + _setConsentDefaultForInit(consentParametersForInit); + expect(defaultConsentSettingsForInit).to.deep.equal( + consentParametersForInit + ); + }); + it('_setConsentDefaultForInit() replaces previous params with new params', async () => { + const consentParametersForInit: ConsentSettings = { + 'analytics_storage': 'granted', + 'functionality_storage': 'denied' + }; + const additionalParams = { 'wait_for_update': 500 }; + _setConsentDefaultForInit(consentParametersForInit); + _setConsentDefaultForInit(additionalParams); + expect(defaultConsentSettingsForInit).to.deep.equal({ + ...additionalParams + }); + }); }); diff --git a/packages/analytics/src/helpers.test.ts b/packages/analytics/src/helpers.test.ts index 79614f9edf4..5ce43b8c201 100644 --- a/packages/analytics/src/helpers.test.ts +++ b/packages/analytics/src/helpers.test.ts @@ -28,6 +28,7 @@ import { } from './helpers'; import { GtagCommand } from './constants'; import { Deferred } from '@firebase/util'; +import { ConsentSettings } from './public-types'; const fakeMeasurementId = 'abcd-efgh-ijkl'; const fakeAppId = 'my-test-app-1234'; @@ -213,7 +214,11 @@ describe('Gtag wrapping functions', () => { expect((window['dataLayer'] as DataLayer).length).to.equal(1); }); - it('new window.gtag function does not wait when sending "set" calls', async () => { + it('new window.gtag function does not wait when sending "consent" calls', async () => { + const consentParameters: ConsentSettings = { + 'analytics_storage': 'granted', + 'functionality_storage': 'denied' + }; wrapOrCreateGtag( { [fakeAppId]: Promise.resolve(fakeMeasurementId) }, fakeDynamicConfigPromises, @@ -222,7 +227,11 @@ describe('Gtag wrapping functions', () => { 'gtag' ); window['dataLayer'] = []; - (window['gtag'] as Gtag)(GtagCommand.SET, { 'language': 'en' }); + (window['gtag'] as Gtag)( + GtagCommand.CONSENT, + 'update', + consentParameters + ); expect((window['dataLayer'] as DataLayer).length).to.equal(1); }); diff --git a/packages/analytics/src/initialize-analytics.test.ts b/packages/analytics/src/initialize-analytics.test.ts index 2ed8053bdad..da2c8f964b6 100644 --- a/packages/analytics/src/initialize-analytics.test.ts +++ b/packages/analytics/src/initialize-analytics.test.ts @@ -30,7 +30,12 @@ import { Deferred } from '@firebase/util'; import { _FirebaseInstallationsInternal } from '@firebase/installations'; import { removeGtagScript } from '../testing/gtag-script-util'; import { setDefaultEventParameters } from './api'; -import { defaultEventParametersForInit } from './functions'; +import { + defaultConsentSettingsForInit, + defaultEventParametersForInit, + _setConsentDefaultForInit +} from './functions'; +import { ConsentSettings } from './public-types'; const fakeMeasurementId = 'abcd-efgh-ijkl'; const fakeFid = 'fid-1234-zyxw'; @@ -118,6 +123,29 @@ describe('initializeAnalytics()', () => { // defaultEventParametersForInit is reset after initialization. expect(defaultEventParametersForInit).to.equal(undefined); }); + it('calls gtag consent if there are default consent parameters', async () => { + stubFetch(); + const consentParametersForInit: ConsentSettings = { + 'analytics_storage': 'granted', + 'functionality_storage': 'denied' + }; + _setConsentDefaultForInit(consentParametersForInit); + await _initializeAnalytics( + app, + dynamicPromisesList, + measurementIdToAppId, + fakeInstallations, + gtagStub, + 'dataLayer' + ); + expect(gtagStub).to.be.calledWith( + GtagCommand.CONSENT, + 'default', + consentParametersForInit + ); + // defaultEventParametersForInit is reset after initialization. + expect(defaultConsentSettingsForInit).to.equal(undefined); + }); it('puts dynamic fetch promise into dynamic promises list', async () => { stubFetch(); await _initializeAnalytics( diff --git a/packages/analytics/src/public-types.ts b/packages/analytics/src/public-types.ts index 9cb2a8ea5f0..03d162ac692 100644 --- a/packages/analytics/src/public-types.ts +++ b/packages/analytics/src/public-types.ts @@ -291,6 +291,9 @@ export interface EventParams { /** * Consent status settings for each consent type. + * For more information, see + * {@link https://developers.google.com/tag-platform/tag-manager/templates/consent-apis + * | the GA4 reference documentation for consent state and consent types}. * @public */ export interface ConsentSettings { From 05ca0d1350d419c3085294d75996a7b24f9478dc Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Mon, 27 Jun 2022 17:45:53 -0400 Subject: [PATCH 09/11] Add back 'set' test --- packages/analytics/src/helpers.test.ts | 13 +++++++++++++ packages/analytics/src/public-types.ts | 16 ++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/analytics/src/helpers.test.ts b/packages/analytics/src/helpers.test.ts index 5ce43b8c201..0a67fafcf60 100644 --- a/packages/analytics/src/helpers.test.ts +++ b/packages/analytics/src/helpers.test.ts @@ -214,6 +214,19 @@ describe('Gtag wrapping functions', () => { expect((window['dataLayer'] as DataLayer).length).to.equal(1); }); + it('new window.gtag function does not wait when sending "set" calls', async () => { + wrapOrCreateGtag( + { [fakeAppId]: Promise.resolve(fakeMeasurementId) }, + fakeDynamicConfigPromises, + {}, + 'dataLayer', + 'gtag' + ); + window['dataLayer'] = []; + (window['gtag'] as Gtag)(GtagCommand.SET, { 'language': 'en' }); + expect((window['dataLayer'] as DataLayer).length).to.equal(1); + }); + it('new window.gtag function does not wait when sending "consent" calls', async () => { const consentParameters: ConsentSettings = { 'analytics_storage': 'granted', diff --git a/packages/analytics/src/public-types.ts b/packages/analytics/src/public-types.ts index 03d162ac692..930ba17b1eb 100644 --- a/packages/analytics/src/public-types.ts +++ b/packages/analytics/src/public-types.ts @@ -297,16 +297,20 @@ export interface EventParams { * @public */ export interface ConsentSettings { - // Enables storage, such as cookies, related to advertising + /** Enables storage, such as cookies, related to advertising */ ad_storage?: ConsentStatusString; - // Enables storage, such as cookies, related to analytics (for example, visit duration) + /** Enables storage, such as cookies, related to analytics (for example, visit duration) */ analytics_storage?: ConsentStatusString; - // Enables storage that supports the functionality of the website or app such as language settings + /** + * Enables storage that supports the functionality of the website or app such as language settings + */ functionality_storage?: ConsentStatusString; - // Enables storage related to personalization such as video recommendations + /** Enables storage related to personalization such as video recommendations */ personalization_storage?: ConsentStatusString; - // Enables storage related to security such as authentication functionality, fraud prevention, - // and other user protection + /** + * Enables storage related to security such as authentication functionality, fraud prevention, + * and other user protection. + */ security_storage?: ConsentStatusString; [key: string]: unknown; } From 2fa1ba87ca6965b571ab1a41e6fd88fe56c72f41 Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Mon, 27 Jun 2022 22:04:19 +0000 Subject: [PATCH 10/11] Update API reports --- common/api-review/analytics.api.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/common/api-review/analytics.api.md b/common/api-review/analytics.api.md index ec7bbb48472..856815b4b5d 100644 --- a/common/api-review/analytics.api.md +++ b/common/api-review/analytics.api.md @@ -25,15 +25,10 @@ export interface AnalyticsSettings { export interface ConsentSettings { // (undocumented) [key: string]: unknown; - // (undocumented) ad_storage?: ConsentStatusString; - // (undocumented) analytics_storage?: ConsentStatusString; - // (undocumented) functionality_storage?: ConsentStatusString; - // (undocumented) personalization_storage?: ConsentStatusString; - // (undocumented) security_storage?: ConsentStatusString; } From 0d0bd6169d4a16a01e1d2d92680c04c21febe1ac Mon Sep 17 00:00:00 2001 From: dwyfrequency Date: Wed, 29 Jun 2022 12:09:06 -0400 Subject: [PATCH 11/11] Add hyphen after param name in jsdoc --- packages/analytics/src/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/analytics/src/api.ts b/packages/analytics/src/api.ts index e5139ecaec0..423092361d9 100644 --- a/packages/analytics/src/api.ts +++ b/packages/analytics/src/api.ts @@ -233,7 +233,7 @@ export function setAnalyticsCollectionEnabled( * With gtag's "set" command, the values passed persist on the current page and are passed with * all subsequent events. * @public - * @param customParams Any custom params the user may pass to gtag.js. + * @param customParams - Any custom params the user may pass to gtag.js. */ export function setDefaultEventParameters(customParams: CustomParams): void { // Check if reference to existing gtag function on window object exists @@ -744,7 +744,7 @@ export type CustomEventName = T extends EventNameString ? never : T; * Use the {@link ConsentSettings} to specify individual consent type values. By default consent * types are set to "granted". * @public - * @param consentSettings Maps the applicable end user consent state for gtag.js. + * @param consentSettings - Maps the applicable end user consent state for gtag.js. */ export function setConsent(consentSettings: ConsentSettings): void { // Check if reference to existing gtag function on window object exists