Last active
October 31, 2023 10:05
-
-
Save sperand-io/4725e248a35d5005d68d810d8a8f7b29 to your computer and use it in GitHub Desktop.
Example of using analytics.js conditional loading with TrustArc Consent Manager
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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