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 function setConsent() to set end user consent state for web apps in Firebase Analytics #6376

Merged
merged 13 commits into from
Jun 29, 2022
5 changes: 5 additions & 0 deletions .changeset/shiny-bats-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/analytics': minor
---

Add function `setConsent()` to set the applicable end user "consent" state.
14 changes: 14 additions & 0 deletions common/api-review/analytics.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;

Expand Down
24 changes: 22 additions & 2 deletions packages/analytics/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
Analytics,
AnalyticsCallOptions,
AnalyticsSettings,
ConsentSettings,
CustomParams,
EventNameString,
EventParams
Expand All @@ -35,7 +36,7 @@ import {
getModularInstance,
deepEqual
} from '@firebase/util';
import { ANALYTICS_TYPE } from './constants';
import { ANALYTICS_TYPE, GtagCommand } from './constants';
import {
AnalyticsService,
initializationPromisesMap,
Expand All @@ -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';

Expand Down Expand Up @@ -716,3 +718,21 @@ export function logEvent(
* @public
*/
export type CustomEventName<T> = 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.
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I just ran build on this branch and noticed warnings from api-documenter that consentSettings needs to be followed by a hyphen here, and also customParams on line 236, which I think was from a previous PR.

*/
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);
}
}
3 changes: 2 additions & 1 deletion packages/analytics/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
27 changes: 26 additions & 1 deletion packages/analytics/src/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
AnalyticsCallOptions,
CustomParams,
ControlParams,
EventParams
EventParams,
ConsentSettings
} from './public-types';
import { Gtag } from './types';
import { GtagCommand } from './constants';
Expand Down Expand Up @@ -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(
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved
consentSettings: ConsentSettings
): void {
if (defaultConsentSettingsForInit) {
defaultConsentSettingsForInit = {
...defaultConsentSettingsForInit,
...consentSettings
};
} else {
defaultConsentSettingsForInit = consentSettings;
}
}
21 changes: 16 additions & 5 deletions packages/analytics/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void> {
try {
// If event, check that relevant initialization promises have completed.
Expand All @@ -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.
Expand All @@ -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);
Expand All @@ -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;
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates global gtag function or wraps existing one if found.
Expand Down
6 changes: 6 additions & 0 deletions packages/analytics/src/initialize-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
if (!isIndexedDBAvailable()) {
Expand Down Expand Up @@ -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);
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved
}

// 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.
Expand Down
10 changes: 10 additions & 0 deletions packages/analytics/src/public-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,14 @@ export interface EventParams {
page_path?: string;
[key: string]: unknown;
}

/** Maps the applicable end user consent state. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest: "Consent status settings for each consent type." or similar, @egilmorez may have suggestions. Also add @public tag.

export interface ConsentSettings {
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved
ad_storage?: ConsentStatusString;
dwyfrequency marked this conversation as resolved.
Show resolved Hide resolved
analytics_storage?: ConsentStatusString;
}

/* eslint-enable camelcase */

/** Whether a particular consent type has been granted or denied. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can ask @egilmorez for a more definitive opinion but I guess maybe "should be" instead of "has been" since in this context we can only set it, not read the current state. Also needs a @public tag.

export type ConsentStatusString = 'granted' | 'denied';
15 changes: 14 additions & 1 deletion packages/analytics/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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[];