Skip to content

Commit

Permalink
feat: Add Consent Support to Doubleclick Kit (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexs-mparticle committed Mar 5, 2024
1 parent 159a498 commit 488ac47
Show file tree
Hide file tree
Showing 5 changed files with 954 additions and 9 deletions.
24 changes: 23 additions & 1 deletion src/common.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
function Common() {}
var ConsentHandler = require('./consent');
function Common() {
this.consentMappings = [];
this.consentPayloadDefaults = {};
this.consentPayloadAsString = '';

this.consentHandler = new ConsentHandler(this);
}

Common.prototype.eventMapping = {};
Common.prototype.customVariablesMappings = {};
Expand Down Expand Up @@ -39,6 +46,21 @@ Common.prototype.sendGtag = function(type, properties, isInitialization) {
}
};

Common.prototype.sendGtagConsent = function (type, payload) {
function gtag() {
window.dataLayer.push(arguments);
}
gtag('consent', type, payload);
};

Common.prototype.cloneObject = function (obj) {
return JSON.parse(JSON.stringify(obj));
};

Common.prototype.isEmpty = function isEmpty(value) {
return value == null || !(Object.keys(value) || value).length;
};

module.exports = Common;

function findValueInMapping(jsHash, mapping) {
Expand Down
110 changes: 110 additions & 0 deletions src/consent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
var googleConsentValues = {
// Server Integration uses 'Unspecified' as a value when the setting is 'not set'.
// However, this is not used by Google's Web SDK. We are referencing it here as a comment
// as a record of this distinction and for posterity.
// If Google ever adds this for web, the line can just be uncommented to support this.
//
// Docs:
// Web: https://developers.google.com/tag-platform/gtagjs/reference#consent
// S2S: https://developers.google.com/google-ads/api/reference/rpc/v15/ConsentStatusEnum.ConsentStatus
//
// Unspecified: 'unspecified',
Denied: 'denied',
Granted: 'granted',
};

// Declares list of valid Google Consent Properties
var googleConsentProperties = [
'ad_storage',
'ad_user_data',
'ad_personalization',
'analytics_storage',
];

function ConsentHandler(common) {
this.common = common || {};
}

ConsentHandler.prototype.getUserConsentState = function () {
var userConsentState = {};

if (mParticle.Identity && mParticle.Identity.getCurrentUser) {
var currentUser = mParticle.Identity.getCurrentUser();

if (!currentUser) {
return {};
}

var consentState =
mParticle.Identity.getCurrentUser().getConsentState();

if (consentState && consentState.getGDPRConsentState) {
userConsentState = consentState.getGDPRConsentState();
}
}

return userConsentState;
};

ConsentHandler.prototype.getEventConsentState = function (eventConsentState) {
return eventConsentState && eventConsentState.getGDPRConsentState
? eventConsentState.getGDPRConsentState()
: {};
};

ConsentHandler.prototype.getConsentSettings = function () {
var consentSettings = {};

var googleToMpConsentSettingsMapping = {
ad_user_data: 'defaultAdUserDataConsent',
ad_personalization: 'defaultAdPersonalizationConsent',
ad_storage: 'defaultAdStorageConsentWeb',
analytics_storage: 'defaultAnalyticsStorageConsentWeb',
};

var settings = this.common.settings;

Object.keys(googleToMpConsentSettingsMapping).forEach(function (
googleConsentKey
) {
var mpConsentSettingKey =
googleToMpConsentSettingsMapping[googleConsentKey];
var googleConsentValuesKey = settings[mpConsentSettingKey];

if (googleConsentValuesKey && mpConsentSettingKey) {
consentSettings[googleConsentKey] =
googleConsentValues[googleConsentValuesKey];
}
});

return consentSettings;
};

ConsentHandler.prototype.generateConsentStatePayloadFromMappings = function (
consentState,
mappings
) {
if (!mappings) return {};

var payload = this.common.cloneObject(this.common.consentPayloadDefaults);

for (var i = 0; i <= mappings.length - 1; i++) {
var mappingEntry = mappings[i];
var mpMappedConsentName = mappingEntry.map;
var googleMappedConsentName = mappingEntry.value;

if (
consentState[mpMappedConsentName] &&
googleConsentProperties.indexOf(googleMappedConsentName) !== -1
) {
payload[googleMappedConsentName] = consentState[mpMappedConsentName]
.Consented
? googleConsentValues.Granted
: googleConsentValues.Denied;
}
}

return payload;
};

module.exports = ConsentHandler;
30 changes: 29 additions & 1 deletion src/event-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,35 @@ function EventHandler(common) {
this.common = common || {};
}

EventHandler.prototype.logEvent = function(event) {
EventHandler.prototype.maybeSendConsentUpdateToGa4 = function (event) {
// If consent payload is empty,
// we never sent an initial default consent state
// so we shouldn't send an update.
if (this.common.consentPayloadAsString && this.common.consentMappings) {
var eventConsentState = this.common.consentHandler.getEventConsentState(
event.ConsentState
);

if (!this.common.isEmpty(eventConsentState)) {
var updatedConsentPayload =
this.common.consentHandler.generateConsentStatePayloadFromMappings(
eventConsentState,
this.common.consentMappings
);

var eventConsentAsString = JSON.stringify(updatedConsentPayload);

if (eventConsentAsString !== this.common.consentPayloadAsString) {
this.common.sendGtagConsent('update', updatedConsentPayload);
this.common.consentPayloadAsString = eventConsentAsString;
}
}
}
};

EventHandler.prototype.logEvent = function (event) {
this.maybeSendConsentUpdateToGa4(event);

var gtagProperties = {};
this.common.setCustomVariables(event, gtagProperties);
var eventMapping = this.common.getEventMapping(event);
Expand Down
41 changes: 38 additions & 3 deletions src/initialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ var initialization = {
initForwarder: function(settings, testMode, userAttributes, userIdentities, processEvent, eventQueue, isInitialized, common) {
common.settings = settings;

if (common.settings.consentMappingWeb) {
common.consentMappings = parseSettingsString(
common.settings.consentMappingWeb
);
} else {
// Ensures consent mappings is an empty array
// for future use
common.consentMappings = [];
common.consentPayloadDefaults = {};
common.consentPayloadAsString = '';
}

window.dataLayer = window.dataLayer || [];
if (!testMode) {
var url = 'https://www.googletagmanager.com/gtag/js?id=' + settings.advertiserId;
Expand All @@ -30,13 +42,32 @@ var initialization = {
} else {
initializeGoogleDFP(common, settings, isInitialized);
}
}

common.consentPayloadDefaults =
common.consentHandler.getConsentSettings();
var initialConsentState = common.consentHandler.getUserConsentState();
var defaultConsentPayload =
common.consentHandler.generateConsentStatePayloadFromMappings(
initialConsentState,
common.consentMappings
);

if (!common.isEmpty(defaultConsentPayload)) {
common.consentPayloadAsString = JSON.stringify(
defaultConsentPayload
);

common.sendGtagConsent('default', defaultConsentPayload);
}
},
};

function initializeGoogleDFP(common, settings, isInitialized) {
common.eventMapping = JSON.parse(settings.eventMapping.replace(/&quot;/g, '\"'));
common.eventMapping = parseSettingsString(settings.eventMapping);

common.customVariablesMappings = JSON.parse(settings.customVariables.replace(/&quot;/g, '\"')).reduce(function(a, b) {
common.customVariablesMappings = parseSettingsString(
settings.customVariables
).reduce(function (a, b) {
a[b.map] = b.value;
return a;
}, {});
Expand All @@ -46,4 +77,8 @@ function initializeGoogleDFP(common, settings, isInitialized) {
isInitialized = true;
}

function parseSettingsString(settingsString) {
return JSON.parse(settingsString.replace(/&quot;/g, '"'));
}

module.exports = initialization;
Loading

0 comments on commit 488ac47

Please sign in to comment.