diff --git a/package.json b/package.json index a7bf578..d14851d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "scripts": { "build": "rollup --config rollup.config.js", "watch": "rollup --config rollup.config.js -w", - "test": "node test/boilerplate/test-karma.js" + "test": "node test/boilerplate/test-karma.js", + "watch:tests": "ENVIRONMENT=production rollup --config rollup.test.config.js -w", + "test:debug": "npm run build && npm run build:test && DEBUG=true karma start test/karma.config.js" }, "devDependencies": { "@rollup/plugin-commonjs": "22.0.1", diff --git a/rollup.test.config.js b/rollup.test.config.js new file mode 100644 index 0000000..70f94dc --- /dev/null +++ b/rollup.test.config.js @@ -0,0 +1,22 @@ +// https://go.mparticle.com/work/SQDSDKS-6875 +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; + +export default [ + { + input: 'test/tests.js', + output: { + file: 'test/test-bundle.js', + format: 'iife', + exports: 'named', + name: 'mpBrazeKit', + strict: false, + }, + plugins: [ + resolve({ + browser: true, + }), + commonjs(), + ], + }, +]; diff --git a/src/BrazeKit-dev.js b/src/BrazeKit-dev.js index 7631162..234a8be 100644 --- a/src/BrazeKit-dev.js +++ b/src/BrazeKit-dev.js @@ -43,6 +43,8 @@ var constructor = function () { forwarderSettings, options = {}, reportingService, + hasConsentMappings, + parsedConsentMappings, mpCustomFlags; self.name = name; @@ -72,6 +74,13 @@ var constructor = function () { var bundleCommerceEventData = false; var forwardSkuAsProductName = false; + var brazeConsentKeys = [ + '$google_ad_user_data', + '$google_ad_personalization' + ]; + + var latestUserBrazeConsentString; + // A purchase event can either log a single event with all products // or multiple purchase events (one per product) function logPurchaseEvent(event) { @@ -364,6 +373,7 @@ var constructor = function () { /**************************/ function processEvent(event) { var reportEvent = false; + maybeSetConsentBeforeEventLogged(event); if (event.EventDataType == MessageType.Commerce) { reportEvent = logCommerceEvent(event); @@ -709,6 +719,110 @@ var constructor = function () { } } + function prepareInitialConsent(user) { + var userConsentState = getUserConsentState(user); + + var currentConsentPayload = generateBrazeConsentStatePayload( + userConsentState + ); + + if (!isEmpty(currentConsentPayload)) { + latestUserBrazeConsentString = JSON.stringify( + currentConsentPayload + ); + + setConsentOnBraze(currentConsentPayload); + } + } + + function setConsentOnBraze(currentConsentPayload) { + for (var key in currentConsentPayload) { + braze + .getUser() + .setCustomUserAttribute(key, currentConsentPayload[key]); + } + } + + function maybeSetConsentBeforeEventLogged(event) { + if (latestUserBrazeConsentString && !isEmpty(parsedConsentMappings)) { + var eventConsentState = getEventConsentState(event.ConsentState); + + if (!isEmpty(eventConsentState)) { + var eventBrazeConsent = generateBrazeConsentStatePayload( + eventConsentState + ); + var eventBrazeConsentAsString = JSON.stringify( + eventBrazeConsent + ); + + if ( + eventBrazeConsentAsString !== latestUserBrazeConsentString + ) { + setConsentOnBraze(eventBrazeConsent); + latestUserBrazeConsentString = eventBrazeConsentAsString; + } + } + } + } + + function getEventConsentState(eventConsentState) { + return eventConsentState && eventConsentState.getGDPRConsentState + ? eventConsentState.getGDPRConsentState() + : {}; + } + + function generateBrazeConsentStatePayload(consentState) { + if (!parsedConsentMappings) return {}; + + var payload = {}; + + // These are Braze's consent constants for Braze's Audience Sync to Google + // https://www.braze.com/docs/partners/canvas_steps/google_audience_sync + + var googleToBrazeConsentMap = { + google_ad_user_data: '$google_ad_user_data', + google_ad_personalization: '$google_ad_personalization', + }; + + for (var i = 0; i <= parsedConsentMappings.length - 1; i++) { + var mappingEntry = parsedConsentMappings[i]; + // Although consent purposes can be inputted into the UI in any casing + // the SDK will automatically lowercase them to prevent pseudo-duplicate + // consent purposes, so we call `toLowerCase` on the consentMapping purposes here + var mpMappedConsentName = mappingEntry.map.toLowerCase(); + // that mappingEntry.value returned from the server does not have a $ appended, so we have to add it + var brazeMappedConsentName = + googleToBrazeConsentMap[mappingEntry.value]; + + if ( + consentState[mpMappedConsentName] && + brazeMappedConsentName && + brazeConsentKeys.indexOf(brazeMappedConsentName) !== -1 + ) { + payload[brazeMappedConsentName] = + consentState[mpMappedConsentName].Consented; + } + } + + return payload; + } + + function getUserConsentState(user) { + var userConsentState = {}; + + var consentState = user.getConsentState(); + + if (consentState && consentState.getGDPRConsentState) { + userConsentState = consentState.getGDPRConsentState(); + } + + return userConsentState; + } + + function parseConsentSettingsString(consentMappingString) { + return JSON.parse(consentMappingString.replace(/"/g, '"')); + } + function initForwarder( settings, service, @@ -755,6 +869,15 @@ var constructor = function () { forwarderSettings.serviceWorkerLocation; } + if (forwarderSettings.consentMappingSDK) { + parsedConsentMappings = parseConsentSettingsString( + forwarderSettings.consentMappingSDK + ); + if (parsedConsentMappings.length) { + hasConsentMappings = true; + } + } + var cluster = forwarderSettings.cluster || forwarderSettings.dataCenterLocation; @@ -804,6 +927,9 @@ var constructor = function () { if (currentUser && mpid) { onUserIdentified(currentUser); + if (hasConsentMappings) { + prepareInitialConsent(currentUser); + } } openSession(forwarderSettings); @@ -992,6 +1118,10 @@ function isObject(val) { ); } +function isEmpty(value) { + return value == null || !(Object.keys(value) || value).length; +} + module.exports = { register: register, getVersion: function () { diff --git a/test/tests.js b/test/tests.js index 5b598e6..f53057a 100644 --- a/test/tests.js +++ b/test/tests.js @@ -79,8 +79,7 @@ describe('Braze Forwarder', function() { this.yearOfBirth = null; this.monthOfBirth = null; this.dayOfBirth = null; - this.customAttribute = null; - this.customAttributeValue = null; + this.customAttributes = {}; this.customAttributeSet = false; @@ -136,8 +135,7 @@ describe('Braze Forwarder', function() { this.setCustomUserAttribute = function(key, value) { self.customAttributeSet = true; - self.customAttribute = key; - self.customAttributeValue = !value ? '' : value; + self.customAttributes[key] = value; }; }, MockBraze = function() { @@ -254,7 +252,25 @@ describe('Braze Forwarder', function() { return { userIdentities: { customerid: 'abc', - email: 'email@gmail.com' + email: 'email@gmail.com', + }, + }; + }, + getConsentState: function() { + return { + getGDPRConsentState: function() { + return { + 'test purpose': { + Consented: false, + Timestamp: 1, + Document: 'some_consent', + }, + 'test 2': { + Consented: false, + Timestamp: 1, + Document: 'test_consent', + }, + }; }, }; }, @@ -1103,30 +1119,31 @@ describe('Braze Forwarder', function() { it('should set a custom user attribute', function() { mParticle.forwarder.setUserAttribute('test', 'result'); window.braze.getUser().should.have.property('customAttributeSet', true); - window.braze.getUser().customAttribute.should.equal('test'); - window.braze.getUser().customAttributeValue.should.equal('result'); + window.braze.getUser().customAttributes['test'].should.equal('result');; }); it('should set a custom user attribute of diffferent types', function() { mParticle.forwarder.setUserAttribute('testint', 3); - window.braze.getUser().customAttributeValue.should.equal(3); + window.braze.getUser().customAttributes['testint'].should.equal(3); var d = new Date(); mParticle.forwarder.setUserAttribute('testdate', d); - window.braze.getUser().customAttributeValue.should.equal(d); + window.braze.getUser().customAttributes['testdate'].should.equal(d); mParticle.forwarder.setUserAttribute('testarray', ['3']); - window.braze.getUser().customAttributeValue[0].should.equal('3'); + window.braze.getUser().customAttributes['testarray'][0].should.equal('3'); }); it('should sanitize a custom user attribute', function() { mParticle.forwarder.setUserAttribute('$$tes$t', '$$res$ult'); window.braze.getUser().should.have.property('customAttributeSet', true); - window.braze.getUser().customAttribute.should.equal('tes$t'); - window.braze.getUser().customAttributeValue.should.equal('res$ult'); + window.braze + .getUser() + .customAttributes['tes$t'].should.equal('res$ult'); }); it('should sanitize a custom user attribute array', function() { mParticle.forwarder.setUserAttribute('att array', ['1', '$2$']); - window.braze.getUser().customAttributeValue[1].should.equal('2$'); + window.braze.getUser().customAttributes['att array'][0].should.equal('1'); + window.braze.getUser().customAttributes['att array'][1].should.equal('2$'); }); it('should not set a custom user attribute array on an invalid array', function() { @@ -1145,15 +1162,13 @@ describe('Braze Forwarder', function() { it('should remove custom user attributes', function() { mParticle.forwarder.setUserAttribute('test', 'result'); mParticle.forwarder.removeUserAttribute('test'); - window.braze.getUser().customAttribute.should.equal('test'); - window.braze.getUser().customAttributeValue.should.equal(''); + (window.braze.getUser().customAttributes['test'] === null).should.equal(true); }); it('should remove custom user attributes', function() { mParticle.forwarder.setUserAttribute('$$test', '$res$ul$t'); mParticle.forwarder.removeUserAttribute('$test'); - window.braze.getUser().customAttribute.should.equal('test'); - window.braze.getUser().customAttributeValue.should.equal(''); + (window.braze.getUser().customAttributes['test'] === null).should.equal(true); }); it('should not set date of birth if passed an invalid value', function() { @@ -1849,6 +1864,160 @@ user.getUserIdentities is not a function,\n`; window.braze.should.have.property('openSessionCalled', true); }); + describe('consent', function() { + beforeEach(function() { + window.braze = new MockBraze(); + }); + // consentMappingSdk below parses to: + // [{ + // map: 'Test Purpose', + // value: 'google_ad_personalization' + // }, + // { + // map: 'Test 2, + // value 'google_ad_user_data' + // }] + const consentMappingSDK = + '[{"jsmap":null,"map":"Test Purpose","maptype":"ConsentPurposes","value":"google_ad_personalization"},{"jsmap":null,"map":"Test 2","maptype":"ConsentPurposes","value":"google_ad_user_data"}]'; + + it('should not call setCustomUserAttribute on user if there is no consentMappingSdk setting', function() { + mParticle.forwarder.init({ + apiKey: '123456', + userIdentificationType: 'MPID', + }); + + window.braze.user.customAttributeSet = false; + }); + + it('should call setCustomUserAttribute on user if consent is set and consentMappingSdk is set, and there are mapped users', function() { + mParticle.forwarder.init({ + apiKey: '123456', + userIdentificationType: 'MPID', + consentMappingSDK: consentMappingSDK, + }); + + window.braze.user.customAttributeSet = true; + + var expectedResult = { + $google_ad_personalization: false, + $google_ad_user_data: false, + }; + + window.braze.user.customAttributes.should.deepEqual(expectedResult); + }); + + it('should not call setCustomUserAttribute on user if consent is set and consentMappingSdk is set, but user consent does not map to consentMappingSdk', function() { + // The below changes consent mapping purposes to Foo Purpose and Test 1 for this one test + const consentMappingSDK = + '[{"jsmap":null,"map":"Foo Purpose","maptype":"ConsentPurposes","value":"google_ad_personalization"},{"jsmap":null,"map":"Test 1","maptype":"ConsentPurposes","value":"google_ad_user_data"}]'; + mParticle.forwarder.init({ + apiKey: '123456', + userIdentificationType: 'MPID', + consentMappingSDK: consentMappingSDK, + }); + + window.braze.user.customAttributeSet.should.equal(false); + }); + + it('should not update user consent if a customer does not change consent before logging an event', function() { + mParticle.forwarder.init({ + apiKey: '123456', + userIdentificationType: 'MPID', + consentMappingSDK: consentMappingSDK, + }); + + window.braze.user.customAttributeSet.should.equal(true); + + var expectedResult = { + $google_ad_personalization: false, + $google_ad_user_data: false, + }; + + window.braze.user.customAttributes.should.deepEqual(expectedResult); + + // reset braze.user.customAttributes and customAttributeSet for the below tests + window.braze = new MockBraze(); + + window.braze.user.customAttributes.should.deepEqual({}); + window.braze.user.customAttributeSet.should.equal(false); + + mParticle.forwarder.process({ + EventName: 'Test Event', + EventDataType: MessageType.PageEvent, + ConsentState: { + getGDPRConsentState: function () { + return { + 'test purpose': { + Consented: false, + Timestamp: Date.now(), + Document: 'test purpose', + }, + 'test 2': { + Consented: false, + Timestamp: Date.now(), + Document: 'test 2', + }, + }; + }, + }, + }); + + // these settings not being updated from the tests immediately before .process above means that setCustomUserAttribute was not set + window.braze.user.customAttributes.should.deepEqual({}); + window.braze.user.customAttributeSet.should.equal(false); + }); + + it('should update user consent if an event has a different consent than the previously set consent', function() { + mParticle.forwarder.init({ + apiKey: '123456', + userIdentificationType: 'MPID', + consentMappingSDK: consentMappingSDK, + }); + + window.braze.user.customAttributeSet.should.equal(true); + + var expectedResult = { + $google_ad_personalization: false, + $google_ad_user_data: false, + }; + + window.braze.user.customAttributes.should.deepEqual(expectedResult); + + // reset braze.user.customAttributes and customAttributeSet + window.braze = new MockBraze(); + + window.braze.user.customAttributes.should.deepEqual({}); + window.braze.user.customAttributeSet.should.equal(false); + + mParticle.forwarder.process({ + EventName: 'Test Event', + EventDataType: MessageType.PageEvent, + ConsentState: { + getGDPRConsentState: function () { + return { + 'test purpose': { + Consented: true, + Timestamp: Date.now(), + Document: 'test purpose', + }, + 'test 2': { + Consented: true, + Timestamp: Date.now(), + Document: 'test 2', + }, + }; + }, + }, + }); + + window.braze.user.customAttributes.should.deepEqual({ + $google_ad_personalization: true, + $google_ad_user_data: true, + }); + window.braze.user.customAttributeSet.should.equal(true); + }); + }); + describe('promotion events', function() { const mpPromotionEvent = { EventName: 'eCommerce - PromotionClick',