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

Reduce size of tcf_consent payload in fides_consent cookie #4480

Merged
merged 20 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dfcdd54
Initial commit for cookie refactor
allisonking Dec 4, 2023
8903cbb
Update to cypress13 and add IAB lib
allisonking Dec 4, 2023
f577669
Refactor test to check for fides string instead of cookie values
allisonking Dec 4, 2023
2e9a91f
Update more tests
allisonking Dec 4, 2023
2580dcd
rely on tc string where possible for consent values, remove redundanc…
eastandwestwind Dec 4, 2023
ec73f9e
Merge branch 'prod-1413/tcf-cookie-refactor' of github.com:ethyca/fid…
eastandwestwind Dec 4, 2023
fd16045
remove unneeded logic, get consent from ac string
eastandwestwind Dec 4, 2023
718365d
Refactor initializing from fides string
allisonking Dec 4, 2023
1f4d62d
Restore override logic
allisonking Dec 4, 2023
070a233
update cookie test
eastandwestwind Dec 5, 2023
4697684
fix some tests, comment out some unit tests that are not working due …
eastandwestwind Dec 5, 2023
7af0c9b
remove unneeded tcf test from cookie unit test
eastandwestwind Dec 5, 2023
a99468e
clean up unused vars
eastandwestwind Dec 5, 2023
807a6c8
pull latest main, fix conflicts
eastandwestwind Dec 5, 2023
f8c80d6
address some comments I left on the pr earlier
eastandwestwind Dec 5, 2023
9f33f26
refactor tcf utils into own file so we can have unit tests for them
eastandwestwind Dec 5, 2023
71685a7
pull latest, fix conflicts
eastandwestwind Dec 5, 2023
b331bea
lint
eastandwestwind Dec 5, 2023
cdf0655
add to changelog
eastandwestwind Dec 5, 2023
5ee1414
Merge branch 'main' of github.com:ethyca/fides into prod-1413/tcf-coo…
eastandwestwind Dec 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 150 additions & 73 deletions clients/fides-js/src/fides-tcf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@
* ```
*/
import type { TCData } from "@iabtechlabtcf/cmpapi";
import { TCString } from "@iabtechlabtcf/core";
import { gtm } from "./integrations/gtm";
import { meta } from "./integrations/meta";
import { shopify } from "./integrations/shopify";

import {
ConsentMechanism,
FidesConfig,
FidesOptionsOverrides,
FidesOverrides,
Expand All @@ -70,13 +72,10 @@ import {
import type { Fides } from "./lib/initialize";
import { dispatchFidesEvent } from "./lib/events";
import {
buildTcfEntitiesFromCookie,
debugLog,
experienceIsValid,
FidesCookie,
hasSavedTcfPreferences,
isPrivacyExperience,
tcfConsentCookieObjHasSomeConsentSet,
transformConsentToFidesUserPreference,
transformTcfPreferencesToCookieKeys,
transformUserPreferenceToBoolean,
} from "./fides";
Expand All @@ -86,14 +85,11 @@ import {
TcfModelsRecord,
TcfSavePreferences,
} from "./lib/tcf/types";
import { TCF_KEY_MAP } from "./lib/tcf/constants";
import {
generateFidesStringFromCookieTcfConsent,
transformFidesStringToCookieKeys,
} from "./lib/tcf/utils";
import { TCF_COOKIE_KEY_MAP, TCF_KEY_MAP } from "./lib/tcf/constants";
import type { GppFunction } from "./lib/gpp/types";
import { makeStub } from "./lib/tcf/stub";
import { customGetConsentPreferences } from "./services/external/preferences";
import { decodeFidesString, idsFromAcString } from "./lib/tcf/fidesString";

declare global {
interface Window {
Expand Down Expand Up @@ -128,6 +124,82 @@ const getInitialPreference = (
return tcfObject.default_preference ?? UserConsentPreference.OPT_OUT;
};

/**
* Populates TCF entities with items from cookie.tcf_consent and Fides string.
* Returns TCF entities to be assigned to an experience.
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
*/
export const buildTcfEntitiesFromCookieAndFidesString = (
experience: PrivacyExperience,
cookie: FidesCookie
) => {
const tcfEntities = {
tcf_purpose_consents: experience.tcf_purpose_consents,
tcf_purpose_legitimate_interests:
experience.tcf_purpose_legitimate_interests,
tcf_special_purposes: experience.tcf_special_purposes,
tcf_features: experience.tcf_features,
tcf_special_features: experience.tcf_special_features,
tcf_vendor_consents: experience.tcf_vendor_consents,
tcf_vendor_legitimate_interests: experience.tcf_vendor_legitimate_interests,
tcf_system_consents: experience.tcf_system_consents,
tcf_system_legitimate_interests: experience.tcf_system_legitimate_interests,
};

// First update tcfEntities based on the cookie (system objects)
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
TCF_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => {
const cookieConsent = cookie.tcf_consent[cookieKey] ?? {};
// @ts-ignore the array map should ensure we will get the right record type
tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => {
const preference = Object.hasOwn(cookieConsent, item.id)
? transformConsentToFidesUserPreference(
Boolean(cookieConsent[item.id]),
ConsentMechanism.OPT_IN
)
: item.default_preference;
return { ...item, current_preference: preference };
});
});

// Now update tcfEntities based on the fides string
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
if (cookie.fides_string) {
const { tc: tcString, ac: acString } = decodeFidesString(
cookie.fides_string
);
const acStringIds = idsFromAcString(acString);

// Populate every field from tcModel
const tcModel = TCString.decode(tcString);
TCF_KEY_MAP.forEach(({ experienceKey, tcfModelKey }) => {
const isVendorKey =
tcfModelKey === "vendorConsents" ||
tcfModelKey === "vendorLegitimateInterests";
const tcIds = Array.from(tcModel[tcfModelKey])
.filter(([, consented]) => consented)
.map(([id]) => (isVendorKey ? `gvl.${id}` : id));
// @ts-ignore the array map should ensure we will get the right record type
tcfEntities[experienceKey] = experience[experienceKey]?.map((item) => {
let consented = !!tcIds.find((id) => id === item.id);
// Also check the AC string, which only applies to tcf_vendor_consents
if (
experienceKey === "tcf_vendor_consents" &&
acStringIds.find((id) => id === item.id)
) {
consented = true;
}
return {
...item,
current_preference: transformConsentToFidesUserPreference(
consented,
ConsentMechanism.OPT_IN
),
};
});
});
}

return tcfEntities;
};

const updateCookieAndExperience = async ({
cookie,
experience,
Expand Down Expand Up @@ -155,7 +227,10 @@ const updateCookieAndExperience = async ({
"Overriding preferences from client-side fetched experience with cookie fides_string consent",
cookie.fides_string
);
const tcfEntities = buildTcfEntitiesFromCookie(experience, cookie);
const tcfEntities = buildTcfEntitiesFromCookieAndFidesString(
experience,
cookie
);
return { cookie, experience: tcfEntities };
}

Expand All @@ -165,8 +240,11 @@ const updateCookieAndExperience = async ({
}

// If the user has prefs on a client-side fetched experience, but there is no fides_string,
// we need to use the prefs on the experience to generate a fidesString and cookie.tcf_consent
const tcSavePrefs: TcfSavePreferences = {};
// we need to use the prefs on the experience to generate
// 1. a fidesString
// 2. a cookie.tcf_consent (which only has system preferences since those are not captured in the fidesString)

// 1. Generate a fidesString from the experience
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
const enabledIds: EnabledIds = {
purposesConsent: [],
purposesLegint: [],
Expand All @@ -176,17 +254,9 @@ const updateCookieAndExperience = async ({
vendorsConsent: [],
vendorsLegint: [],
};

TCF_KEY_MAP.forEach(({ experienceKey, cookieKey, enabledIdsKey }) => {
tcSavePrefs[cookieKey] = [];
TCF_KEY_MAP.forEach(({ experienceKey, enabledIdsKey }) => {
experience[experienceKey]?.forEach((record) => {
const pref: UserConsentPreference = getInitialPreference(record);
// map experience to tcSavePrefs (same as cookie keys)
tcSavePrefs[cookieKey]?.push({
// @ts-ignore
id: record.id,
preference: pref,
});
// add to enabledIds only if user consent is True
if (transformUserPreferenceToBoolean(pref)) {
if (enabledIdsKey) {
Expand All @@ -195,50 +265,60 @@ const updateCookieAndExperience = async ({
}
});
});

const fidesString = await generateFidesString({
experience,
tcStringPreferences: enabledIds,
});

// 2. Generate a cookie object from the experience
const tcSavePrefs: TcfSavePreferences = {};
TCF_COOKIE_KEY_MAP.forEach(({ cookieKey, experienceKey }) => {
tcSavePrefs[cookieKey] = [];
experience[experienceKey]?.forEach((record) => {
const preference = getInitialPreference(record);
tcSavePrefs[cookieKey]?.push({ id: `${record.id}`, preference });
});
});
const tcfConsent = transformTcfPreferencesToCookieKeys(tcSavePrefs);

// Return the updated cookie
return {
cookie: { ...cookie, fides_string: fidesString, tcf_consent: tcfConsent },
experience,
};
};

/**
* If a fidesString is provided either explicitly or retrieved with a custom get preferences fn,
* we override the associated cookie props, which are then used to override associated props in the experience.
* TCF version of updating prefetched experience, based on:
* 1) experience: pre-fetched or client-side experience-based consent configuration
* 2) cookie: cookie containing user preference.

*
* Returns updated experience with user preferences. We have a separate function for notices
* and for TCF so that the bundle trees do not overlap.
*/
const updateFidesCookieFromString = (
cookie: FidesCookie,
fidesString: string,
debug: boolean,
fidesStringVersionHash: string | undefined
): { cookie: FidesCookie; success: boolean } => {
debugLog(
debug,
"Explicit fidesString detected. Proceeding to override all TCF preferences with given fidesString"
const updateExperienceFromCookieConsent = ({
experience,
cookie,
debug,
}: {
experience: PrivacyExperience;
cookie: FidesCookie;
debug?: boolean;
}): PrivacyExperience => {
const tcfEntities = buildTcfEntitiesFromCookieAndFidesString(
experience,
cookie
);
try {
const cookieKeys = transformFidesStringToCookieKeys(fidesString, debug);
return {
cookie: {
...cookie,
tcf_consent: cookieKeys,
fides_string: fidesString,
tcf_version_hash: fidesStringVersionHash ?? cookie.tcf_version_hash,
},
success: true,
};
} catch (error) {

if (debug) {
debugLog(
debug,
`Could not decode tcString from ${fidesString}, it may be invalid. ${error}`
`Returning updated pre-fetched experience with user consent.`,
experience
);
return { cookie, success: false };
}
return { ...experience, ...tcfEntities };
};

/**
Expand Down Expand Up @@ -266,35 +346,32 @@ const init = async (config: FidesConfig) => {
...getInitialCookie(config),
...overrides.consentPrefsOverrides?.consent,
};
if (config.options.fidesString) {
const { cookie: updatedCookie, success } = updateFidesCookieFromString(
cookie,
config.options.fidesString,
config.options.debug,
overrides.consentPrefsOverrides?.version_hash
);
if (success) {
// Update the fidesString if we have an override and the TC portion is valid
const { fidesString } = config.options;
if (fidesString) {
try {
// Make sure TC string is valid before we assign it
const { tc: tcString } = decodeFidesString(fidesString);
TCString.decode(tcString);
const updatedCookie: Partial<FidesCookie> = {
fides_string: fidesString,
tcf_version_hash:
overrides.consentPrefsOverrides?.version_hash ??
cookie.tcf_version_hash,
};
Object.assign(cookie, updatedCookie);
} catch (error) {
debugLog(
config.options.debug,
`Could not decode tcString from ${fidesString}, it may be invalid. ${error}`
);
}
Comment on lines +241 to 258
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like this got... a lot simpler! That's actually a pretty good sign 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, I agree! was a pleasant surprise 😄

} else if (
tcfConsentCookieObjHasSomeConsentSet(cookie.tcf_consent) &&
!cookie.fides_string &&
isPrivacyExperience(config.experience) &&
experienceIsValid(config.experience, config.options)
) {
// This state should not be hit, but just in case: if fidesString is missing on cookie but we have tcf consent,
// we should generate fidesString so that our CMP API accurately reflects user preference
cookie.fides_string = await generateFidesStringFromCookieTcfConsent(
config.experience,
cookie.tcf_consent
);
debugLog(
config.options.debug,
"fides_string was missing from cookie, so it has been generated based on tcf_consent",
cookie.fides_string
);
Comment on lines -288 to -292
Copy link
Contributor

Choose a reason for hiding this comment

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

Totally makes sense that this case is removed now. Nice.

}
const initialFides = getInitialFides({ ...config, cookie });
const initialFides = getInitialFides({
...config,
cookie,
updateExperienceFromCookieConsent,
});
// Initialize the CMP API early so that listeners are established
initializeTcfCmpApi();
if (initialFides) {
Expand Down
6 changes: 5 additions & 1 deletion clients/fides-js/src/fides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,11 @@ const init = async (config: FidesConfig) => {
...getInitialCookie(config),
...overrides.consentPrefsOverrides?.consent,
};
const initialFides = getInitialFides({ ...config, cookie });
const initialFides = getInitialFides({
...config,
cookie,
updateExperienceFromCookieConsent,
});
if (initialFides) {
Object.assign(_Fides, initialFides);
dispatchFidesEvent("FidesInitialized", cookie, config.options.debug);
Expand Down
Loading
Loading