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

Dispatch Fides.js lifecycle events on window (FidesInitialized, FidesUpdated) and cross-publish to Fides.gtm() integration #3454

Merged
merged 17 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ The types of changes are:
- Add `notice_key` field to Privacy Notice UI form [#3403](https://github.com/ethyca/fides/pull/3403)
- Add `identity` query param to the consent reporting API view [#3418](https://github.com/ethyca/fides/pull/3418)
- Use `rollup-plugin-postcss` to bundle and optimize the `fides.js` components CSS [#3431](https://github.com/ethyca/fides/pull/3431)
- Dispatch Fides.js lifecycle events on window (FidesInitialized, FidesUpdated) and cross-publish to Fides.gtm() integration [#3454](https://github.com/ethyca/fides/pull/3454)


### Fixed

Expand Down
12 changes: 10 additions & 2 deletions clients/fides-js/src/fides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ import {
transformConsentToFidesUserPreference,
validateOptions,
} from "./lib/consent-utils";
import { dispatchFidesEvent } from "./lib/events";
import { fetchExperience } from "./services/fides/api";
import { getGeolocation } from "./services/external/geolocation";
import { OverlayProps } from "~/components/Overlay";
import { OverlayProps } from "./components/Overlay";
import { updateConsentPreferences } from "./lib/preferences";

export type Fides = {
Expand Down Expand Up @@ -250,6 +251,12 @@ const init = async ({
_Fides.options = options;
_Fides.initialized = true;

// Dispatch the "FidesInitialized" event to update listeners with the initial
// state. For convenience, also dispatch the "FidesUpdated" event; this allows
// listeners to ignore the initialization event if they prefer
dispatchFidesEvent("FidesInitialized", cookie);
dispatchFidesEvent("FidesUpdated", cookie);

automaticallyApplyGPCPreferences(
cookie,
fidesRegionString,
Expand Down Expand Up @@ -287,14 +294,15 @@ if (typeof window !== "undefined") {

// Export everything from ./lib/* to use when importing fides.mjs as a module
// TODO: pretty sure we need ./services/* too?
export * from "./lib/consent";
export * from "./components";
export * from "./lib/consent";
export * from "./lib/consent-context";
export * from "./lib/consent-types";
export * from "./lib/consent-links";
export * from "./lib/consent-utils";
export * from "./lib/consent-value";
export * from "./lib/cookie";
export * from "./lib/events";

// DEFER: this default export isn't very useful, it's just the Fides type
export default Fides;
53 changes: 45 additions & 8 deletions clients/fides-js/src/integrations/gtm.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
import { CookieKeyConsent } from "../lib/cookie";
import { FidesEventDetail } from "../lib/events";

declare global {
interface Window {
dataLayer?: any[];
}
}

/**
* Call Fides.gtm to configure Google Tag Manager. The user's consent choices will be
* pushed into GTM's `dataLayer` under `Fides.consent`.
* Defines the structure of the Fides variable pushed to the GTM data layer
*/
export const gtm = () => {
interface FidesVariable {
consent: CookieKeyConsent;
}

// Helper function to push the Fides variable to the GTM data layer from a FidesEvent
const pushFidesVariableToGTM = (fidesEvent: {
type: string;
detail: FidesEventDetail;
}) => {
// Initialize the dataLayer object, just in case we run before GTM is initialized
const dataLayer = window.dataLayer ?? [];
window.dataLayer = dataLayer;
dataLayer.push({
Fides: {
consent: window.Fides.consent,
},
});

// Construct the Fides variable that will be pushed to GTM
const Fides: FidesVariable = {
consent: fidesEvent.detail.consent,
};

// Push to the GTM dataLayer
dataLayer.push({ event: fidesEvent.type, Fides });
};

/**
* Call Fides.gtm() to configure the Fides <> Google Tag Manager integration.
* The user's consent choices will automatically be pushed into GTM's
* `dataLayer` under `Fides.consent` variable.
*/
export const gtm = () => {
// Listen for Fides events and cross-publish them to GTM
window.addEventListener("FidesInitialized", (event) =>
pushFidesVariableToGTM(event)
);
window.addEventListener("FidesUpdated", (event) =>
pushFidesVariableToGTM(event)
);

// If Fides was already initialized, publish a synthetic event immediately
if (window.Fides?.initialized) {
pushFidesVariableToGTM({
type: "FidesInitialized",
detail: { consent: window.Fides.consent },
});
}
};
2 changes: 2 additions & 0 deletions clients/fides-js/src/integrations/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ type MetaOptions = {
/**
* Call Fides.meta to configure Meta Pixel tracking.
*
* DEFER: Update this integration to support async Fides events
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add follow-up issue

*
* @example
* Fides.meta({
* consent: Fides.consent.data_sales,
Expand Down
2 changes: 2 additions & 0 deletions clients/fides-js/src/integrations/shopify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const applyOptions = (options: ShopifyOptions) => {
* Call Fides.shopify to configure Shopify customer privacy. Currently the only consent option
* Shopify allows to be configured is user tracking.
*
* DEFER: Update this integration to support async Fides events
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add follow-up issue

*
* @example
* Fides.shopify({ tracking: Fides.consent.data_sales })
*/
Expand Down
52 changes: 52 additions & 0 deletions clients/fides-js/src/lib/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { FidesCookie } from "./cookie";

/**
* Defines the available event names:
* - FidesInitialized: dispatched when initialization is complete, from Fides.init()
* - FidesUpdated: dispatched when preferences are updated, from updateConsentPreferences() or Fides.init()
*/
export type FidesEventType = "FidesInitialized" | "FidesUpdated";

// Bonus points: update the WindowEventMap interface with our custom event types
declare global {
interface WindowEventMap {
FidesInitialized: FidesEvent;
FidesUpdated: FidesEvent;
}
}

/**
* Defines the properties available on event.detail. As of now, these are an
* explicit subset of properties from the Fides cookie
* TODO: add identity and meta?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Decide on this - I think it makes sense to include these in the event, I could see a listener wanting a bit of information about the identity when processing the preferences, etc.

Copy link
Contributor

@eastandwestwind eastandwestwind Jun 5, 2023

Choose a reason for hiding this comment

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

Will facebook / shopify need identity or other metadata?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nothing needs it yet, but I'm wondering what a developer might need when integrating consent into their app via these events...

*/
export type FidesEventDetail = Pick<FidesCookie, "consent">;

export type FidesEvent = CustomEvent<FidesEventDetail>;

/**
* Dispatch a custom event on the window object, providing the current Fides
* state on the "detail" property of the event.
*
* Example usage:
* ```
* window.addEventListener("FidesUpdated", (evt) => console.log("Fides.consent updated:", evt.detail.consent));
* ```
*
* The snippet above will print a console log whenever consent preferences are initialized/updated, like:
* ```
* Fides.consent updated: { data_sales_and_sharing: true }
* ```
*/
export const dispatchFidesEvent = (
type: FidesEventType,
cookie: FidesCookie
) => {
if (typeof window !== "undefined" && typeof CustomEvent !== "undefined") {
// Pick a subset of the Fides cookie properties to provide as event detail
const { consent }: FidesEventDetail = cookie;
const detail: FidesEventDetail = { consent };
const event = new CustomEvent(type, { detail });
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah this is cool, learned about CustomEvent native obj today

window.dispatchEvent(event);
}
};
12 changes: 10 additions & 2 deletions clients/fides-js/src/lib/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "./consent-types";
import { debugLog, transformUserPreferenceToBoolean } from "./consent-utils";
import { CookieKeyConsent, FidesCookie, saveFidesCookie } from "./cookie";
import { dispatchFidesEvent } from "./events";
import { patchUserPreferenceToFidesServer } from "../services/fides/api";

/**
Expand Down Expand Up @@ -49,6 +50,10 @@ export const updateConsentPreferences = ({
});
});

// Update the cookie object
// eslint-disable-next-line no-param-reassign
cookie.consent = consentCookieKey;
Comment on lines +53 to +55
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This mutates the provided cookie param, which might be a bit surprising... but I think it makes sense to do this as we don't store that state anywhere else super declaratively. This is especially true since we call saveFidesCookie below which persists the updated state to the browser - it'd be surprising to me to find that the cookie object was actually more stale than the version saved to browser since we weren't updating that object before!

Copy link
Contributor

Choose a reason for hiding this comment

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

this is fine. It's the same pattern we use in the init method of fides.ts


// 1. Save preferences to Fides API
debugLog(debug, "Saving preferences to Fides API");
const privacyPreferenceCreate: PrivacyPreferencesRequest = {
Expand All @@ -67,9 +72,12 @@ export const updateConsentPreferences = ({

// 2. Update the window.Fides.consent object
debugLog(debug, "Updating window.Fides");
window.Fides.consent = consentCookieKey;
window.Fides.consent = cookie.consent;

// 3. Save preferences to the cookie
debugLog(debug, "Saving preferences to cookie");
saveFidesCookie({ ...cookie, consent: consentCookieKey });
saveFidesCookie(cookie);

// 4. Dispatch a "FidesUpdated" event
dispatchFidesEvent("FidesUpdated", cookie);
};
Loading