Skip to content

Instantly share code, notes, and snippets.

@sperand-io
Last active October 31, 2023 10:05
Show Gist options
  • Save sperand-io/4725e248a35d5005d68d810d8a8f7b29 to your computer and use it in GitHub Desktop.
Save sperand-io/4725e248a35d5005d68d810d8a8f7b29 to your computer and use it in GitHub Desktop.
Example of using analytics.js conditional loading with TrustArc Consent Manager
// To get started, make sure you're using the latest version of the analytics.js snippet (4.1.0 or above)
// and remove the `analytics.load("YOUR_WRITE_KEY")` call. then drop this script below the snippet.
// this is a standalone script for modern browsers. if you'd like to target older browsers, include
// a fetch polyfill and use that instead of window.fetch. This would require that you build and package the
// script somehow (rollup, webpack, browserify, etc). You may also want to transpile it to ES5 w eg Babel.
// This script is configured to make all collection opt-in (rather than opt-out) for all users.
// If you want to conditionally require whether or not to block data collection before affirmative consent, use something
// like inEU below and pass that function into `conditionallyLoadAnalytics`. If you want to collect data until the user
// opts out, just change OPT_IN to false.
// import fetch from 'isomorphic-fetch'
// import inEU from '@segment/in-eu'
// CONFIG — EDIT ME!
// CONFIG — EDIT ME!
const OPT_IN = true; // false = only disable after opt out, can replace with a function such as inEU above
const YOUR_DOMAIN = "domain.com"; // the hostname of your website
const WEBSITE_WRITE_KEY = "sNOks4InIbuBaPcSQa76ny0neogp6yDf"; // your segment website source write key
const SEGMENT_MAPPING = 1;
const OTHER_WRITE_KEYS = []; // any other sources w destinations that you want to include in config
// gets enabled destination configurations from across your source (and optionally other sources in workspace)
fetchDestinations([WEBSITE_WRITE_KEY, ...OTHER_WRITE_KEYS]).then(
destinations => {
// KEY LOGIC HERE
// eg. could instead use trustArc's other APIs such as getConsentCategories
// and map between the returned vendor domains to specific Segment Integrationss, etc
// review this logic carefully, and edit per your business requirements
//
// in this case, we go with all when customer provides full consent
// remove Advertising tools when the customer provides functional
// and mark Segment as required (for data to flow to warehouse etc— if you make that clear in your policy)
// note that though Segment will receive data we wont forward (even server side) to other sources because we
// decorate the calls with the full set of integrations you have enabled with "false."
//
// no preference here goes with all on — you may want to flip that!
const { consentDecision } = truste.cma.callApi(
"getGDPRConsentDecision",
YOUR_DOMAIN
);
const destinationPreferences = destinations
.map(function(dest) {
if (consentDecision.includes(3)) return { [dest.id]: true };
if (consentDecision.includes(2)) return { [dest.id]: dest.category === "Advertising" ? false : true };
if (consentDecision.includes(1)) return { [dest.id]: false };
if (consentDecision.includes(0)) return { [dest.id]: !OPT_IN };
})
.reduce(
(acc, val) => {
return {
...val,
...acc
};
},
{ "Segment.io": consentDecision.some(d => d === SEGMENT_MAPPING) }
);
conditionallyLoadAnalytics({
writeKey: WEBSITE_WRITE_KEY,
destinations,
destinationPreferences,
isConsentRequired: OPT_IN
});
}
);
// instructs trustarc to notify on changes to consent
window.top.postMessage(JSON.stringify({
PrivacyManagerAPI: {
action: "getConsent",
timestamp: new Date().getTime(),
self: YOUR_DOMAIN
}
}), "*");
// registers listener for consent changes.
// some care has been taken here to handle messages safely.
// will reload window if they've "denied"... to reset tracking.
window.addEventListener("message", function reload(e) {
let data = e.data;
if (typeof data === "string") {
try {
data = JSON.parse(data);
} catch (e) { /* weird message, bail */}
}
if (
data &&
data.PrivacyManagerAPI &&
data.PrivacyManagerAPI.consent === "denied"
) {
return window.location.reload();
}
// otherwise approved... carry on!
}, false);
// helper functions below...
function conditionallyLoadAnalytics({
writeKey,
destinations,
destinationPreferences,
isConsentRequired,
shouldReload = true // change if you dont want to reload on consent changes
}) {
let isAnythingEnabled = false;
if (!destinationPreferences) {
if (isConsentRequired) {
return;
}
// Load a.js normally when consent isn't required and there's no preferences
if (!window.analytics.initialized) {
window.analytics.load(writeKey);
}
return;
}
for (const destination of destinationPreferences) {
const isEnabled = destinationPreferences[destination];
if (isEnabled) {
isAnythingEnabled = true;
}
}
// Reload the page if the trackers have already been initialized so that
// the user's new preferences can take effect
if (window.analytics.initialized) {
if (shouldReload) {
window.location.reload();
}
return;
}
// Don't load a.js at all if nothing has been enabled
if (isAnythingEnabled) {
window.analytics.load(writeKey, { integrations: destinationPreferences });
}
}
async function fetchDestinationForWriteKey(writeKey) {
const res = await window.fetch(
`https://cdn.segment.com/v1/projects/${writeKey}/integrations`
);
if (!res.ok) {
throw new Error(
`Failed to fetch integrations for write key ${writeKey}: HTTP ${
res.status
} ${res.statusText}`
);
}
const destinations = await res.json();
// Rename creationName to id to abstract the weird data model
for (const destination of destinations) {
destination.id = destination.creationName;
delete destination.creationName;
}
return destinations;
}
async function fetchDestinations(...writeKeys) {
const destinationsRequests = [];
for (const writeKey of writeKeys) {
destinationsRequests.push(fetchDestinationForWriteKey(writeKey));
}
let destinations = await Promise.all(destinationsRequests);
// unique list of destination across all sources
destinations = [
...destinations
.reduce((a, b) => a.concat(b), []) // flatten multi-d array
.reduce((map, item) => {
if (d.id === "Repeater") return map; // remove repeater
map.has(item["id"]) || map.set(item["id"], item);
return map;
}, new Map()) // return object
.values()
];
return destinations;
}
// To get started, make sure you're using the latest version of the analytics.js snippet (4.1.0 or above)
// and remove the `analytics.load("YOUR_WRITE_KEY")` call. then drop this script below the snippet.
// this is a standalone script for modern browsers. if you'd like to target older browsers, include
// a fetch polyfill and use that instead of window.fetch. This would require that you build and package the
// script somehow (rollup, webpack, browserify, etc). You may also want to transpile it to ES5 w eg Babel.
// This script is configured to make all collection opt-in (rather than opt-out) for all users.
// If you want to conditionally require whether or not to block data collection before affirmative consent, use something
// like inEU below and pass that function into `conditionallyLoadAnalytics`. If you want to collect data until the user
// opts out, just change OPT_IN to false.
// import fetch from 'isomorphic-fetch'
// import inEU from '@segment/in-eu'
// CONFIG — EDIT ME!
// CONFIG — EDIT ME!
const OPT_IN = true; // false = only disable after opt out, can replace with a function such as inEU above
const YOUR_DOMAIN = "domain.com"; // the hostname of your website
const WEBSITE_WRITE_KEY = "sNOks4InIbuBaPcSQa76ny0neogp6yDf"; // your segment website source write key
const SEGMENT_MAPPING = 1;
const OTHER_WRITE_KEYS = []; // any other sources w destinations that you want to include in config
// gets enabled destination configurations from across your source (and optionally other sources in workspace)
fetchDestinations([WEBSITE_WRITE_KEY, ...OTHER_WRITE_KEYS]).then(
destinations => {
// KEY LOGIC HERE
// eg. could instead use trustArc's other APIs such as getConsentCategories
// and map between the returned vendor domains to specific Segment Integrationss, etc
// review this logic carefully, and edit per your business requirements
//
// in this case, we go with all when customer provides full consent
// remove Advertising tools when the customer provides functional
// and mark Segment as required (for data to flow to warehouse etc— if you make that clear in your policy)
// note that though Segment will receive data we wont forward (even server side) to other sources because we
// decorate the calls with the full set of integrations you have enabled with "false."
//
// no preference here goes with all on — you may want to flip that!
const consentDecision = truste.cma.callApi(
"getConsentDecision",
YOUR_DOMAIN
);
const destinationPreferences = destinations
.map(function(dest) {
if (consentDecision === 3) return { [dest.id]: true };
if (consentDecision === 2) return { [dest.id]: dest.category === "Advertising" ? false : true };
if (consentDecision === 1) return { [dest.id]: false };
if (consentDecision === 0) return { [dest.id]: !OPT_IN };
})
.reduce(
(acc, val) => {
return {
...val,
...acc
};
},
{ "Segment.io": consentDecision >= SEGMENT_MAPPING }
);
conditionallyLoadAnalytics({
writeKey: WEBSITE_WRITE_KEY,
destinations,
destinationPreferences,
isConsentRequired: OPT_IN
});
}
);
// instructs trustarc to notify on changes to consent
window.top.postMessage(JSON.stringify({
PrivacyManagerAPI: {
action: "getConsent",
timestamp: new Date().getTime(),
self: YOUR_DOMAIN
}
}), "*");
// registers listener for consent changes.
// some care has been taken here to handle messages safely.
// will reload window if they've "denied"... to reset tracking.
window.addEventListener("message", function reload(e) {
let data = e.data;
if (typeof data === "string") {
try {
data = JSON.parse(data);
} catch (e) { /* weird message, bail */}
}
if (
data &&
data.PrivacyManagerAPI &&
data.PrivacyManagerAPI.consent === "denied"
) {
return window.location.reload();
}
// otherwise approved... carry on!
}, false);
// helper functions below...
function conditionallyLoadAnalytics({
writeKey,
destinations,
destinationPreferences,
isConsentRequired,
shouldReload = true // change if you dont want to reload on consent changes
}) {
let isAnythingEnabled = false;
if (!destinationPreferences) {
if (isConsentRequired) {
return;
}
// Load a.js normally when consent isn't required and there's no preferences
if (!window.analytics.initialized) {
window.analytics.load(writeKey);
}
return;
}
for (const destination of destinationPreferences) {
const isEnabled = destinationPreferences[destination];
if (isEnabled) {
isAnythingEnabled = true;
}
}
// Reload the page if the trackers have already been initialised so that
// the user's new preferences can take affect
if (window.analytics.initialized) {
if (shouldReload) {
window.location.reload();
}
return;
}
// Don't load a.js at all if nothing has been enabled
if (isAnythingEnabled) {
window.analytics.load(writeKey, { integrations: destinationPreferences });
}
}
async function fetchDestinationForWriteKey(writeKey) {
const res = await window.fetch(
`https://cdn.segment.com/v1/projects/${writeKey}/integrations`
);
if (!res.ok) {
throw new Error(
`Failed to fetch integrations for write key ${writeKey}: HTTP ${
res.status
} ${res.statusText}`
);
}
const destinations = await res.json();
// Rename creationName to id to abstract the weird data model
for (const destination of destinations) {
destination.id = destination.creationName;
delete destination.creationName;
}
return destinations;
}
async function fetchDestinations(...writeKeys) {
const destinationsRequests = [];
for (const writeKey of writeKeys) {
destinationsRequests.push(fetchDestinationForWriteKey(writeKey));
}
let destinations = await Promise.all(destinationsRequests);
// unique list of destination across all sources
destinations = [
...destinations
.reduce((a, b) => a.concat(b), []) // flatten multi-d array
.filter(d => d.id !== "Repeater") // remove repeater
.reduce((map, item) => {
map.has(item["id"]) || map.set(item["id"], item);
return map;
}, new Map())
.values()
];
return destinations;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment