From cbbf1be5cbf1ad2d43f69992a443d3bbba9ddf64 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Mon, 11 Aug 2025 23:01:45 -0400 Subject: [PATCH 1/8] one test failing --- src/Rokt-Kit.js | 20 +++++++++--- test/src/tests.js | 77 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index f92bca2..0e44a44 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -23,6 +23,7 @@ var constructor = function () { var PerformanceMarks = { RoktScriptAppended: 'mp:RoktScriptAppended', }; + var EMAIL_IDENTITY = 'email'; self.name = name; self.moduleId = moduleId; @@ -157,7 +158,7 @@ var constructor = function () { var userIdentities = filteredUser.getUserIdentities().userIdentities; - return replaceOtherWithEmailsha256(userIdentities); + return replaceOtherIdentityWithEmailsha256(userIdentities); } function returnLocalSessionAttributes() { @@ -172,16 +173,26 @@ var constructor = function () { return window.mParticle.Rokt.getLocalSessionAttributes(); } - function replaceOtherWithEmailsha256(_data) { + function replaceOtherIdentityWithEmailsha256(_data) { var data = mergeObjects({}, _data || {}); if (_data.hasOwnProperty(OTHER_IDENTITY)) { data[EMAIL_SHA256_IDENTITY] = _data[OTHER_IDENTITY]; delete data[OTHER_IDENTITY]; + delete data[EMAIL_IDENTITY]; } return data; } + // function replaceEmailIdentityWithEmailsha256(_data) { + // var data = mergeObjects({}, _data || {}); + // if (_data.hasOwnProperty(EMAIL_SHA256_IDENTITY)) { + // delete data[EMAIL_IDENTITY]; + // } + + // return data; + // } + /** * Selects placements for Rokt Web SDK with merged attributes, filters, and experimentation options * @see https://docs.rokt.com/developers/integration-guides/web/library/select-placements-options/ @@ -227,19 +238,20 @@ var constructor = function () { : {}; var filteredUserIdentities = returnUserIdentities(filteredUser); + console.log(filteredUserIdentities); var localSessionAttributes = returnLocalSessionAttributes(); var selectPlacementsAttributes = mergeObjects( filteredUserIdentities, - replaceOtherWithEmailsha256(filteredAttributes), + filteredAttributes, optimizelyAttributes, localSessionAttributes, { mpid: mpid, } ); - + console.log(selectPlacementsAttributes); var selectPlacementsOptions = mergeObjects(options, { attributes: selectPlacementsAttributes, }); diff --git a/test/src/tests.js b/test/src/tests.js index 87367ef..68b68f4 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -999,7 +999,7 @@ describe('Rokt Forwarder', () => { }); }); - describe('Identity handling', () => { + describe.only('Identity handling', () => { beforeEach(() => { window.Rokt = new MockRoktForwarder(); window.mParticle.Rokt = window.Rokt; @@ -1420,7 +1420,7 @@ describe('Rokt Forwarder', () => { await window.mParticle.forwarder.selectPlacements({ identifier: 'test-placement', attributes: { - other: 'sha256-test@gmail.com', + other: 'other-attribute', }, }); @@ -1428,13 +1428,13 @@ describe('Rokt Forwarder', () => { { 'test-attribute': 'test-value', customerid: 'customer123', - emailsha256: 'sha256-test@gmail.com', + other: 'other-attribute', mpid: '123', } ); }); - it('should prioritize other passed to selectPlacements over other in userIdentities', async () => { + it('should pass the attribute other in selectPlacements directly to Rokt', async () => { window.mParticle.Rokt.filters = { userAttributeFilters: [], filterUserAttributes: function (attributes) { @@ -1448,7 +1448,7 @@ describe('Rokt Forwarder', () => { return { userIdentities: { customerid: 'customer123', - other: 'not-prioritized-from-userIdentities@gmail.com', + other: 'other-id', }, }; }, @@ -1486,7 +1486,7 @@ describe('Rokt Forwarder', () => { await window.mParticle.forwarder.selectPlacements({ identifier: 'test-placement', attributes: { - other: 'prioritized-from-selectPlacements@gmail.com', + other: 'continues-to-exist', }, }); @@ -1494,12 +1494,73 @@ describe('Rokt Forwarder', () => { { 'test-attribute': 'test-value', customerid: 'customer123', - emailsha256: - 'prioritized-from-selectPlacements@gmail.com', + other: 'continues-to-exist', + emailsha256: 'other-id', mpid: '123', } ); }); + + it('should map email identity to emailsha256 when only email exists', async () => { + window.mParticle.Rokt.filters = { + userAttributeFilters: [], + filterUserAttributes: function () { + return {}; + }, + filteredUser: { + getMPID: function () { + return '456'; + }, + getUserIdentities: function () { + return { + userIdentities: { + email: 'test@example.com', + }, + }; + }, + }, + }; + + // Set up the createLauncher to properly resolve asynchronously + window.Rokt.createLauncher = async function () { + return Promise.resolve({ + selectPlacements: function (options) { + window.mParticle.Rokt.selectPlacementsOptions = + options; + window.mParticle.Rokt.selectPlacementsCalled = true; + }, + }); + }; + + await window.mParticle.forwarder.init( + { + accountId: '123456', + }, + reportService.cb, + true, + null, + {} + ); + + // Wait for initialization to complete (after launcher is created) + await waitForCondition(() => { + return window.mParticle.forwarder.isInitialized; + }); + + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: { + emailsha256: 'hashed-email-value', + }, + }); + + window.Rokt.selectPlacementsOptions.attributes.should.deepEqual( + { + emailsha256: 'hashed-email-value', + mpid: '456', + } + ); + }); }); }); From 08268028c56f29780a7174e30266ba4f2740e702 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Wed, 13 Aug 2025 16:43:31 -0400 Subject: [PATCH 2/8] remove email from selectPlacements when hashedemail exists --- src/Rokt-Kit.js | 19 +++++++++---------- test/src/tests.js | 8 ++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index 0e44a44..b001b28 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -184,14 +184,14 @@ var constructor = function () { return data; } - // function replaceEmailIdentityWithEmailsha256(_data) { - // var data = mergeObjects({}, _data || {}); - // if (_data.hasOwnProperty(EMAIL_SHA256_IDENTITY)) { - // delete data[EMAIL_IDENTITY]; - // } + function sanitizeIdentities(_data) { + var data = mergeObjects({}, _data || {}); + if (_data.hasOwnProperty(EMAIL_SHA256_IDENTITY)) { + delete data[EMAIL_IDENTITY]; + } - // return data; - // } + return data; + } /** * Selects placements for Rokt Web SDK with merged attributes, filters, and experimentation options @@ -238,7 +238,6 @@ var constructor = function () { : {}; var filteredUserIdentities = returnUserIdentities(filteredUser); - console.log(filteredUserIdentities); var localSessionAttributes = returnLocalSessionAttributes(); @@ -251,9 +250,9 @@ var constructor = function () { mpid: mpid, } ); - console.log(selectPlacementsAttributes); + var selectPlacementsOptions = mergeObjects(options, { - attributes: selectPlacementsAttributes, + attributes: sanitizeIdentities(selectPlacementsAttributes), }); return self.launcher.selectPlacements(selectPlacementsOptions); diff --git a/test/src/tests.js b/test/src/tests.js index 68b68f4..20c866a 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -999,7 +999,7 @@ describe('Rokt Forwarder', () => { }); }); - describe.only('Identity handling', () => { + describe('Identity handling', () => { beforeEach(() => { window.Rokt = new MockRoktForwarder(); window.mParticle.Rokt = window.Rokt; @@ -1501,11 +1501,11 @@ describe('Rokt Forwarder', () => { ); }); - it('should map email identity to emailsha256 when only email exists', async () => { + it('should remove email identity if emailsha256 is passed through selectPlacements', async () => { window.mParticle.Rokt.filters = { userAttributeFilters: [], - filterUserAttributes: function () { - return {}; + filterUserAttributes: function (attributes) { + return attributes; }, filteredUser: { getMPID: function () { From 51037dd04346ba99d57bc3c04c12b0579502a67a Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Thu, 14 Aug 2025 13:46:45 -0400 Subject: [PATCH 3/8] remove redundant deletion of email --- src/Rokt-Kit.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index b001b28..ae2734c 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -178,7 +178,6 @@ var constructor = function () { if (_data.hasOwnProperty(OTHER_IDENTITY)) { data[EMAIL_SHA256_IDENTITY] = _data[OTHER_IDENTITY]; delete data[OTHER_IDENTITY]; - delete data[EMAIL_IDENTITY]; } return data; From 1909e54e732418383dbdf7035f2ec133c38201be Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Mon, 15 Sep 2025 22:29:06 -0400 Subject: [PATCH 4/8] Refactor to use hashedEmailUserIdentityType --- src/Rokt-Kit.js | 26 ++++++--- test/src/tests.js | 131 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 8 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index ae2734c..ebf06a0 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -19,12 +19,14 @@ var moduleId = 181; var constructor = function () { var self = this; var EMAIL_SHA256_IDENTITY = 'emailsha256'; - var OTHER_IDENTITY = 'other'; var PerformanceMarks = { RoktScriptAppended: 'mp:RoktScriptAppended', }; var EMAIL_IDENTITY = 'email'; + // Dynamic identity type for Rokt's emailsha256 identity value which MP doesn't support - will be set during initialization + var OTHER_IDENTITY; + self.name = name; self.moduleId = moduleId; self.isInitialized = false; @@ -90,6 +92,12 @@ var constructor = function () { placementEventMapping ); + // Set dynamic OTHER_IDENTITY based on server settings + // Convert to lowercase since server sends TitleCase (e.g., 'Other' -> 'other') + if (settings.hashedEmailUserIdentityType) { + OTHER_IDENTITY = settings.hashedEmailUserIdentityType.toLowerCase(); + } + var domain = window.mParticle.Rokt.domain; var launcherOptions = mergeObjects( {}, @@ -157,7 +165,6 @@ var constructor = function () { } var userIdentities = filteredUser.getUserIdentities().userIdentities; - return replaceOtherIdentityWithEmailsha256(userIdentities); } @@ -173,14 +180,15 @@ var constructor = function () { return window.mParticle.Rokt.getLocalSessionAttributes(); } - function replaceOtherIdentityWithEmailsha256(_data) { - var data = mergeObjects({}, _data || {}); - if (_data.hasOwnProperty(OTHER_IDENTITY)) { - data[EMAIL_SHA256_IDENTITY] = _data[OTHER_IDENTITY]; - delete data[OTHER_IDENTITY]; + function replaceOtherIdentityWithEmailsha256(userIdentities) { + var newUserIdentities = mergeObjects({}, userIdentities || {}); + if (userIdentities.hasOwnProperty(OTHER_IDENTITY)) { + newUserIdentities[EMAIL_SHA256_IDENTITY] = + userIdentities[OTHER_IDENTITY]; + delete newUserIdentities[OTHER_IDENTITY]; } - return data; + return newUserIdentities; } function sanitizeIdentities(_data) { @@ -471,6 +479,8 @@ var constructor = function () { } }; +function getOtherIdentityType(settings) {} + function generateIntegrationName(customIntegrationName) { var coreSdkVersion = window.mParticle.getVersion(); var kitVersion = process.env.PACKAGE_VERSION; diff --git a/test/src/tests.js b/test/src/tests.js index 20c866a..47ca34c 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -1344,6 +1344,7 @@ describe('Rokt Forwarder', () => { await window.mParticle.forwarder.init( { accountId: '123456', + hashedEmailUserIdentityType: 'Other', }, reportService.cb, true, @@ -1501,6 +1502,136 @@ describe('Rokt Forwarder', () => { ); }); + it('should use custom hashedEmailUserIdentityType when provided in settings', async () => { + window.mParticle.Rokt.filters = { + userAttributeFilters: [], + filterUserAttributes: function (attributes) { + return attributes; + }, + filteredUser: { + getMPID: function () { + return '789'; + }, + getUserIdentities: function () { + return { + userIdentities: { + // Using 'customerid' as the identity type instead of 'other' + other5: 'hashed-customer-id-value', + }, + }; + }, + }, + }; + + // Set up the createLauncher to properly resolve asynchronously + window.Rokt.createLauncher = async function () { + return Promise.resolve({ + selectPlacements: function (options) { + window.mParticle.Rokt.selectPlacementsOptions = + options; + window.mParticle.Rokt.selectPlacementsCalled = true; + }, + }); + }; + + await window.mParticle.forwarder.init( + { + accountId: '123456', + hashedEmailUserIdentityType: 'Other5', // TitleCase from server + }, + reportService.cb, + true, + null, + {} + ); + + // Wait for initialization to complete (after launcher is created) + await waitForCondition(() => { + return window.mParticle.forwarder.isInitialized; + }); + + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: { + 'test-attribute': 'test-value', + }, + }); + + // Should map customerid from userIdentities to emailsha256 since hashedEmailUserIdentityType was set to 'CustomerID' + window.Rokt.selectPlacementsOptions.attributes.should.deepEqual( + { + 'test-attribute': 'test-value', + emailsha256: 'hashed-customer-id-value', // mapped from customerid in userIdentities + mpid: '789', + } + ); + }); + + it('should not set emailsha256 on final select placements attributes when hashedEmailUserIdentityType is Unassigned', async () => { + window.mParticle.Rokt.filters = { + userAttributeFilters: [], + filterUserAttributes: function (attributes) { + return attributes; + }, + filteredUser: { + getMPID: function () { + return '999'; + }, + getUserIdentities: function () { + return { + userIdentities: { + // Using lowercase identity name that matches the converted OTHER_IDENTITY + other: 'hashed-custom-identity-value', + }, + }; + }, + }, + }; + + // Set up the createLauncher to properly resolve asynchronously + window.Rokt.createLauncher = async function () { + return Promise.resolve({ + selectPlacements: function (options) { + window.mParticle.Rokt.selectPlacementsOptions = + options; + window.mParticle.Rokt.selectPlacementsCalled = true; + }, + }); + }; + + await window.mParticle.forwarder.init( + { + accountId: '123456', + hashedEmailUserIdentityType: 'Unassigned', // Mixed case from server + }, + reportService.cb, + true, + null, + {} + ); + + // Wait for initialization to complete (after launcher is created) + await waitForCondition(() => { + return window.mParticle.forwarder.isInitialized; + }); + + await window.mParticle.forwarder.selectPlacements({ + identifier: 'test-placement', + attributes: { + 'test-attr': 'test-value', + }, + }); + + // Should map customidentity from userIdentities to emailsha256 (TitleCase converted to lowercase) + window.Rokt.selectPlacementsOptions.attributes.should.deepEqual( + { + 'test-attr': 'test-value', + other: 'hashed-custom-identity-value', + mpid: '999', + } + ); + }); + it('should remove email identity if emailsha256 is passed through selectPlacements', async () => { window.mParticle.Rokt.filters = { userAttributeFilters: [], From deda76f0a9840ef17ae574db7a94a2e7d73c3e79 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Thu, 18 Sep 2025 14:34:22 -0400 Subject: [PATCH 5/8] remove unused function --- src/Rokt-Kit.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index ebf06a0..7c644a6 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -479,8 +479,6 @@ var constructor = function () { } }; -function getOtherIdentityType(settings) {} - function generateIntegrationName(customIntegrationName) { var coreSdkVersion = window.mParticle.getVersion(); var kitVersion = process.env.PACKAGE_VERSION; From 8e3af5707e7b248099360b5ccd1ca268c38bbb7b Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Mon, 22 Sep 2025 12:06:26 -0400 Subject: [PATCH 6/8] rename other/email variables for clarity --- src/Rokt-Kit.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index 7c644a6..76b3bf7 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -18,14 +18,15 @@ var moduleId = 181; var constructor = function () { var self = this; - var EMAIL_SHA256_IDENTITY = 'emailsha256'; var PerformanceMarks = { RoktScriptAppended: 'mp:RoktScriptAppended', }; - var EMAIL_IDENTITY = 'email'; - // Dynamic identity type for Rokt's emailsha256 identity value which MP doesn't support - will be set during initialization - var OTHER_IDENTITY; + var EMAIL_SHA256_KEY = 'emailsha256'; + var EMAIL_KEY = 'email'; + + // Dynamic identity type for Rokt's emailsha256 identity value which MP doesn't natively support - will be set during initialization + var MAPPED_EMAIL_SHA256_IDENTITY; self.name = name; self.moduleId = moduleId; @@ -95,7 +96,8 @@ var constructor = function () { // Set dynamic OTHER_IDENTITY based on server settings // Convert to lowercase since server sends TitleCase (e.g., 'Other' -> 'other') if (settings.hashedEmailUserIdentityType) { - OTHER_IDENTITY = settings.hashedEmailUserIdentityType.toLowerCase(); + MAPPED_EMAIL_SHA256_IDENTITY = + settings.hashedEmailUserIdentityType.toLowerCase(); } var domain = window.mParticle.Rokt.domain; @@ -182,10 +184,10 @@ var constructor = function () { function replaceOtherIdentityWithEmailsha256(userIdentities) { var newUserIdentities = mergeObjects({}, userIdentities || {}); - if (userIdentities.hasOwnProperty(OTHER_IDENTITY)) { - newUserIdentities[EMAIL_SHA256_IDENTITY] = - userIdentities[OTHER_IDENTITY]; - delete newUserIdentities[OTHER_IDENTITY]; + if (userIdentities.hasOwnProperty(MAPPED_EMAIL_SHA256_IDENTITY)) { + newUserIdentities[EMAIL_SHA256_KEY] = + userIdentities[MAPPED_EMAIL_SHA256_IDENTITY]; + delete newUserIdentities[MAPPED_EMAIL_SHA256_IDENTITY]; } return newUserIdentities; @@ -193,8 +195,8 @@ var constructor = function () { function sanitizeIdentities(_data) { var data = mergeObjects({}, _data || {}); - if (_data.hasOwnProperty(EMAIL_SHA256_IDENTITY)) { - delete data[EMAIL_IDENTITY]; + if (_data.hasOwnProperty(EMAIL_SHA256_KEY)) { + delete data[EMAIL_KEY]; } return data; From 707d9db48ef31780ffc8d9a639d9c1a4f2a49489 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Thu, 25 Sep 2025 22:15:45 -0400 Subject: [PATCH 7/8] Rename variables, update test name --- src/Rokt-Kit.js | 14 +++++++------- test/src/tests.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index 76b3bf7..d65ab4e 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -26,7 +26,7 @@ var constructor = function () { var EMAIL_KEY = 'email'; // Dynamic identity type for Rokt's emailsha256 identity value which MP doesn't natively support - will be set during initialization - var MAPPED_EMAIL_SHA256_IDENTITY; + var mappedEmailSha256Key; self.name = name; self.moduleId = moduleId; @@ -96,7 +96,7 @@ var constructor = function () { // Set dynamic OTHER_IDENTITY based on server settings // Convert to lowercase since server sends TitleCase (e.g., 'Other' -> 'other') if (settings.hashedEmailUserIdentityType) { - MAPPED_EMAIL_SHA256_IDENTITY = + mappedEmailSha256Key = settings.hashedEmailUserIdentityType.toLowerCase(); } @@ -184,16 +184,16 @@ var constructor = function () { function replaceOtherIdentityWithEmailsha256(userIdentities) { var newUserIdentities = mergeObjects({}, userIdentities || {}); - if (userIdentities.hasOwnProperty(MAPPED_EMAIL_SHA256_IDENTITY)) { + if (userIdentities.hasOwnProperty(mappedEmailSha256Key)) { newUserIdentities[EMAIL_SHA256_KEY] = - userIdentities[MAPPED_EMAIL_SHA256_IDENTITY]; - delete newUserIdentities[MAPPED_EMAIL_SHA256_IDENTITY]; + userIdentities[mappedEmailSha256Key]; + delete newUserIdentities[mappedEmailSha256Key]; } return newUserIdentities; } - function sanitizeIdentities(_data) { + function sanitizeEmailIdentities(_data) { var data = mergeObjects({}, _data || {}); if (_data.hasOwnProperty(EMAIL_SHA256_KEY)) { delete data[EMAIL_KEY]; @@ -261,7 +261,7 @@ var constructor = function () { ); var selectPlacementsOptions = mergeObjects(options, { - attributes: sanitizeIdentities(selectPlacementsAttributes), + attributes: sanitizeEmailIdentities(selectPlacementsAttributes), }); return self.launcher.selectPlacements(selectPlacementsOptions); diff --git a/test/src/tests.js b/test/src/tests.js index 47ca34c..8a99e00 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -1435,7 +1435,7 @@ describe('Rokt Forwarder', () => { ); }); - it('should pass the attribute other in selectPlacements directly to Rokt', async () => { + it('should pass the attribute `other` in selectPlacements directly to Rokt', async () => { window.mParticle.Rokt.filters = { userAttributeFilters: [], filterUserAttributes: function (attributes) { From 1da3438b3f31158dd3695dfea81b210434013439 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Fri, 26 Sep 2025 11:15:26 -0400 Subject: [PATCH 8/8] Apply suggestions from code review Co-authored-by: Alex S <49695018+alexs-mparticle@users.noreply.github.com> --- src/Rokt-Kit.js | 1 + test/src/tests.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index d65ab4e..f59bcf2 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -167,6 +167,7 @@ var constructor = function () { } var userIdentities = filteredUser.getUserIdentities().userIdentities; + return replaceOtherIdentityWithEmailsha256(userIdentities); } diff --git a/test/src/tests.js b/test/src/tests.js index 8a99e00..a96375b 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -1567,7 +1567,7 @@ describe('Rokt Forwarder', () => { ); }); - it('should not set emailsha256 on final select placements attributes when hashedEmailUserIdentityType is Unassigned', async () => { + it('should NOT set emailsha256 on final select placements attributes when hashedEmailUserIdentityType is Unassigned', async () => { window.mParticle.Rokt.filters = { userAttributeFilters: [], filterUserAttributes: function (attributes) {