From 31b8be7a4ca8705603a4db23af46909673c40171 Mon Sep 17 00:00:00 2001 From: Preston Starkey Date: Mon, 26 Jan 2026 15:11:14 -0500 Subject: [PATCH 1/4] LE - add placement event mapping rules on attributes --- src/Rokt-Kit.js | 163 +++++++++++++- test/src/tests.js | 546 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 706 insertions(+), 3 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index 1e11709..e24ceb2 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -36,8 +36,125 @@ var constructor = function () { self.userAttributes = {}; self.testHelpers = null; self.placementEventMappingLookup = {}; + self.placementEventMappingRulesLookup = {}; self.eventQueue = []; + /** + * Evaluates a condition against an mParticle event.s + * @param {Object} event - The mParticle event to evaluate the condition against. + * @param {Object} condition - The condition to evaluate from mParticle UI config. + * @returns {boolean} True if the condition is met, false otherwise. + */ + function doesConditionMatch(event, condition) { + if (!condition || typeof condition.operator !== 'string') { + return false; + } + + var attribute = condition.attribute; + var operator = condition.operator.toLowerCase(); + var expectedValue = condition.attributeValue; + + var actualValue = + event && event.EventAttributes && event.EventAttributes[attribute]; + + if (operator === 'exists') { + return typeof actualValue !== 'undefined' && actualValue !== null; + } + + // Equals check (type-sensitive) + if (operator === 'equals') { + return actualValue === expectedValue; + } + + // Only explicitly supported string operator + if (operator !== 'contains') { + return false; + } + + // Contains check (string-only, case-sensitive) + if ( + typeof actualValue !== 'string' || + typeof expectedValue !== 'string' + ) { + return false; + } + + return actualValue.indexOf(expectedValue) !== -1; + } + + function doesRuleMatch(event, rule) { + var conditions = rule.conditions; + if (!Array.isArray(conditions)) { + return false; + } + if (conditions.length === 0) { + return true; + } + for (var i = 0; i < conditions.length; i++) { + if (!doesConditionMatch(event, conditions[i])) { + return false; + } + } + return true; + } + + function buildPlacementEventMappingRulesLookup(rules) { + var index = {}; + if (!Array.isArray(rules)) { + return index; + } + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + + var jsmapKey = rule.jsmap; + var value = rule.value; + + if (!index[jsmapKey]) index[jsmapKey] = {}; + if (!index[jsmapKey][value]) index[jsmapKey][value] = []; + + index[jsmapKey][value].push({ + jsmap: jsmapKey, + value: value, + conditions: rule.conditions, + }); + } + return index; + } + + function applyEventMapping(event, jsmap) { + var mapped = self.placementEventMappingLookup[jsmap]; + if (!mapped) { + return; + } + var keys = Array.isArray(mapped) ? mapped : [mapped]; + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var rulesForKey = + self.placementEventMappingRulesLookup && + self.placementEventMappingRulesLookup[jsmap] && + self.placementEventMappingRulesLookup[jsmap][key] + ? self.placementEventMappingRulesLookup[jsmap][key] + : null; + + // If there are rules for (jsmap,key), only set when ALL rules match (AND). + if (rulesForKey && rulesForKey.length) { + var allMatch = true; + for (var j = 0; j < rulesForKey.length; j++) { + if (!doesRuleMatch(event, rulesForKey[j])) { + allMatch = false; + break; + } + } + if (!allMatch) { + continue; + } + } + + window.mParticle.Rokt.setLocalSessionAttribute(key, true); + } + } + /** * Generates the Rokt launcher script URL with optional domain override and extensions * @param {string} domain - The CNAME domain to use for overriding the launcher url @@ -92,6 +209,12 @@ var constructor = function () { placementEventMapping ); + var placementEventMappingRules = parseSettingsString( + settings.placementEventMappingRules + ); + self.placementEventMappingRulesLookup = + buildPlacementEventMappingRulesLookup(placementEventMappingRules); + // Set dynamic OTHER_IDENTITY based on server settings // Convert to lowercase since server sends TitleCase (e.g., 'Other' -> 'other') if (settings.hashedEmailUserIdentityType) { @@ -115,6 +238,9 @@ var constructor = function () { hashEventMessage: hashEventMessage, parseSettingsString: parseSettingsString, generateMappedEventLookup: generateMappedEventLookup, + buildPlacementEventMappingRulesLookup: + buildPlacementEventMappingRulesLookup, + doesConditionMatch: doesConditionMatch, }; attachLauncher(accountId, launcherOptions); return; @@ -319,8 +445,21 @@ var constructor = function () { ); if (self.placementEventMappingLookup[hashedEvent]) { - var mappedValue = self.placementEventMappingLookup[hashedEvent]; - window.mParticle.Rokt.setLocalSessionAttribute(mappedValue, true); + applyEventMapping(event, hashedEvent); + } + + // Allow wildcard event-name mapping (e.g., any ScreenView for a given type/category). + var hashedEventWildcard = hashEventMessage( + event.EventDataType, + event.EventCategory, + '*' + ); + + if ( + hashedEventWildcard !== hashedEvent && + self.placementEventMappingLookup[hashedEventWildcard] + ) { + applyEventMapping(event, hashedEventWildcard); } } @@ -565,8 +704,26 @@ function generateMappedEventLookup(placementEventMapping) { var mappedEvents = {}; for (var i = 0; i < placementEventMapping.length; i++) { var mapping = placementEventMapping[i]; - mappedEvents[mapping.jsmap] = mapping.value; + + var jsmap = mapping.jsmap; + var value = mapping.value; + if (!mappedEvents[jsmap]) { + mappedEvents[jsmap] = []; + } + // Avoid duplicates + if (mappedEvents[jsmap].indexOf(value) === -1) { + mappedEvents[jsmap].push(value); + } } + + var jsmapKeys = Object.keys(mappedEvents); + for (var j = 0; j < jsmapKeys.length; j++) { + var key = jsmapKeys[j]; + if (mappedEvents[key] && mappedEvents[key].length === 1) { + mappedEvents[key] = mappedEvents[key][0]; + } + } + return mappedEvents; } diff --git a/test/src/tests.js b/test/src/tests.js index 0b75846..2029634 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -3148,6 +3148,552 @@ describe('Rokt Forwarder', () => { }); }); + it('should set local session attribute only when rule conditions match (URL contains)', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'saleSeeker', + }, + ]); + + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'saleSeeker', + conditions: [ + { + attribute: 'URL', + operator: 'contains', + attributeValue: 'sale', + }, + ], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + // Non-matching URL => should NOT set saleSeeker + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/home', + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + + // Matching URL => should set saleSeeker + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/sale/items', + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({ + saleSeeker: true, + }); + }); + + it('should apply both exact and wildcard mappings when both are configured', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'exactFlag', + }, + { + jsmap: 'hashed-<30*>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'wildcardFlag', + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + }); + + window.mParticle._Store.localSessionAttributes.should.deepEqual({ + exactFlag: true, + wildcardFlag: true, + }); + }); + + it('should evaluate contains and equals case-sensitively', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'caseSensitive', + }, + ]); + + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'caseSensitive', + conditions: [ + { + attribute: 'URL', + operator: 'contains', + attributeValue: 'Sale', + }, + ], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + // URL contains "sale" but NOT "Sale" => should NOT set + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/sale/items', + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + }); + + it('should evaluate EventAttributes keys case-sensitively', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'attrCaseSensitive', + }, + ]); + + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'attrCaseSensitive', + conditions: [{ attribute: 'url', operator: 'exists' }], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + // Event has URL, rule checks "url" => should NOT match + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/anything', + }, + }); + + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + }); + + it('should require ALL rules for the same (jsmap,key) to match (AND across rules)', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'saleSeeker', + }, + ]); + + // Two separate rules for the same (jsmap,value). Both must match. + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'saleSeeker', + conditions: [ + { + attribute: 'URL', + operator: 'contains', + attributeValue: 'sale', + }, + ], + }, + { + jsmap: 'hashed-<30Browse>-value', + value: 'saleSeeker', + conditions: [ + { + attribute: 'URL', + operator: 'contains', + attributeValue: 'items', + }, + ], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + // Matches only 1/2 rules => should NOT set + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/sale', + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + + // Matches both rules => should set + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/sale/items', + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({ + saleSeeker: true, + }); + }); + + it('should support exists operator for rule conditions', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'hasUrl', + }, + ]); + + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'hasUrl', + conditions: [{ attribute: 'URL', operator: 'exists' }], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/anything', + }, + }); + + window.mParticle._Store.localSessionAttributes.should.deepEqual({ + hasUrl: true, + }); + }); + + it('should treat invalid rule operators as non-matching', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'badOperator', + }, + ]); + + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'badOperator', + conditions: [ + { + attribute: 'URL', + operator: null, + attributeValue: 'sale', + }, + ], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/sale/items', + }, + }); + + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + }); + + it('should treat unknown string operators as non-matching', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'unknownOperator', + }, + ]); + + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'unknownOperator', + conditions: [ + { + attribute: 'URL', + operator: 'starts_with', + attributeValue: 'https://example.com/sale', + }, + ], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/sale/items', + }, + }); + + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + }); + + it('should evaluate equals type-sensitively (numeric)', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'multipleproducts', + }, + ]); + + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'multipleproducts', + conditions: [ + { + attribute: 'number_of_products', + operator: 'equals', + attributeValue: 2, + }, + ], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + // number => should match + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + number_of_products: 2, + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({ + multipleproducts: true, + }); + + // string => should NOT match numeric attributeValue + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + number_of_products: '2', + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + }); + + it('should treat contains as string-only (non-strings do not match)', async () => { + const placementEventMapping = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + map: 'any', + maptype: 'EventClass.Id', + value: 'containsNumber', + }, + ]); + + const placementEventMappingRules = JSON.stringify([ + { + jsmap: 'hashed-<30Browse>-value', + value: 'containsNumber', + conditions: [ + { + attribute: 'number_of_products', + operator: 'contains', + attributeValue: '2', + }, + ], + }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventMapping, + placementEventMappingRules, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + number_of_products: 2, + }, + }); + + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + }); + it('should add the event to the event queue if the kit is not initialized', async () => { await window.mParticle.forwarder.init( { From d4877d78592dcc4653d40f8e7a79204b2f750be3 Mon Sep 17 00:00:00 2001 From: Preston Starkey Date: Tue, 3 Feb 2026 18:21:50 -0500 Subject: [PATCH 2/4] LE - update to use AttributeSelector --- src/Rokt-Kit.js | 218 ++++++++++++------------ test/src/tests.js | 411 +++++++--------------------------------------- 2 files changed, 165 insertions(+), 464 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index e24ceb2..ba6c764 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -36,122 +36,136 @@ var constructor = function () { self.userAttributes = {}; self.testHelpers = null; self.placementEventMappingLookup = {}; - self.placementEventMappingRulesLookup = {}; + self.placementEventAttributeMappingLookup = {}; self.eventQueue = []; - /** - * Evaluates a condition against an mParticle event.s - * @param {Object} event - The mParticle event to evaluate the condition against. - * @param {Object} condition - The condition to evaluate from mParticle UI config. - * @returns {boolean} True if the condition is met, false otherwise. - */ - function doesConditionMatch(event, condition) { + function getEventAttributeValue(event, attributeKey) { + var attributes = event && event.EventAttributes; + if (!attributes) { + return null; + } + + if (typeof attributes[attributeKey] === 'undefined') { + return null; + } + + return attributes[attributeKey]; + } + + function checkAttributeCondition(condition, actualValue) { if (!condition || typeof condition.operator !== 'string') { return false; } - var attribute = condition.attribute; var operator = condition.operator.toLowerCase(); var expectedValue = condition.attributeValue; - var actualValue = - event && event.EventAttributes && event.EventAttributes[attribute]; - if (operator === 'exists') { - return typeof actualValue !== 'undefined' && actualValue !== null; + return actualValue !== null; } - // Equals check (type-sensitive) if (operator === 'equals') { return actualValue === expectedValue; } - // Only explicitly supported string operator - if (operator !== 'contains') { - return false; + if (operator === 'contains') { + if ( + typeof actualValue !== 'string' || + typeof expectedValue !== 'string' + ) { + return false; + } + return actualValue.indexOf(expectedValue) !== -1; } - // Contains check (string-only, case-sensitive) - if ( - typeof actualValue !== 'string' || - typeof expectedValue !== 'string' - ) { + return false; + } + + function checkMappedKeyRule(event, rule) { + if (!rule || typeof rule.attribute !== 'string') { return false; } - return actualValue.indexOf(expectedValue) !== -1; - } - - function doesRuleMatch(event, rule) { var conditions = rule.conditions; if (!Array.isArray(conditions)) { return false; } + if (conditions.length === 0) { return true; } + + var actualValue = getEventAttributeValue(event, rule.attribute); for (var i = 0; i < conditions.length; i++) { - if (!doesConditionMatch(event, conditions[i])) { + if (!checkAttributeCondition(conditions[i], actualValue)) { return false; } } + return true; } - function buildPlacementEventMappingRulesLookup(rules) { - var index = {}; - if (!Array.isArray(rules)) { - return index; - } - for (var i = 0; i < rules.length; i++) { - var rule = rules[i]; - - var jsmapKey = rule.jsmap; - var value = rule.value; + function generateMappedEventAttributeLookup( + placementEventAttributeMapping + ) { + var mappedEventAttributes = {}; + if (!Array.isArray(placementEventAttributeMapping)) { + return mappedEventAttributes; + } + for (var i = 0; i < placementEventAttributeMapping.length; i++) { + var mapping = placementEventAttributeMapping[i]; + if ( + !mapping || + typeof mapping.value !== 'string' || + typeof mapping.map !== 'string' + ) { + continue; + } - if (!index[jsmapKey]) index[jsmapKey] = {}; - if (!index[jsmapKey][value]) index[jsmapKey][value] = []; + if (!mappedEventAttributes[mapping.value]) { + mappedEventAttributes[mapping.value] = []; + } - index[jsmapKey][value].push({ - jsmap: jsmapKey, - value: value, - conditions: rule.conditions, + mappedEventAttributes[mapping.value].push({ + attribute: mapping.map, + conditions: Array.isArray(mapping.conditions) + ? mapping.conditions + : [], }); } - return index; + return mappedEventAttributes; } - function applyEventMapping(event, jsmap) { - var mapped = self.placementEventMappingLookup[jsmap]; - if (!mapped) { + function applyPlacementEventAttributeMapping(event) { + if ( + !self.placementEventAttributeMappingLookup || + isEmpty(self.placementEventAttributeMappingLookup) + ) { return; } - var keys = Array.isArray(mapped) ? mapped : [mapped]; - - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var rulesForKey = - self.placementEventMappingRulesLookup && - self.placementEventMappingRulesLookup[jsmap] && - self.placementEventMappingRulesLookup[jsmap][key] - ? self.placementEventMappingRulesLookup[jsmap][key] - : null; - - // If there are rules for (jsmap,key), only set when ALL rules match (AND). - if (rulesForKey && rulesForKey.length) { - var allMatch = true; - for (var j = 0; j < rulesForKey.length; j++) { - if (!doesRuleMatch(event, rulesForKey[j])) { - allMatch = false; - break; - } - } - if (!allMatch) { - continue; + + var mappedKeys = Object.keys(self.placementEventAttributeMappingLookup); + for (var i = 0; i < mappedKeys.length; i++) { + var mappedKey = mappedKeys[i]; + var rulesForMappedKey = + self.placementEventAttributeMappingLookup[mappedKey]; + if (!rulesForMappedKey || !rulesForMappedKey.length) { + continue; + } + + // Require ALL rules for the same key to match (AND across rules). + var allMatch = true; + for (var j = 0; j < rulesForMappedKey.length; j++) { + if (!checkMappedKeyRule(event, rulesForMappedKey[j])) { + allMatch = false; + break; } } + if (!allMatch) { + continue; + } - window.mParticle.Rokt.setLocalSessionAttribute(key, true); + window.mParticle.Rokt.setLocalSessionAttribute(mappedKey, true); } } @@ -209,11 +223,11 @@ var constructor = function () { placementEventMapping ); - var placementEventMappingRules = parseSettingsString( - settings.placementEventMappingRules + var placementEventAttributeMapping = parseSettingsString( + settings.placementEventAttributeMapping ); - self.placementEventMappingRulesLookup = - buildPlacementEventMappingRulesLookup(placementEventMappingRules); + self.placementEventAttributeMappingLookup = + generateMappedEventAttributeLookup(placementEventAttributeMapping); // Set dynamic OTHER_IDENTITY based on server settings // Convert to lowercase since server sends TitleCase (e.g., 'Other' -> 'other') @@ -238,9 +252,8 @@ var constructor = function () { hashEventMessage: hashEventMessage, parseSettingsString: parseSettingsString, generateMappedEventLookup: generateMappedEventLookup, - buildPlacementEventMappingRulesLookup: - buildPlacementEventMappingRulesLookup, - doesConditionMatch: doesConditionMatch, + generateMappedEventAttributeLookup: + generateMappedEventAttributeLookup, }; attachLauncher(accountId, launcherOptions); return; @@ -298,13 +311,18 @@ var constructor = function () { function returnLocalSessionAttributes() { if ( - isEmpty(self.placementEventMappingLookup) || !window.mParticle.Rokt || typeof window.mParticle.Rokt.getLocalSessionAttributes !== 'function' ) { return {}; } + if ( + isEmpty(self.placementEventMappingLookup) && + isEmpty(self.placementEventAttributeMappingLookup) + ) { + return {}; + } return window.mParticle.Rokt.getLocalSessionAttributes(); } @@ -432,34 +450,26 @@ var constructor = function () { } if ( - isEmpty(self.placementEventMappingLookup) || typeof window.mParticle.Rokt.setLocalSessionAttribute !== 'function' ) { return; } - var hashedEvent = hashEventMessage( - event.EventDataType, - event.EventCategory, - event.EventName - ); + applyPlacementEventAttributeMapping(event); - if (self.placementEventMappingLookup[hashedEvent]) { - applyEventMapping(event, hashedEvent); + if (isEmpty(self.placementEventMappingLookup)) { + return; } - // Allow wildcard event-name mapping (e.g., any ScreenView for a given type/category). - var hashedEventWildcard = hashEventMessage( + var hashedEvent = hashEventMessage( event.EventDataType, event.EventCategory, - '*' + event.EventName ); - if ( - hashedEventWildcard !== hashedEvent && - self.placementEventMappingLookup[hashedEventWildcard] - ) { - applyEventMapping(event, hashedEventWildcard); + if (self.placementEventMappingLookup[hashedEvent]) { + var mappedValue = self.placementEventMappingLookup[hashedEvent]; + window.mParticle.Rokt.setLocalSessionAttribute(mappedValue, true); } } @@ -704,26 +714,8 @@ function generateMappedEventLookup(placementEventMapping) { var mappedEvents = {}; for (var i = 0; i < placementEventMapping.length; i++) { var mapping = placementEventMapping[i]; - - var jsmap = mapping.jsmap; - var value = mapping.value; - if (!mappedEvents[jsmap]) { - mappedEvents[jsmap] = []; - } - // Avoid duplicates - if (mappedEvents[jsmap].indexOf(value) === -1) { - mappedEvents[jsmap].push(value); - } + mappedEvents[mapping.jsmap] = mapping.value; } - - var jsmapKeys = Object.keys(mappedEvents); - for (var j = 0; j < jsmapKeys.length; j++) { - var key = jsmapKeys[j]; - if (mappedEvents[key] && mappedEvents[key].length === 1) { - mappedEvents[key] = mappedEvents[key][0]; - } - } - return mappedEvents; } diff --git a/test/src/tests.js b/test/src/tests.js index 2029634..ff624fd 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -3148,23 +3148,15 @@ describe('Rokt Forwarder', () => { }); }); - it('should set local session attribute only when rule conditions match (URL contains)', async () => { - const placementEventMapping = JSON.stringify([ + it('should set local session attribute only when placementEventAttributeMapping conditions match (URL contains)', async () => { + const placementEventAttributeMapping = JSON.stringify([ { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'saleSeeker', - }, - ]); - - const placementEventMappingRules = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', value: 'saleSeeker', conditions: [ { - attribute: 'URL', operator: 'contains', attributeValue: 'sale', }, @@ -3175,8 +3167,7 @@ describe('Rokt Forwarder', () => { await window.mParticle.forwarder.init( { accountId: '123456', - placementEventMapping, - placementEventMappingRules, + placementEventAttributeMapping, }, reportService.cb, true, @@ -3186,7 +3177,6 @@ describe('Rokt Forwarder', () => { await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); - // Non-matching URL => should NOT set saleSeeker window.mParticle._Store.localSessionAttributes = {}; window.mParticle.forwarder.process({ EventName: 'Browse', @@ -3198,7 +3188,6 @@ describe('Rokt Forwarder', () => { }); window.mParticle._Store.localSessionAttributes.should.deepEqual({}); - // Matching URL => should set saleSeeker window.mParticle._Store.localSessionAttributes = {}; window.mParticle.forwarder.process({ EventName: 'Browse', @@ -3213,26 +3202,21 @@ describe('Rokt Forwarder', () => { }); }); - it('should apply both exact and wildcard mappings when both are configured', async () => { - const placementEventMapping = JSON.stringify([ + it('should support exists operator for placementEventAttributeMapping conditions', async () => { + const placementEventAttributeMapping = JSON.stringify([ { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'exactFlag', - }, - { - jsmap: 'hashed-<30*>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'wildcardFlag', + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', + value: 'hasUrl', + conditions: [{ operator: 'exists' }], }, ]); await window.mParticle.forwarder.init( { accountId: '123456', - placementEventMapping, + placementEventAttributeMapping, }, reportService.cb, true, @@ -3247,33 +3231,27 @@ describe('Rokt Forwarder', () => { EventName: 'Browse', EventCategory: EventType.Unknown, EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/anything', + }, }); window.mParticle._Store.localSessionAttributes.should.deepEqual({ - exactFlag: true, - wildcardFlag: true, + hasUrl: true, }); }); - it('should evaluate contains and equals case-sensitively', async () => { - const placementEventMapping = JSON.stringify([ + it('should evaluate equals type-sensitively for placementEventAttributeMapping conditions', async () => { + const placementEventAttributeMapping = JSON.stringify([ { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'caseSensitive', - }, - ]); - - const placementEventMappingRules = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - value: 'caseSensitive', + jsmap: null, + map: 'number_of_products', + maptype: 'EventAttributeClass.Name', + value: 'multipleproducts', conditions: [ { - attribute: 'URL', - operator: 'contains', - attributeValue: 'Sale', + operator: 'equals', + attributeValue: 2, }, ], }, @@ -3282,8 +3260,7 @@ describe('Rokt Forwarder', () => { await window.mParticle.forwarder.init( { accountId: '123456', - placementEventMapping, - placementEventMappingRules, + placementEventAttributeMapping, }, reportService.cb, true, @@ -3293,96 +3270,42 @@ describe('Rokt Forwarder', () => { await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); - // URL contains "sale" but NOT "Sale" => should NOT set window.mParticle._Store.localSessionAttributes = {}; window.mParticle.forwarder.process({ EventName: 'Browse', EventCategory: EventType.Unknown, EventDataType: MessageType.PageView, EventAttributes: { - URL: 'https://example.com/sale/items', + number_of_products: 2, }, }); - window.mParticle._Store.localSessionAttributes.should.deepEqual({}); - }); - - it('should evaluate EventAttributes keys case-sensitively', async () => { - const placementEventMapping = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'attrCaseSensitive', - }, - ]); - - const placementEventMappingRules = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - value: 'attrCaseSensitive', - conditions: [{ attribute: 'url', operator: 'exists' }], - }, - ]); - - await window.mParticle.forwarder.init( - { - accountId: '123456', - placementEventMapping, - placementEventMappingRules, - }, - reportService.cb, - true, - null, - {} - ); - - await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + window.mParticle._Store.localSessionAttributes.should.deepEqual({ + multipleproducts: true, + }); - // Event has URL, rule checks "url" => should NOT match window.mParticle._Store.localSessionAttributes = {}; window.mParticle.forwarder.process({ EventName: 'Browse', EventCategory: EventType.Unknown, EventDataType: MessageType.PageView, EventAttributes: { - URL: 'https://example.com/anything', + number_of_products: '2', }, }); - window.mParticle._Store.localSessionAttributes.should.deepEqual({}); }); - it('should require ALL rules for the same (jsmap,key) to match (AND across rules)', async () => { - const placementEventMapping = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'saleSeeker', - }, - ]); - - // Two separate rules for the same (jsmap,value). Both must match. - const placementEventMappingRules = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - value: 'saleSeeker', - conditions: [ - { - attribute: 'URL', - operator: 'contains', - attributeValue: 'sale', - }, - ], - }, + it('should treat contains as string-only (non-strings do not match) for placementEventAttributeMapping', async () => { + const placementEventAttributeMapping = JSON.stringify([ { - jsmap: 'hashed-<30Browse>-value', - value: 'saleSeeker', + jsmap: null, + map: 'number_of_products', + maptype: 'EventAttributeClass.Name', + value: 'containsNumber', conditions: [ { - attribute: 'URL', operator: 'contains', - attributeValue: 'items', + attributeValue: '2', }, ], }, @@ -3391,8 +3314,7 @@ describe('Rokt Forwarder', () => { await window.mParticle.forwarder.init( { accountId: '123456', - placementEventMapping, - placementEventMappingRules, + placementEventAttributeMapping, }, reportService.cb, true, @@ -3402,150 +3324,41 @@ describe('Rokt Forwarder', () => { await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); - // Matches only 1/2 rules => should NOT set window.mParticle._Store.localSessionAttributes = {}; window.mParticle.forwarder.process({ EventName: 'Browse', EventCategory: EventType.Unknown, EventDataType: MessageType.PageView, EventAttributes: { - URL: 'https://example.com/sale', + number_of_products: 2, }, }); window.mParticle._Store.localSessionAttributes.should.deepEqual({}); - - // Matches both rules => should set - window.mParticle._Store.localSessionAttributes = {}; - window.mParticle.forwarder.process({ - EventName: 'Browse', - EventCategory: EventType.Unknown, - EventDataType: MessageType.PageView, - EventAttributes: { - URL: 'https://example.com/sale/items', - }, - }); - window.mParticle._Store.localSessionAttributes.should.deepEqual({ - saleSeeker: true, - }); }); - it('should support exists operator for rule conditions', async () => { - const placementEventMapping = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'hasUrl', - }, - ]); - - const placementEventMappingRules = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - value: 'hasUrl', - conditions: [{ attribute: 'URL', operator: 'exists' }], - }, - ]); - - await window.mParticle.forwarder.init( - { - accountId: '123456', - placementEventMapping, - placementEventMappingRules, - }, - reportService.cb, - true, - null, - {} - ); - - await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); - - window.mParticle._Store.localSessionAttributes = {}; - window.mParticle.forwarder.process({ - EventName: 'Browse', - EventCategory: EventType.Unknown, - EventDataType: MessageType.PageView, - EventAttributes: { - URL: 'https://example.com/anything', - }, - }); - - window.mParticle._Store.localSessionAttributes.should.deepEqual({ - hasUrl: true, - }); - }); - - it('should treat invalid rule operators as non-matching', async () => { - const placementEventMapping = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'badOperator', - }, - ]); - - const placementEventMappingRules = JSON.stringify([ + it('should require ALL rules for the same mapped key to match (AND across rules)', async () => { + const placementEventAttributeMapping = JSON.stringify([ { - jsmap: 'hashed-<30Browse>-value', - value: 'badOperator', + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', + value: 'saleSeeker', conditions: [ { - attribute: 'URL', - operator: null, + operator: 'contains', attributeValue: 'sale', }, ], }, - ]); - - await window.mParticle.forwarder.init( - { - accountId: '123456', - placementEventMapping, - placementEventMappingRules, - }, - reportService.cb, - true, - null, - {} - ); - - await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); - - window.mParticle._Store.localSessionAttributes = {}; - window.mParticle.forwarder.process({ - EventName: 'Browse', - EventCategory: EventType.Unknown, - EventDataType: MessageType.PageView, - EventAttributes: { - URL: 'https://example.com/sale/items', - }, - }); - - window.mParticle._Store.localSessionAttributes.should.deepEqual({}); - }); - - it('should treat unknown string operators as non-matching', async () => { - const placementEventMapping = JSON.stringify([ { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'unknownOperator', - }, - ]); - - const placementEventMappingRules = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - value: 'unknownOperator', + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', + value: 'saleSeeker', conditions: [ { - attribute: 'URL', - operator: 'starts_with', - attributeValue: 'https://example.com/sale', + operator: 'contains', + attributeValue: 'items', }, ], }, @@ -3554,8 +3367,7 @@ describe('Rokt Forwarder', () => { await window.mParticle.forwarder.init( { accountId: '123456', - placementEventMapping, - placementEventMappingRules, + placementEventAttributeMapping, }, reportService.cb, true, @@ -3565,135 +3377,32 @@ describe('Rokt Forwarder', () => { await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + // Matches only 1/2 rules => should NOT set window.mParticle._Store.localSessionAttributes = {}; window.mParticle.forwarder.process({ EventName: 'Browse', EventCategory: EventType.Unknown, EventDataType: MessageType.PageView, EventAttributes: { - URL: 'https://example.com/sale/items', + URL: 'https://example.com/sale', }, }); - window.mParticle._Store.localSessionAttributes.should.deepEqual({}); - }); - - it('should evaluate equals type-sensitively (numeric)', async () => { - const placementEventMapping = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'multipleproducts', - }, - ]); - - const placementEventMappingRules = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - value: 'multipleproducts', - conditions: [ - { - attribute: 'number_of_products', - operator: 'equals', - attributeValue: 2, - }, - ], - }, - ]); - - await window.mParticle.forwarder.init( - { - accountId: '123456', - placementEventMapping, - placementEventMappingRules, - }, - reportService.cb, - true, - null, - {} - ); - - await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); - // number => should match + // Matches both rules => should set window.mParticle._Store.localSessionAttributes = {}; window.mParticle.forwarder.process({ EventName: 'Browse', EventCategory: EventType.Unknown, EventDataType: MessageType.PageView, EventAttributes: { - number_of_products: 2, + URL: 'https://example.com/sale/items', }, }); window.mParticle._Store.localSessionAttributes.should.deepEqual({ - multipleproducts: true, - }); - - // string => should NOT match numeric attributeValue - window.mParticle._Store.localSessionAttributes = {}; - window.mParticle.forwarder.process({ - EventName: 'Browse', - EventCategory: EventType.Unknown, - EventDataType: MessageType.PageView, - EventAttributes: { - number_of_products: '2', - }, - }); - window.mParticle._Store.localSessionAttributes.should.deepEqual({}); - }); - - it('should treat contains as string-only (non-strings do not match)', async () => { - const placementEventMapping = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - map: 'any', - maptype: 'EventClass.Id', - value: 'containsNumber', - }, - ]); - - const placementEventMappingRules = JSON.stringify([ - { - jsmap: 'hashed-<30Browse>-value', - value: 'containsNumber', - conditions: [ - { - attribute: 'number_of_products', - operator: 'contains', - attributeValue: '2', - }, - ], - }, - ]); - - await window.mParticle.forwarder.init( - { - accountId: '123456', - placementEventMapping, - placementEventMappingRules, - }, - reportService.cb, - true, - null, - {} - ); - - await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); - - window.mParticle._Store.localSessionAttributes = {}; - window.mParticle.forwarder.process({ - EventName: 'Browse', - EventCategory: EventType.Unknown, - EventDataType: MessageType.PageView, - EventAttributes: { - number_of_products: 2, - }, + saleSeeker: true, }); - - window.mParticle._Store.localSessionAttributes.should.deepEqual({}); }); - it('should add the event to the event queue if the kit is not initialized', async () => { await window.mParticle.forwarder.init( { From 34a6d44724d1218fcfa322112e05593a6795d1dc Mon Sep 17 00:00:00 2001 From: Preston Starkey Date: Wed, 4 Feb 2026 11:41:10 -0500 Subject: [PATCH 3/4] LE - update unit tests --- test/src/tests.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/test/src/tests.js b/test/src/tests.js index ff624fd..edbdc0f 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -3348,13 +3348,72 @@ describe('Rokt Forwarder', () => { operator: 'contains', attributeValue: 'sale', }, + { + operator: 'contains', + attributeValue: 'items', + }, ], }, + ]); + + await window.mParticle.forwarder.init( + { + accountId: '123456', + placementEventAttributeMapping, + }, + reportService.cb, + true, + null, + {} + ); + + await waitForCondition(() => window.mParticle.Rokt.attachKitCalled); + + // Matches only 1/2 rules => should NOT set + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/sale', + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + + // Matches both rules => should set + window.mParticle._Store.localSessionAttributes = {}; + window.mParticle.forwarder.process({ + EventName: 'Browse', + EventCategory: EventType.Unknown, + EventDataType: MessageType.PageView, + EventAttributes: { + URL: 'https://example.com/sale/items', + }, + }); + window.mParticle._Store.localSessionAttributes.should.deepEqual({ + saleSeeker: true, + }); + }); + it('should map multiple attributes for the same mapped key (AND across rules)', async () => { + const placementEventAttributeMapping = JSON.stringify([ { jsmap: null, map: 'URL', maptype: 'EventAttributeClass.Name', value: 'saleSeeker', + conditions: [ + { + operator: 'contains', + attributeValue: 'sale', + }, + ], + }, + { + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', + value: 'saleSeeker1', conditions: [ { operator: 'contains', @@ -3387,7 +3446,9 @@ describe('Rokt Forwarder', () => { URL: 'https://example.com/sale', }, }); - window.mParticle._Store.localSessionAttributes.should.deepEqual({}); + window.mParticle._Store.localSessionAttributes.should.deepEqual({ + saleSeeker: true, + }); // Matches both rules => should set window.mParticle._Store.localSessionAttributes = {}; @@ -3401,6 +3462,7 @@ describe('Rokt Forwarder', () => { }); window.mParticle._Store.localSessionAttributes.should.deepEqual({ saleSeeker: true, + saleSeeker1: true, }); }); it('should add the event to the event queue if the kit is not initialized', async () => { From 66792add160de1aae552f6ebba4bc8285636eadc Mon Sep 17 00:00:00 2001 From: Preston Starkey Date: Wed, 4 Feb 2026 17:53:33 -0500 Subject: [PATCH 4/4] LE - update names --- src/Rokt-Kit.js | 61 ++++++++++++-------- test/src/tests.js | 142 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 25 deletions(-) diff --git a/src/Rokt-Kit.js b/src/Rokt-Kit.js index ba6c764..dd9a66e 100644 --- a/src/Rokt-Kit.js +++ b/src/Rokt-Kit.js @@ -39,20 +39,20 @@ var constructor = function () { self.placementEventAttributeMappingLookup = {}; self.eventQueue = []; - function getEventAttributeValue(event, attributeKey) { + function getEventAttributeValue(event, eventAttributeKey) { var attributes = event && event.EventAttributes; if (!attributes) { return null; } - if (typeof attributes[attributeKey] === 'undefined') { + if (typeof attributes[eventAttributeKey] === 'undefined') { return null; } - return attributes[attributeKey]; + return attributes[eventAttributeKey]; } - function checkAttributeCondition(condition, actualValue) { + function doesEventAttributeConditionMatch(condition, actualValue) { if (!condition || typeof condition.operator !== 'string') { return false; } @@ -81,8 +81,8 @@ var constructor = function () { return false; } - function checkMappedKeyRule(event, rule) { - if (!rule || typeof rule.attribute !== 'string') { + function doesEventMatchRule(event, rule) { + if (!rule || typeof rule.eventAttributeKey !== 'string') { return false; } @@ -95,9 +95,9 @@ var constructor = function () { return true; } - var actualValue = getEventAttributeValue(event, rule.attribute); + var actualValue = getEventAttributeValue(event, rule.eventAttributeKey); for (var i = 0; i < conditions.length; i++) { - if (!checkAttributeCondition(conditions[i], actualValue)) { + if (!doesEventAttributeConditionMatch(conditions[i], actualValue)) { return false; } } @@ -108,9 +108,9 @@ var constructor = function () { function generateMappedEventAttributeLookup( placementEventAttributeMapping ) { - var mappedEventAttributes = {}; + var mappedAttributeKeys = {}; if (!Array.isArray(placementEventAttributeMapping)) { - return mappedEventAttributes; + return mappedAttributeKeys; } for (var i = 0; i < placementEventAttributeMapping.length; i++) { var mapping = placementEventAttributeMapping[i]; @@ -122,18 +122,21 @@ var constructor = function () { continue; } - if (!mappedEventAttributes[mapping.value]) { - mappedEventAttributes[mapping.value] = []; + var mappedAttributeKey = mapping.value; + var eventAttributeKey = mapping.map; + + if (!mappedAttributeKeys[mappedAttributeKey]) { + mappedAttributeKeys[mappedAttributeKey] = []; } - mappedEventAttributes[mapping.value].push({ - attribute: mapping.map, + mappedAttributeKeys[mappedAttributeKey].push({ + eventAttributeKey: eventAttributeKey, conditions: Array.isArray(mapping.conditions) ? mapping.conditions : [], }); } - return mappedEventAttributes; + return mappedAttributeKeys; } function applyPlacementEventAttributeMapping(event) { @@ -144,19 +147,24 @@ var constructor = function () { return; } - var mappedKeys = Object.keys(self.placementEventAttributeMappingLookup); - for (var i = 0; i < mappedKeys.length; i++) { - var mappedKey = mappedKeys[i]; - var rulesForMappedKey = - self.placementEventAttributeMappingLookup[mappedKey]; - if (!rulesForMappedKey || !rulesForMappedKey.length) { + var mappedAttributeKeys = Object.keys( + self.placementEventAttributeMappingLookup + ); + for (var i = 0; i < mappedAttributeKeys.length; i++) { + var mappedAttributeKey = mappedAttributeKeys[i]; + var rulesForMappedAttributeKey = + self.placementEventAttributeMappingLookup[mappedAttributeKey]; + if ( + !rulesForMappedAttributeKey || + !rulesForMappedAttributeKey.length + ) { continue; } - // Require ALL rules for the same key to match (AND across rules). + // Require ALL rules for the same key to match (AND). var allMatch = true; - for (var j = 0; j < rulesForMappedKey.length; j++) { - if (!checkMappedKeyRule(event, rulesForMappedKey[j])) { + for (var j = 0; j < rulesForMappedAttributeKey.length; j++) { + if (!doesEventMatchRule(event, rulesForMappedAttributeKey[j])) { allMatch = false; break; } @@ -165,7 +173,10 @@ var constructor = function () { continue; } - window.mParticle.Rokt.setLocalSessionAttribute(mappedKey, true); + window.mParticle.Rokt.setLocalSessionAttribute( + mappedAttributeKey, + true + ); } } diff --git a/test/src/tests.js b/test/src/tests.js index edbdc0f..bc38396 100644 --- a/test/src/tests.js +++ b/test/src/tests.js @@ -3055,6 +3055,148 @@ describe('Rokt Forwarder', () => { }); }); + describe('#generateMappedEventAttributeLookup', () => { + beforeEach(async () => { + window.Rokt = new MockRoktForwarder(); + window.mParticle.Rokt = window.Rokt; + + await window.mParticle.forwarder.init( + { + accountId: '123456', + }, + reportService.cb, + true + ); + }); + + it('should generate a lookup table from placementEventAttributeMapping', () => { + const placementEventAttributeMapping = [ + { + jsmap: null, + map: 'number_of_products', + maptype: 'EventAttributeClass.Name', + value: 'tof_products_2', + conditions: [ + { + operator: 'equals', + attributeValue: 2, + }, + ], + }, + { + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', + value: 'saleSeeker', + conditions: [ + { + operator: 'contains', + attributeValue: 'sale', + }, + ], + }, + ]; + + window.mParticle.forwarder.testHelpers + .generateMappedEventAttributeLookup( + placementEventAttributeMapping + ) + .should.deepEqual({ + tof_products_2: [ + { + eventAttributeKey: 'number_of_products', + conditions: [ + { + operator: 'equals', + attributeValue: 2, + }, + ], + }, + ], + saleSeeker: [ + { + eventAttributeKey: 'URL', + conditions: [ + { + operator: 'contains', + attributeValue: 'sale', + }, + ], + }, + ], + }); + }); + + it('should default conditions to an empty array when missing', () => { + const placementEventAttributeMapping = [ + { + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', + value: 'hasUrl', + }, + ]; + + window.mParticle.forwarder.testHelpers + .generateMappedEventAttributeLookup( + placementEventAttributeMapping + ) + .should.deepEqual({ + hasUrl: [ + { + eventAttributeKey: 'URL', + conditions: [], + }, + ], + }); + }); + + it('should return an empty object when placementEventAttributeMapping is null', () => { + window.mParticle.forwarder.testHelpers + .generateMappedEventAttributeLookup(null) + .should.deepEqual({}); + }); + + it('should ignore invalid mappings (non-string map/value)', () => { + const placementEventAttributeMapping = [ + { + jsmap: null, + map: null, + maptype: 'EventAttributeClass.Name', + value: 'bad', + conditions: [], + }, + { + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', + value: null, + conditions: [], + }, + { + jsmap: null, + map: 'URL', + maptype: 'EventAttributeClass.Name', + value: 'good', + conditions: [], + }, + ]; + + window.mParticle.forwarder.testHelpers + .generateMappedEventAttributeLookup( + placementEventAttributeMapping + ) + .should.deepEqual({ + good: [ + { + eventAttributeKey: 'URL', + conditions: [], + }, + ], + }); + }); + }); + describe('#processEvent', () => { beforeEach(() => { window.Rokt = new MockRoktForwarder();