From 67c69c6fef712ec29dfdbc1a525570c41fb796be Mon Sep 17 00:00:00 2001 From: joshua-ostrom Date: Wed, 12 Jun 2024 09:01:07 -0400 Subject: [PATCH] Wired up initial values support --- README.md | 4 +++ packages/cookie-manager/README.md | 4 +++ packages/cookie-manager/src/CookieContext.tsx | 34 +++++++++++++------ packages/cookie-manager/src/types.ts | 2 ++ .../src/utils/applyGpcToAdPref.ts | 15 ++++---- .../src/utils/applyGpcToCookiePref.ts | 16 +++++---- .../cookie-manager/src/utils/getAllCookies.ts | 1 + .../cookie-manager/src/utils/getGpc.test.ts | 17 ++++++++++ packages/cookie-manager/src/utils/getGpc.ts | 21 ++++++++++++ 9 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 packages/cookie-manager/src/utils/getGpc.test.ts create mode 100644 packages/cookie-manager/src/utils/getGpc.ts diff --git a/README.md b/README.md index ed68d1d..296bb6f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,10 @@ cd /path/to/coinbase/cb-cookie-manager/example/app yarn dev ``` +## Testing + +yarn test + ## Packages - `@coinbase/cookie-manager`: Package that helps with managing first party client side cookies to adhere to CCPA and GDPR Cookie regulations. More information [here](./packages/cookie-manager/README.md) diff --git a/packages/cookie-manager/README.md b/packages/cookie-manager/README.md index 04db9eb..12ea5cc 100644 --- a/packages/cookie-manager/README.md +++ b/packages/cookie-manager/README.md @@ -214,6 +214,10 @@ The provider must wrap the entire application and only be instantiated once. On `log: (str: string, options?: Record) => void`: Log function +`initialCookieValues?:Record `: Useful for server side rendering flows - setting of initial cookie values + +`initialGPCValue?:boolean`: Useful for server side rendering flows - honoring of Set-GPC header + Example usage: ```typescript diff --git a/packages/cookie-manager/src/CookieContext.tsx b/packages/cookie-manager/src/CookieContext.tsx index 5ac61c3..466a8dc 100644 --- a/packages/cookie-manager/src/CookieContext.tsx +++ b/packages/cookie-manager/src/CookieContext.tsx @@ -38,13 +38,22 @@ type Props = { }; export const CookieProvider = ({ children }: Props) => { - const { config, region, shadowMode, log, onPreferenceChange } = useTrackingManager(); + const { + config, + region, + shadowMode, + log, + onPreferenceChange, + initialCookieValues, + initialGPCValue, + } = useTrackingManager(); const POLL_INTERVAL = 500; const [cookieValues, setCookieValues] = useState(() => getAllCookies(region)); let priorCookieValue: Record; let trackingPreference: TrackingPreference; let adTrackingPreference: AdTrackingPreference; + const gpc = initialGPCValue || false; const removeCookies = useCallback( (cookies: string[]) => { @@ -62,17 +71,18 @@ export const CookieProvider = ({ children }: Props) => { ); useEffect(() => { + // TODO clean up hydration if (typeof window !== 'undefined') { const checkCookies = () => { - const currentCookie = getAllCookies(region); + const currentCookie = getAllCookies(region, initialCookieValues); if (priorCookieValue == undefined || !areRecordsEqual(priorCookieValue, currentCookie)) { priorCookieValue = currentCookie; setCookieValues(currentCookie); - // Grab out prefences (they wil have GPC applied if present) - trackingPreference = getTrackingPreference(currentCookie, region, config); - adTrackingPreference = getAdTrackingPreference(currentCookie, region); + // Grab out prefences (they will have GPC applied if present) + trackingPreference = getTrackingPreference(currentCookie, region, config, gpc); + adTrackingPreference = getAdTrackingPreference(currentCookie, region, gpc); setGTMVariables(trackingPreference, adTrackingPreference); const cookiesToRemove: Array = []; @@ -197,7 +207,8 @@ const setCookieFunction = ({ const getTrackingPreference = ( cookieCache: Record, region: Region, - config: Config + config: Config, + gpcDefault?: boolean ): TrackingPreference => { const trackingPreference = region === Region.EU @@ -208,13 +219,14 @@ const getTrackingPreference = ( // { region: Region.EU, consent: ['necessary'] } const preference = trackingPreference || getDefaultTrackingPreference(region, config); // Apply GPC when present - return applyGpcToCookiePref(preference); + return applyGpcToCookiePref(preference, gpcDefault || false); }; // Do we want to change the ADVERTISING_SHARING_ALLOWED value to clear prior values? const getAdTrackingPreference = ( cookieCache: Record, - region: Region + region: Region, + gpcHeader?: boolean ): AdTrackingPreference => { const adTrackingPreference = cookieCache[ADVERTISING_SHARING_ALLOWED]; @@ -222,13 +234,13 @@ const getAdTrackingPreference = ( // Example: adPreference { value: 'false' } const adPreference = adTrackingPreference || adTrackingDefault; - return applyGpcToAdPref(region, adPreference); + return applyGpcToAdPref(region, adPreference, gpcHeader || false); }; export const useCookie = (cookieName: string): [any | undefined, SetCookieFunction] => { const cookieCache = useContext(CookieContext); - const { config, region, log, shadowMode, onError } = useTrackingManager(); - const trackingPreference = getTrackingPreference(cookieCache, region, config); + const { config, region, log, shadowMode, onError, initialGPCValue } = useTrackingManager(); + const trackingPreference = getTrackingPreference(cookieCache, region, config, initialGPCValue); const setCookie = setCookieFunction({ cookieName, trackingPreference, diff --git a/packages/cookie-manager/src/types.ts b/packages/cookie-manager/src/types.ts index 17c1416..a245b1a 100644 --- a/packages/cookie-manager/src/types.ts +++ b/packages/cookie-manager/src/types.ts @@ -66,6 +66,8 @@ export type TrackingManagerDependencies = { config: Config; shadowMode?: boolean; log: LogFunction; + initialCookieValues?: Record; + initialGPCValue?: boolean; }; export type AdTrackingPreference = { diff --git a/packages/cookie-manager/src/utils/applyGpcToAdPref.ts b/packages/cookie-manager/src/utils/applyGpcToAdPref.ts index fd07934..23994a4 100644 --- a/packages/cookie-manager/src/utils/applyGpcToAdPref.ts +++ b/packages/cookie-manager/src/utils/applyGpcToAdPref.ts @@ -1,20 +1,19 @@ import { AdTrackingPreference, Region } from '../types'; +import getGpc from './getGpc'; const applyGpcToAdPref = ( region: Region, - preference: AdTrackingPreference + preference: AdTrackingPreference, + gpcHeader?: boolean ): AdTrackingPreference => { // We are only applying GPC in non-EU countries at this point if (region == Region.EU) { return preference; } - - if (typeof window === 'undefined' || typeof window.navigator === 'undefined') { - return preference; - } - - // If we lack GPC or it's set ot false we are done - if (!(window.navigator as any).globalPrivacyControl) { + // If the browser is has global privacy control enabled + // we will honor it + const gpc = getGpc(gpcHeader); + if (!gpc) { return preference; } diff --git a/packages/cookie-manager/src/utils/applyGpcToCookiePref.ts b/packages/cookie-manager/src/utils/applyGpcToCookiePref.ts index e0b02e2..4ee0e06 100644 --- a/packages/cookie-manager/src/utils/applyGpcToCookiePref.ts +++ b/packages/cookie-manager/src/utils/applyGpcToCookiePref.ts @@ -1,20 +1,22 @@ import { Region, TrackingCategory, TrackingPreference } from '../types'; +import getGpc from './getGpc'; // { region: Region.DEFAULT, consent: ['necessary', 'performance', 'functional', 'targeting'] } -const applyGpcToCookiePref = (preference: TrackingPreference): TrackingPreference => { +const applyGpcToCookiePref = ( + preference: TrackingPreference, + gpcHeader?: boolean +): TrackingPreference => { // We are only applying GPC in non-EU countries at this point if (preference.region == Region.EU) { return preference; } - // TODO: We want to support server side render flows - // where the user can set an initial value and indicate that gpc has been enabled - if (typeof window === 'undefined' || typeof window.navigator === 'undefined') { + // If the browser is has global privacy control enabled + // we will honor it + const gpc = getGpc(gpcHeader); + if (!gpc) { return preference; } - if (!(window.navigator as any).globalPrivacyControl) { - return preference; - } // If the user had opted in to GPC we want to honor it const categories = preference.consent.filter((cat) => cat !== TrackingCategory.TARGETING); diff --git a/packages/cookie-manager/src/utils/getAllCookies.ts b/packages/cookie-manager/src/utils/getAllCookies.ts index 412a6cc..73b4e8b 100644 --- a/packages/cookie-manager/src/utils/getAllCookies.ts +++ b/packages/cookie-manager/src/utils/getAllCookies.ts @@ -25,6 +25,7 @@ export const deserializeCookies = (region: Region, cookies: Record) { if (typeof window === 'undefined' && initialCookies) { return deserializeCookies(region, initialCookies); diff --git a/packages/cookie-manager/src/utils/getGpc.test.ts b/packages/cookie-manager/src/utils/getGpc.test.ts new file mode 100644 index 0000000..aca20c9 --- /dev/null +++ b/packages/cookie-manager/src/utils/getGpc.test.ts @@ -0,0 +1,17 @@ +import getGpc from './getGpc'; + +describe('getGpc', () => { + it('honors navigator.globalPrivacyControl', () => { + (navigator as any).globalPrivacyControl = true; + expect(getGpc()).toEqual(true); + }); + + it('honors the passed header value when passed', () => { + (navigator as any).globalPrivacyControl = false; + expect(getGpc(true)).toEqual(true); + }); + + it('returns false by default', () => { + expect(getGpc()).toEqual(false); + }); +}); diff --git a/packages/cookie-manager/src/utils/getGpc.ts b/packages/cookie-manager/src/utils/getGpc.ts new file mode 100644 index 0000000..eedcae3 --- /dev/null +++ b/packages/cookie-manager/src/utils/getGpc.ts @@ -0,0 +1,21 @@ +export function getGpc(gpcHeaderValue?: boolean): boolean { + const header = gpcHeaderValue == null ? false : gpcHeaderValue; + + if (header) { + // honor the Set-GPC header if it's set to true + return true; + } + + if (typeof window === 'undefined' || typeof window.navigator === 'undefined') { + // if we don't have access to the window.navigator return the header value + // if present, false otherwise + return header; + } + + if (!(window.navigator as any).globalPrivacyControl) { + return false; + } + return true; +} + +export default getGpc;