From 17f960927f4bc40fe4ceb131e37e7349e8c68eb1 Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Mon, 21 Aug 2023 09:21:12 -0400 Subject: [PATCH 1/9] chore(build): Generate latest bundle [skip ci] --- dist/FacebookEventForwarder.common.js | 28 +- dist/FacebookEventForwarder.esm.js | 392 ++++++++++++++++++++++++++ dist/FacebookEventForwarder.iife.js | 14 +- 3 files changed, 418 insertions(+), 16 deletions(-) create mode 100644 dist/FacebookEventForwarder.esm.js diff --git a/dist/FacebookEventForwarder.common.js b/dist/FacebookEventForwarder.common.js index 9f7d526..8a29b3d 100644 --- a/dist/FacebookEventForwarder.common.js +++ b/dist/FacebookEventForwarder.common.js @@ -11,6 +11,16 @@ function isObject(val) { return val != null && typeof val === 'object' && Array.isArray(val) === false; } +var isobject = /*#__PURE__*/Object.freeze({ + 'default': isObject +}); + +function getCjsExportFromNamespace (n) { + return n && n['default'] || n; +} + +var isobject$1 = getCjsExportFromNamespace(isobject); + /* eslint-disable no-undef */ // Copyright 2015 mParticle, Inc. // @@ -63,13 +73,13 @@ function isObject(val) { }, SupportedCommerceTypes = [], // Standard FB Event Names from https://developers.facebook.com/docs/facebook-pixel/reference#standard-events - ADD_TO_CART_EVENT_NAME = 'AddToCart'; - ADD_TO_WISHLIST_EVENT_NAME = 'AddToWishlist'; - CHECKOUT_EVENT_NAME = 'InitiateCheckout'; - PAGE_VIEW_EVENT_NAME = 'PageView'; - PURCHASE_EVENT_NAME = 'Purchase'; - REMOVE_FROM_CART_EVENT_NAME = 'RemoveFromCart'; - VIEW_CONTENT_EVENT_NAME = 'ViewContent'; + ADD_TO_CART_EVENT_NAME = 'AddToCart', + ADD_TO_WISHLIST_EVENT_NAME = 'AddToWishlist', + CHECKOUT_EVENT_NAME = 'InitiateCheckout', + PAGE_VIEW_EVENT_NAME = 'PageView', + PURCHASE_EVENT_NAME = 'Purchase', + REMOVE_FROM_CART_EVENT_NAME = 'RemoveFromCart', + VIEW_CONTENT_EVENT_NAME = 'ViewContent', constructor = function () { var self = this, isInitialized = false, @@ -357,12 +367,12 @@ function isObject(val) { return; } - if (!isObject(config)) { + if (!isobject$1(config)) { console.log('\'config\' must be an object. You passed in a ' + typeof config); return; } - if (isObject(config.kits)) { + if (isobject$1(config.kits)) { config.kits[name] = { constructor: constructor }; diff --git a/dist/FacebookEventForwarder.esm.js b/dist/FacebookEventForwarder.esm.js new file mode 100644 index 0000000..4a319ff --- /dev/null +++ b/dist/FacebookEventForwarder.esm.js @@ -0,0 +1,392 @@ +/*! + * isobject + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ + +function isObject(val) { + return val != null && typeof val === 'object' && Array.isArray(val) === false; +} + +/* eslint-disable no-undef */ +// Copyright 2015 mParticle, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + + var name = 'Facebook', + moduleId = 45, + MessageType = { + SessionStart: 1, + SessionEnd: 2, + PageView: 3, + PageEvent: 4, + CrashReport: 5, + OptOut: 6, + Commerce: 16, + }, + IdentityType = { + Other: 0, + CustomerId: 1, + Facebook: 2, + Twitter: 3, + Google: 4, + Microsoft: 5, + Yahoo: 6, + Email: 7, + FacebookCustomAudienceId: 9, + Other2: 10, + Other3: 11, + Other4: 12, + Other5: 13, + Other6: 14, + Other7: 15, + Other8: 16, + Other9: 17, + Other10: 18, + MobileNumber: 19, + PhoneNumber2: 20, + PhoneNumber3: 21, + }, + SupportedCommerceTypes = [], + // Standard FB Event Names from https://developers.facebook.com/docs/facebook-pixel/reference#standard-events + ADD_TO_CART_EVENT_NAME = 'AddToCart', + ADD_TO_WISHLIST_EVENT_NAME = 'AddToWishlist', + CHECKOUT_EVENT_NAME = 'InitiateCheckout', + PAGE_VIEW_EVENT_NAME = 'PageView', + PURCHASE_EVENT_NAME = 'Purchase', + REMOVE_FROM_CART_EVENT_NAME = 'RemoveFromCart', + VIEW_CONTENT_EVENT_NAME = 'ViewContent', + constructor = function () { + var self = this, + isInitialized = false, + reportingService = null; + + self.name = name; + + function initForwarder(settings, service, testMode, trackerId, userAttributes, userIdentities) { + reportingService = service; + + SupportedCommerceTypes = [ + mParticle.ProductActionType.Checkout, + mParticle.ProductActionType.Purchase, + mParticle.ProductActionType.AddToCart, + mParticle.ProductActionType.RemoveFromCart, + mParticle.ProductActionType.AddToWishlist, + mParticle.ProductActionType.ViewDetail + ]; + + try { + if (!testMode) { + !function (f, b, e, v, n, t, s) { + if (f.fbq) return; n = f.fbq = function () { n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments); }; if (!f._fbq) f._fbq = n; + n.push = n; n.loaded = !0; n.version = '2.0'; n.queue = []; t = b.createElement(e); t.async = !0; t.src = v; s = b.getElementsByTagName(e)[0]; + s.parentNode.insertBefore(t, s); + } (window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js'); + + var visitorData = {}; + + if(settings.externalUserIdentityType && userIdentities && userIdentities.length > 0) { + var selectedType = IdentityType[settings.externalUserIdentityType]; + var selectedIdentity = userIdentities.filter(function (identityElement) { + if (identityElement.Type === selectedType) { + return identityElement.Identity; + } + }); + + if (selectedIdentity.length > 0) { + visitorData['external_id'] = selectedIdentity[0].Identity; + } + } + } + + if (settings.disablePushState === 'True') { + // Facebook will automatically track page views whenever a new state is pushed to the HTML 5 History State API + // this option can be disabled to prevent duplicate page views + // https://developers.facebook.com/docs/facebook-pixel/implementation/tag_spa/#tagging-single-page-applications + fbq.disablePushState = true; + } + fbq('init', settings.pixelId, visitorData); + + isInitialized = true; + + return 'Successfully initialized: ' + name; + + } + catch (e) { + return 'Can\'t initialize forwarder: ' + name + ':' + e; + } + } + + function processEvent(event) { + var reportEvent = false; + + if (!isInitialized) { + return 'Can\'t send forwarder ' + name + ', not initialized'; + } + + try { + if (event.EventDataType == MessageType.PageView) { + reportEvent = true; + logPageView(event); + } + else if (event.EventDataType == MessageType.PageEvent) { + reportEvent = true; + logPageEvent(event); + } + else if (event.EventDataType == MessageType.Commerce) { + reportEvent = logCommerceEvent(event); + } + + if (reportEvent && reportingService) { + reportingService(self, event); + } + + return 'Successfully sent to forwarder ' + name; + } + catch (error) { + return 'Can\'t send to forwarder: ' + name + ' ' + error; + } + } + + function logCommerceEvent(event) { + if (event.ProductAction && + event.ProductAction.ProductList && + event.ProductAction.ProductActionType && + SupportedCommerceTypes.indexOf(event.ProductAction.ProductActionType) > -1) { + + var eventName, + totalValue, + params = cloneEventAttributes(event), + eventID = createEventId(event); + params['currency'] = event.CurrencyCode || 'USD'; + + if (event.EventName) { + params['content_name'] = event.EventName; + } + + var productSkus = event.ProductAction.ProductList.reduce(function (arr, curr) { + if (curr.Sku) { + arr.push(curr.Sku); + } + return arr; + }, []); + + if (productSkus && productSkus.length > 0) { + params['content_ids'] = productSkus; + } + + if (event.ProductAction.ProductActionType == mParticle.ProductActionType.AddToWishlist || + event.ProductAction.ProductActionType == mParticle.ProductActionType.Checkout) { + var eventCategory = getEventCategoryString(event); + if (eventCategory) { + params['content_category'] = eventCategory; + } + if (event.ProductAction.ProductActionType == mParticle.ProductActionType.Checkout && event.ProductAction.CheckoutStep) { + params['checkout_step'] = event.ProductAction.CheckoutStep; + } + } + + if (event.ProductAction.ProductActionType == mParticle.ProductActionType.AddToCart || + event.ProductAction.ProductActionType == mParticle.ProductActionType.AddToWishlist || + event.ProductAction.ProductActionType == mParticle.ProductActionType.ViewDetail) { + + totalValue = event.ProductAction.ProductList.reduce(function(sum, product){ + if (isNumeric(product.Price) && isNumeric(product.Quantity)) { + sum += product.Price * product.Quantity; + } + return sum; + }, 0); + + params['value'] = totalValue; + + if (event.ProductAction.ProductActionType == mParticle.ProductActionType.AddToWishlist){ + eventName = ADD_TO_WISHLIST_EVENT_NAME; + } + else if (event.ProductAction.ProductActionType == mParticle.ProductActionType.AddToCart){ + eventName = ADD_TO_CART_EVENT_NAME; + } + else{ + eventName = VIEW_CONTENT_EVENT_NAME; + } + + } + else if (event.ProductAction.ProductActionType == mParticle.ProductActionType.Checkout || + event.ProductAction.ProductActionType == mParticle.ProductActionType.Purchase) { + + eventName = event.ProductAction.ProductActionType == mParticle.ProductActionType.Checkout ? CHECKOUT_EVENT_NAME : PURCHASE_EVENT_NAME; + + if (event.ProductAction.TotalAmount) { + params['value'] = event.ProductAction.TotalAmount; + } + + var num_items = event.ProductAction.ProductList.reduce(function(sum, product){ + if (isNumeric(product.Quantity)) { + sum += product.Quantity; + } + return sum; + }, 0); + params['num_items'] = num_items; + } + else if (event.ProductAction.ProductActionType == mParticle.ProductActionType.RemoveFromCart) { + eventName = REMOVE_FROM_CART_EVENT_NAME; + + // remove from cart can be performed in 1 of 2 ways: + // 1. mParticle.eCommerce.logProductEvent(), which contains event.ProductAction.TotalAmount + // 2. mParticle.eCommerce.Cart.remove(), which does not contain event.ProductAction.TotalAmount + // when there is no TotalAmount, a manual calculation must be done + if (event.ProductAction.TotalAmount) { + totalValue = event.ProductAction.TotalAmount; + } else { + totalValue = event.ProductAction.ProductList.reduce(function(sum, product) { + if (isNumeric(product.TotalAmount)) { + sum += product.TotalAmount; + } + return sum; + }, 0); + } + + params['value'] = totalValue; + + fbq('trackCustom', eventName || 'customEvent', params, eventID); + return true; + } + + if (eventName) { + fbq('track', eventName, params, eventID); + } + else { + return false; + } + + return true; + } + + return false; + } + + function logPageView(event) { + logPageEvent(event, PAGE_VIEW_EVENT_NAME); + } + + function logPageEvent(event, eventName) { + var params = cloneEventAttributes(event); + var eventID = createEventId(event); + + eventName = eventName || event.EventName; + if (event.EventName) { + params['content_name'] = event.EventName; + } + + fbq('trackCustom', eventName || 'customEvent', params, eventID); + } + + function cloneEventAttributes(event) { + var attr = {}; + if (event && event.EventAttributes) { + try { + attr = JSON.parse(JSON.stringify(event.EventAttributes)); + } + catch (e) { + // + } + } + return attr; + } + + function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + + function getEventCategoryString(event) { + + var enumTypeValues; + var enumValue; + if (event.EventDataType == MessageType.Commerce) { + enumTypeValues = event.EventCategory ? mParticle.CommerceEventType : mParticle.ProductActionType; + enumValue = event.EventCategory || event.ProductAction.ProductActionType; + } + else { + enumTypeValues = mParticle.EventType; + enumValue = event.EventCategory; + } + + if (enumTypeValues && enumValue) { + + for (var category in enumTypeValues) { + if (enumValue == enumTypeValues[category]) { + return category; + } + } + } + + return null; + } + + // https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events#event-deduplication-options + function createEventId(event) { + return { + eventID: event.SourceMessageId || null + } + } + + this.init = initForwarder; + this.process = processEvent; + }; + + function getId() { + return moduleId; + } + + function register(config) { + if (!config) { + console.log('You must pass a config object to register the kit ' + name); + return; + } + + if (!isObject(config)) { + console.log('\'config\' must be an object. You passed in a ' + typeof config); + return; + } + + if (isObject(config.kits)) { + config.kits[name] = { + constructor: constructor + }; + } else { + config.kits = {}; + config.kits[name] = { + constructor: constructor + }; + } + console.log('Successfully registered ' + name + ' to your mParticle configuration'); + } + + if (typeof window !== 'undefined') { + if (window && window.mParticle && window.mParticle.addForwarder) { + window.mParticle.addForwarder({ + name: name, + constructor: constructor, + getId: getId + }); + } + } + + var FacebookEventForwarder = { + register: register + }; +var FacebookEventForwarder_1 = FacebookEventForwarder.register; + +export default FacebookEventForwarder; +export { FacebookEventForwarder_1 as register }; diff --git a/dist/FacebookEventForwarder.iife.js b/dist/FacebookEventForwarder.iife.js index c1ccd6c..47e0ecb 100644 --- a/dist/FacebookEventForwarder.iife.js +++ b/dist/FacebookEventForwarder.iife.js @@ -62,13 +62,13 @@ var mpFacebookKit = (function (exports) { }, SupportedCommerceTypes = [], // Standard FB Event Names from https://developers.facebook.com/docs/facebook-pixel/reference#standard-events - ADD_TO_CART_EVENT_NAME = 'AddToCart'; - ADD_TO_WISHLIST_EVENT_NAME = 'AddToWishlist'; - CHECKOUT_EVENT_NAME = 'InitiateCheckout'; - PAGE_VIEW_EVENT_NAME = 'PageView'; - PURCHASE_EVENT_NAME = 'Purchase'; - REMOVE_FROM_CART_EVENT_NAME = 'RemoveFromCart'; - VIEW_CONTENT_EVENT_NAME = 'ViewContent'; + ADD_TO_CART_EVENT_NAME = 'AddToCart', + ADD_TO_WISHLIST_EVENT_NAME = 'AddToWishlist', + CHECKOUT_EVENT_NAME = 'InitiateCheckout', + PAGE_VIEW_EVENT_NAME = 'PageView', + PURCHASE_EVENT_NAME = 'Purchase', + REMOVE_FROM_CART_EVENT_NAME = 'RemoveFromCart', + VIEW_CONTENT_EVENT_NAME = 'ViewContent', constructor = function () { var self = this, isInitialized = false, From 023a1bbada18b1cb043179516e5c56d839d8cb4c Mon Sep 17 00:00:00 2001 From: Robert Ing Date: Mon, 21 Aug 2023 09:21:38 -0400 Subject: [PATCH 2/9] chore(release): Update version to 2.1.1 --- CHANGELOG.md | 5 +++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6ae5bb..f087b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ -- +#### 2.1.1 - 2023-08-21 + +- fix: Correct variable declarations, add ESM module (#46) + + #### 2.1.0 - 2021-07-26 - feat: Support page view de-duplication diff --git a/package-lock.json b/package-lock.json index 306cc15..a341550 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@mparticle/web-facebook-kit", - "version": "2.1.0", + "version": "2.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0d5804c..5424525 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mparticle/web-facebook-kit", - "version": "2.1.0", + "version": "2.1.1", "author": "mParticle Developers (https://www.mparticle.com)", "description": "mParticle integration sdk for facebook", "main": "dist/FacebookEventForwarder.common.js", From f655ed14460339e08d3a34da56995726b9d694cf Mon Sep 17 00:00:00 2001 From: Jaissica Date: Wed, 5 Nov 2025 17:17:14 -0500 Subject: [PATCH 3/9] feat: add custom product attributes --- src/FacebookEventForwarder.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/FacebookEventForwarder.js b/src/FacebookEventForwarder.js index 5c1053e..b0bcd6d 100755 --- a/src/FacebookEventForwarder.js +++ b/src/FacebookEventForwarder.js @@ -227,6 +227,17 @@ return sum; }, 0); params['num_items'] = num_items; + + if (event.ProductAction.ProductActionType == mParticle.ProductActionType.Purchase) { + var contents = buildProductContents(event.ProductAction.ProductList); + if (contents && contents.length) { + params['contents'] = contents; + params['content_type'] = 'product'; + } + if (event.ProductAction.TransactionId) { + params['order_id'] = event.ProductAction.TransactionId; + } + } } else if (event.ProductAction.ProductActionType == mParticle.ProductActionType.RemoveFromCart) { eventName = REMOVE_FROM_CART_EVENT_NAME; @@ -298,6 +309,29 @@ return !isNaN(parseFloat(n)) && isFinite(n); } + function buildProductContents(productList) { + if (!productList || !productList.length) { + return []; + } + try { + return productList + .filter(function (p) { return p && (p.Sku || p.Id); }) + .map(function (p) { + return { + id: p.Sku || p.Id, + quantity: isNumeric(p.Quantity) ? p.Quantity : 1, + product_type: 'test', + content_name: p.Name, + name: p.Name, + variant: p.Variant, + category: p.Category + }; + }); + } catch (e) { + return []; + } + } + function getEventCategoryString(event) { var enumTypeValues; From 93c20a62dc583fc83085c31a23f3f398d75b2a58 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Fri, 7 Nov 2025 15:59:08 -0500 Subject: [PATCH 4/9] update FbKit to handle productAttributesMapping --- src/FacebookEventForwarder.js | 46 +++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/FacebookEventForwarder.js b/src/FacebookEventForwarder.js index b0bcd6d..1dd7984 100755 --- a/src/FacebookEventForwarder.js +++ b/src/FacebookEventForwarder.js @@ -109,6 +109,7 @@ fbq('init', settings.pixelId, visitorData); isInitialized = true; + self._settings = settings || {}; return 'Successfully initialized: ' + name; @@ -309,23 +310,64 @@ return !isNaN(parseFloat(n)) && isFinite(n); } + function parseMappings(str) { + if (!str) return []; + try { return JSON.parse((str + '').replace(/"/g, '"')); } catch (_) { return []; } + } + + // Resolve a simple product key by checking top-level first, then Attributes bag + function getField(product, key) { + if (!product || !key) return undefined; + if (Object.prototype.hasOwnProperty.call(product, key)) { + return product[key]; + } + var attrs = product.Attributes; + if (attrs && typeof attrs === 'object' && Object.prototype.hasOwnProperty.call(attrs, key)) { + return attrs[key]; + } + return undefined; + } + function buildProductContents(productList) { if (!productList || !productList.length) { return []; } try { + var setting = self._settings; + var mappings = parseMappings(setting.productAttributeMapping); + var sourceKey = setting.productAttributeSource; + var destKey = setting.productAttributeDestKey; return productList .filter(function (p) { return p && (p.Sku || p.Id); }) .map(function (p) { - return { + var obj = { id: p.Sku || p.Id, quantity: isNumeric(p.Quantity) ? p.Quantity : 1, - product_type: 'test', content_name: p.Name, name: p.Name, variant: p.Variant, category: p.Category }; + if (mappings) { + mappings.forEach(function (m) { + if (!m || !m.map || !m.value) return; + var val = getField(p, m.map); + if (val !== undefined && val !== null) { + obj[m.value] = val; + } + }); + return obj; + } + var srcVal = ''; + if (sourceKey) { + if (p.Attributes && typeof p.Attributes === 'object' && p.Attributes.hasOwnProperty(sourceKey)) { + srcVal = p.Attributes[sourceKey]; + } else if (p.hasOwnProperty(sourceKey)) { + srcVal = p[sourceKey]; + } + } + obj[destKey] = srcVal; + return obj; }); } catch (e) { return []; From ac918f03f988bee9fbedd3bc3551e847718773ab Mon Sep 17 00:00:00 2001 From: Jaissica Date: Tue, 11 Nov 2025 09:18:29 -0500 Subject: [PATCH 5/9] update buildProductContents function and add tests for Purchase event --- src/FacebookEventForwarder.js | 141 ++++++++++++++++------------------ test/tests.js | 74 ++++++++++++++++++ 2 files changed, 142 insertions(+), 73 deletions(-) diff --git a/src/FacebookEventForwarder.js b/src/FacebookEventForwarder.js index 1dd7984..25549d2 100755 --- a/src/FacebookEventForwarder.js +++ b/src/FacebookEventForwarder.js @@ -60,11 +60,14 @@ constructor = function () { var self = this, isInitialized = false, - reportingService = null; + reportingService = null, + settings, + productAttributeMapping; self.name = name; - function initForwarder(settings, service, testMode, trackerId, userAttributes, userIdentities) { + function initForwarder(forwarderSettings, service, testMode, trackerId, userAttributes, userIdentities) { + settings = forwarderSettings; reportingService = service; SupportedCommerceTypes = [ @@ -107,9 +110,10 @@ fbq.disablePushState = true; } fbq('init', settings.pixelId, visitorData); + + loadMappings(); isInitialized = true; - self._settings = settings || {}; return 'Successfully initialized: ' + name; @@ -119,6 +123,12 @@ } } + function loadMappings() { + productAttributeMapping = settings.productAttributeMapping + ? JSON.parse(settings.productAttributeMapping.replace(/"/g, '"')) + : {}; + } + function processEvent(event) { var reportEvent = false; @@ -213,7 +223,7 @@ } else if (event.ProductAction.ProductActionType == mParticle.ProductActionType.Checkout || - event.ProductAction.ProductActionType == mParticle.ProductActionType.Purchase) { + event.ProductAction.ProductActionType == mParticle.ProductActionType.Purchase) { eventName = event.ProductAction.ProductActionType == mParticle.ProductActionType.Checkout ? CHECKOUT_EVENT_NAME : PURCHASE_EVENT_NAME; @@ -228,15 +238,16 @@ return sum; }, 0); params['num_items'] = num_items; + + if (event.ProductAction.TransactionId) { + params['order_id'] = event.ProductAction.TransactionId; + } + // Build contents array for Purchase events if (event.ProductAction.ProductActionType == mParticle.ProductActionType.Purchase) { var contents = buildProductContents(event.ProductAction.ProductList); - if (contents && contents.length) { + if (contents && contents.length > 0) { params['contents'] = contents; - params['content_type'] = 'product'; - } - if (event.ProductAction.TransactionId) { - params['order_id'] = event.ProductAction.TransactionId; } } } @@ -310,70 +321,6 @@ return !isNaN(parseFloat(n)) && isFinite(n); } - function parseMappings(str) { - if (!str) return []; - try { return JSON.parse((str + '').replace(/"/g, '"')); } catch (_) { return []; } - } - - // Resolve a simple product key by checking top-level first, then Attributes bag - function getField(product, key) { - if (!product || !key) return undefined; - if (Object.prototype.hasOwnProperty.call(product, key)) { - return product[key]; - } - var attrs = product.Attributes; - if (attrs && typeof attrs === 'object' && Object.prototype.hasOwnProperty.call(attrs, key)) { - return attrs[key]; - } - return undefined; - } - - function buildProductContents(productList) { - if (!productList || !productList.length) { - return []; - } - try { - var setting = self._settings; - var mappings = parseMappings(setting.productAttributeMapping); - var sourceKey = setting.productAttributeSource; - var destKey = setting.productAttributeDestKey; - return productList - .filter(function (p) { return p && (p.Sku || p.Id); }) - .map(function (p) { - var obj = { - id: p.Sku || p.Id, - quantity: isNumeric(p.Quantity) ? p.Quantity : 1, - content_name: p.Name, - name: p.Name, - variant: p.Variant, - category: p.Category - }; - if (mappings) { - mappings.forEach(function (m) { - if (!m || !m.map || !m.value) return; - var val = getField(p, m.map); - if (val !== undefined && val !== null) { - obj[m.value] = val; - } - }); - return obj; - } - var srcVal = ''; - if (sourceKey) { - if (p.Attributes && typeof p.Attributes === 'object' && p.Attributes.hasOwnProperty(sourceKey)) { - srcVal = p.Attributes[sourceKey]; - } else if (p.hasOwnProperty(sourceKey)) { - srcVal = p[sourceKey]; - } - } - obj[destKey] = srcVal; - return obj; - }); - } catch (e) { - return []; - } - } - function getEventCategoryString(event) { var enumTypeValues; @@ -399,6 +346,54 @@ return null; } + /** + * Builds contents array for Facebook Pixel commerce events. + * Creates a nested array of content items with product details. + * + * @param {Array} productList - Array of products from event.ProductAction.ProductList + * @returns {Array} Array of content objects for Facebook Pixel + */ + function buildProductContents(productList) { + if (!productList || productList.length === 0) { + return []; + } + + return productList.map(function(product) { + var contentItem = { + id: product.Id, + quantity: isNumeric(product.Quantity) ? product.Quantity : 1, + name: product.Name, + brand: product.Brand, + category: product.Category, + variant: product.Variant, + item_price: isNumeric(product.Price) ? product.Price : null + }; + + // Apply configured mappings to custom attributes + for (var sourceField in productAttributeMapping) { + if (productAttributeMapping.hasOwnProperty(sourceField)) { + var facebookFieldName = productAttributeMapping[sourceField]; + var value = null; + + // Check standard product field first + if (product.hasOwnProperty(sourceField)) { + value = product[sourceField]; + } + // Then check custom attributes + else if (product.Attributes && product.Attributes[sourceField]) { + value = product.Attributes[sourceField]; + } + + if (value !== null && value !== undefined) { + contentItem[facebookFieldName] = value; + } + } + } + + return contentItem; + }); + } + // https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events#event-deduplication-options function createEventId(event) { return { diff --git a/test/tests.js b/test/tests.js index c441f45..329ae44 100755 --- a/test/tests.js +++ b/test/tests.js @@ -481,6 +481,7 @@ describe('Facebook Forwarder', function () { window.fbqObj.params.should.have.property('content_name', 'eCommerce - AddToCart'); window.fbqObj.params.should.have.property('content_ids', ['12345']); window.fbqObj.eventData.should.have.property('eventID', SOURCE_MESSAGE_ID); + window.fbqObj.params.should.not.have.property('contents'); done(); }); @@ -857,5 +858,78 @@ describe('Facebook Forwarder', function () { window.fbqObj.should.have.property('trackCalled', false); done(); }); + + it('should build contents array with mapped attributes for Purchase events', function (done) { + // Initialize with product attribute mapping (as JSON string) + mParticle.forwarder.init({ + pixelCode: 'test-pixel-code', + productAttributeMapping: JSON.stringify({ + 'Name': 'custom_name', + 'Brand': 'custom_brand', + 'Price': 'custom_price', + 'category': 'custom_attribute_category', + 'Category': 'custom_category', + + }) + }, reportService.cb, true); + + mParticle.forwarder.process({ + EventName: 'eCommerce - Purchase', + EventDataType: MessageType.Commerce, + ProductAction: { + ProductActionType: ProductActionType.Purchase, + ProductList: [ + { + Id: 'id-12', + Name: 'iPhone', + Brand: 'Apple', + Category: 'electronics', + Variant: 'blue', + Price: 1000.99, + Quantity: 1, + Attributes: { + category: 'phones' + } + }, + { + Id: 'id-34', + Name: 'Watch', + Brand: 'Samsung', + Price: 450.99, + Quantity: 2 + } + ], + TransactionId: 'txn-1234', + TotalAmount: 1451.98 + }, + CurrencyCode: 'USD', + SourceMessageId: SOURCE_MESSAGE_ID, + }); + + checkBasicProperties('track'); + window.fbqObj.should.have.property('eventName', 'Purchase'); + window.fbqObj.params.should.have.property('order_id', 'txn-1234'); + window.fbqObj.params.should.have.property('contents'); + window.fbqObj.params.contents.length.should.equal(2); + + var firstProduct = window.fbqObj.params.contents[0]; + // Standard Facebook fields + firstProduct.should.have.property('id', 'id-12'); + firstProduct.should.have.property('name', 'iPhone'); + firstProduct.should.have.property('brand', 'Apple'); + firstProduct.should.have.property('item_price', 1000.99); + firstProduct.should.have.property('quantity', 1); + + // Mapped standard fields + firstProduct.should.have.property('custom_name', 'iPhone'); + firstProduct.should.have.property('custom_brand', 'Apple'); + firstProduct.should.have.property('custom_price', 1000.99); + firstProduct.should.have.property('custom_category', 'electronics'); + + // Mapped custom attribute + firstProduct.should.have.property('custom_attribute_category', 'phones'); + + done(); + }); }); }); From 517c3ac5fa8e7db772bf862f3abd8b2966b4c961 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Tue, 11 Nov 2025 09:37:53 -0500 Subject: [PATCH 6/9] add Sku requirement for Purchase event and update tests --- src/FacebookEventForwarder.js | 69 ++++++++++++++++++----------------- test/tests.js | 6 +-- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/FacebookEventForwarder.js b/src/FacebookEventForwarder.js index 25549d2..48654f8 100755 --- a/src/FacebookEventForwarder.js +++ b/src/FacebookEventForwarder.js @@ -357,41 +357,44 @@ if (!productList || productList.length === 0) { return []; } - - return productList.map(function(product) { - var contentItem = { - id: product.Id, - quantity: isNumeric(product.Quantity) ? product.Quantity : 1, - name: product.Name, - brand: product.Brand, - category: product.Category, - variant: product.Variant, - item_price: isNumeric(product.Price) ? product.Price : null - }; - - // Apply configured mappings to custom attributes - for (var sourceField in productAttributeMapping) { - if (productAttributeMapping.hasOwnProperty(sourceField)) { - var facebookFieldName = productAttributeMapping[sourceField]; - var value = null; - - // Check standard product field first - if (product.hasOwnProperty(sourceField)) { - value = product[sourceField]; - } - // Then check custom attributes - else if (product.Attributes && product.Attributes[sourceField]) { - value = product.Attributes[sourceField]; - } - - if (value !== null && value !== undefined) { - contentItem[facebookFieldName] = value; + + return productList + .filter(function(product) { + return product && product.Sku; + }) + .map(function(product) { + var contentItem = { + id: product.Sku, + quantity: isNumeric(product.Quantity) ? product.Quantity : 1, + name: product.Name, + brand: product.Brand, + category: product.Category, + variant: product.Variant, + item_price: isNumeric(product.Price) ? product.Price : null + }; + + // Apply configured mappings to custom attributes + for (var sourceField in productAttributeMapping) { + if (productAttributeMapping.hasOwnProperty(sourceField)) { + var facebookFieldName = productAttributeMapping[sourceField]; + var value = null; + + // check for standard product field + if (product.hasOwnProperty(sourceField)) { + value = product[sourceField]; + } + // check for custom attributes + else if (product.Attributes && product.Attributes[sourceField]) { + value = product.Attributes[sourceField]; + } + + if (value !== null && value !== undefined) { + contentItem[facebookFieldName] = value; + } } } - } - - return contentItem; - }); + return contentItem; + }); } // https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events#event-deduplication-options diff --git a/test/tests.js b/test/tests.js index 329ae44..1b0ce79 100755 --- a/test/tests.js +++ b/test/tests.js @@ -880,7 +880,7 @@ describe('Facebook Forwarder', function () { ProductActionType: ProductActionType.Purchase, ProductList: [ { - Id: 'id-12', + Sku: 'sku-12', Name: 'iPhone', Brand: 'Apple', Category: 'electronics', @@ -892,7 +892,7 @@ describe('Facebook Forwarder', function () { } }, { - Id: 'id-34', + Sku: 'sku-34', Name: 'Watch', Brand: 'Samsung', Price: 450.99, @@ -914,7 +914,7 @@ describe('Facebook Forwarder', function () { var firstProduct = window.fbqObj.params.contents[0]; // Standard Facebook fields - firstProduct.should.have.property('id', 'id-12'); + firstProduct.should.have.property('id', 'sku-12'); firstProduct.should.have.property('name', 'iPhone'); firstProduct.should.have.property('brand', 'Apple'); firstProduct.should.have.property('item_price', 1000.99); From 7ab307e103cfe1c7883707522d957b1aec385a07 Mon Sep 17 00:00:00 2001 From: Jaissica Date: Tue, 11 Nov 2025 11:45:21 -0500 Subject: [PATCH 7/9] adapt product attribute mapping to use array format and update related tests --- src/FacebookEventForwarder.js | 41 +++++++++++++++++++---------------- test/tests.js | 9 +------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/FacebookEventForwarder.js b/src/FacebookEventForwarder.js index 48654f8..a811d72 100755 --- a/src/FacebookEventForwarder.js +++ b/src/FacebookEventForwarder.js @@ -126,7 +126,7 @@ function loadMappings() { productAttributeMapping = settings.productAttributeMapping ? JSON.parse(settings.productAttributeMapping.replace(/"/g, '"')) - : {}; + : []; } function processEvent(event) { @@ -374,25 +374,28 @@ }; // Apply configured mappings to custom attributes - for (var sourceField in productAttributeMapping) { - if (productAttributeMapping.hasOwnProperty(sourceField)) { - var facebookFieldName = productAttributeMapping[sourceField]; - var value = null; - - // check for standard product field - if (product.hasOwnProperty(sourceField)) { - value = product[sourceField]; - } - // check for custom attributes - else if (product.Attributes && product.Attributes[sourceField]) { - value = product.Attributes[sourceField]; - } - - if (value !== null && value !== undefined) { - contentItem[facebookFieldName] = value; - } + productAttributeMapping.forEach(function(productMapping) { + if (!productMapping || !productMapping.map || !productMapping.value) { + return; } - } + + var sourceField = productMapping.map; + var facebookFieldName = productMapping.value; + var value = null; + + // Check for Product level field first + if (product.hasOwnProperty(sourceField)) { + value = product[sourceField]; + } + // then check for Product.Attributes level field + else if (product.Attributes && product.Attributes[sourceField]) { + value = product.Attributes[sourceField]; + } + + if (value !== null && value !== undefined) { + contentItem[facebookFieldName] = value; + } + }); return contentItem; }); } diff --git a/test/tests.js b/test/tests.js index 1b0ce79..b9fe7c0 100755 --- a/test/tests.js +++ b/test/tests.js @@ -863,14 +863,7 @@ describe('Facebook Forwarder', function () { // Initialize with product attribute mapping (as JSON string) mParticle.forwarder.init({ pixelCode: 'test-pixel-code', - productAttributeMapping: JSON.stringify({ - 'Name': 'custom_name', - 'Brand': 'custom_brand', - 'Price': 'custom_price', - 'category': 'custom_attribute_category', - 'Category': 'custom_category', - - }) + "productAttributeMapping":"[{"jsmap":"3373707","map":"Name","maptype":"ProductAttributeSelector.Name","value":"custom_name"},{"jsmap":"93997959","map":"Brand","maptype":"ProductAttributeSelector.Name","value":"custom_brand"},{"jsmap":"106934601","map":"Price","maptype":"ProductAttributeSelector.Name","value":"custom_price"},{"jsmap":"50511102","map":"Category","maptype":"ProductAttributeSelector.Name","value":"custom_category"},{"jsmap":"94842723","map":"category","maptype":"ProductAttributeSelector.Name","value":"custom_attribute_category"}]" }, reportService.cb, true); mParticle.forwarder.process({ From 575600f4eb5ca1f56b14801a82bb05b7283f717c Mon Sep 17 00:00:00 2001 From: Jaissica Date: Tue, 11 Nov 2025 14:23:15 -0500 Subject: [PATCH 8/9] add a check to validate if productMapping is an object Co-authored-by: Robert Ing --- src/FacebookEventForwarder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FacebookEventForwarder.js b/src/FacebookEventForwarder.js index a811d72..87455ea 100755 --- a/src/FacebookEventForwarder.js +++ b/src/FacebookEventForwarder.js @@ -375,7 +375,7 @@ // Apply configured mappings to custom attributes productAttributeMapping.forEach(function(productMapping) { - if (!productMapping || !productMapping.map || !productMapping.value) { + if (!isObject(productMapping) || !productMapping.map || !productMapping.value) { return; } From 0f029f945972bdce75ca7fd0b5637a67a844a5cb Mon Sep 17 00:00:00 2001 From: Jaissica Date: Tue, 11 Nov 2025 14:50:30 -0500 Subject: [PATCH 9/9] fix isobject casing --- src/FacebookEventForwarder.js | 2 +- test/tests.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FacebookEventForwarder.js b/src/FacebookEventForwarder.js index 87455ea..782a8ae 100755 --- a/src/FacebookEventForwarder.js +++ b/src/FacebookEventForwarder.js @@ -375,7 +375,7 @@ // Apply configured mappings to custom attributes productAttributeMapping.forEach(function(productMapping) { - if (!isObject(productMapping) || !productMapping.map || !productMapping.value) { + if (!isobject(productMapping) || !productMapping.map || !productMapping.value) { return; } diff --git a/test/tests.js b/test/tests.js index b9fe7c0..b0e0763 100755 --- a/test/tests.js +++ b/test/tests.js @@ -860,7 +860,7 @@ describe('Facebook Forwarder', function () { }); it('should build contents array with mapped attributes for Purchase events', function (done) { - // Initialize with product attribute mapping (as JSON string) + // Initialize with product attribute mapping mParticle.forwarder.init({ pixelCode: 'test-pixel-code', "productAttributeMapping":"[{"jsmap":"3373707","map":"Name","maptype":"ProductAttributeSelector.Name","value":"custom_name"},{"jsmap":"93997959","map":"Brand","maptype":"ProductAttributeSelector.Name","value":"custom_brand"},{"jsmap":"106934601","map":"Price","maptype":"ProductAttributeSelector.Name","value":"custom_price"},{"jsmap":"50511102","map":"Category","maptype":"ProductAttributeSelector.Name","value":"custom_category"},{"jsmap":"94842723","map":"category","maptype":"ProductAttributeSelector.Name","value":"custom_attribute_category"}]"