diff --git a/src/v0/destinations/facebook_pixel/config.js b/src/v0/destinations/facebook_pixel/config.js index 09be8e043d..e3c20d291c 100644 --- a/src/v0/destinations/facebook_pixel/config.js +++ b/src/v0/destinations/facebook_pixel/config.js @@ -15,32 +15,38 @@ const CONFIG_CATEGORIES = { PRODUCT_LIST_VIEWED: { standard: true, type: 'product list viewed', - name: 'FBPIXELPSimpleCustomConfig', + eventName: 'ViewContent', + name: 'FBPIXELProductListViewedCustomData', }, PRODUCT_VIEWED: { standard: true, type: 'product viewed', - name: 'FBPIXELPSimpleCustomConfig', + eventName: 'ViewContent', + name: 'FBPIXELProductViewedCustomData', }, PRODUCT_ADDED: { standard: true, type: 'product added', - name: 'FBPIXELPSimpleCustomConfig', + eventName: 'AddToCart', + name: 'FBPIXELProductAddedCustomData', }, ORDER_COMPLETED: { standard: true, type: 'order completed', - name: 'FBPIXELPSimpleCustomConfig', + eventName: 'Purchase', + name: 'FBPIXELOrderCompletedCustomData', }, PRODUCTS_SEARCHED: { standard: true, type: 'products searched', - name: 'FBPIXELPSimpleCustomConfig', + eventName: 'Search', + name: 'FBPIXELProductSearchedCustomData', }, CHECKOUT_STARTED: { standard: true, type: 'checkout started', - name: 'FBPIXELPSimpleCustomConfig', + eventName: 'InitiateCheckout', + name: 'FBPIXELCheckoutStartedCustomData', }, OTHER_STANDARD: { standard: true, @@ -50,9 +56,15 @@ const CONFIG_CATEGORIES = { PAGE_VIEW: { standard: true, type: 'page_view', + eventName: 'PageView', + name: 'FBPIXELPSimpleCustomConfig', + }, + PAGE: { + standard: false, + type: 'page', + eventName: 'PageView', name: 'FBPIXELPSimpleCustomConfig', }, - PAGE: { standard: false, type: 'page', name: 'FBPIXELPSimpleCustomConfig' }, }; const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); @@ -67,6 +79,21 @@ const ACTION_SOURCES_VALUES = [ 'other', ]; +const OTHER_STANDARD_EVENTS = [ + 'AddToWishlist', + 'AddPaymentInfo', + 'Lead', + 'CompleteRegistration', + 'Contact', + 'CustomizeProduct', + 'Donate', + 'FindLocation', + 'Schedule', + 'StartTrial', + 'SubmitApplication', + 'Subscribe', +]; + const FB_PIXEL_DEFAULT_EXCLUSION = ['opt_out', 'event_id', 'action_source']; const STANDARD_ECOMM_EVENTS_TYPE = [ CONFIG_CATEGORIES.PRODUCT_LIST_VIEWED.type, @@ -83,5 +110,6 @@ module.exports = { ACTION_SOURCES_VALUES, FB_PIXEL_DEFAULT_EXCLUSION, STANDARD_ECOMM_EVENTS_TYPE, + OTHER_STANDARD_EVENTS, DESTINATION: 'FACEBOOK_PIXEL', }; diff --git a/src/v0/destinations/facebook_pixel/data/FBPIXELCheckoutStartedCustomData.json b/src/v0/destinations/facebook_pixel/data/FBPIXELCheckoutStartedCustomData.json new file mode 100644 index 0000000000..47938912b4 --- /dev/null +++ b/src/v0/destinations/facebook_pixel/data/FBPIXELCheckoutStartedCustomData.json @@ -0,0 +1,48 @@ +[ + { + "destKey": "content_category", + "sourceKeys": "properties.category" + }, + { + "destKey": "content_ids", + "sourceKeys": "", + "metadata": { + "defaultValue": [] + } + }, + { + "destKey": "content_type", + "sourceKeys": "", + "metadata": { + "defaultValue": "product" + } + }, + { + "destKey": "currency", + "sourceKeys": "properties.currency", + "metadata": { + "defaultValue": "USD" + } + }, + { + "destKey": "value", + "sourceKeys": "properties.revenue", + "metadata": { + "type": "numberForRevenue" + } + }, + { + "destKey": "contents", + "sourceKeys": "", + "metadata": { + "defaultValue": [] + } + }, + { + "destKey": "num_items", + "sourceKeys": "", + "metadata": { + "defaultValue": 0 + } + } +] diff --git a/src/v0/destinations/facebook_pixel/data/FBPIXELOrderCompletedCustomData.json b/src/v0/destinations/facebook_pixel/data/FBPIXELOrderCompletedCustomData.json new file mode 100644 index 0000000000..a95315008b --- /dev/null +++ b/src/v0/destinations/facebook_pixel/data/FBPIXELOrderCompletedCustomData.json @@ -0,0 +1,52 @@ +[ + { + "destKey": "content_category", + "sourceKeys": "properties.category" + }, + { + "destKey": "content_ids", + "sourceKeys": "", + "metadata": { + "defaultValue": [] + } + }, + { + "destKey": "content_type", + "sourceKeys": "", + "metadata": { + "defaultValue": "product" + } + }, + { + "destKey": "currency", + "sourceKeys": "properties.currency", + "metadata": { + "defaultValue": "USD" + } + }, + { + "destKey": "value", + "sourceKeys": "properties.revenue", + "metadata": { + "type": "numberForRevenue" + } + }, + { + "destKey": "contents", + "sourceKeys": "", + "metadata": { + "defaultValue": [] + } + }, + { + "destKey": "num_items", + "sourceKeys": "", + "metadata": { + "defaultValue": 0 + } + }, + { + "destKey": "content_name", + "sourceKeys": "properties.contentName" + } +] diff --git a/src/v0/destinations/facebook_pixel/data/FBPIXELProductAddedCustomData.json b/src/v0/destinations/facebook_pixel/data/FBPIXELProductAddedCustomData.json new file mode 100644 index 0000000000..786cb58e62 --- /dev/null +++ b/src/v0/destinations/facebook_pixel/data/FBPIXELProductAddedCustomData.json @@ -0,0 +1,48 @@ +[ + { + "destKey": "content_ids", + "sourceKeys": ["properties.product_id", "properties.sku", "properties.id"] + }, + { + "destKey": "content_type", + "sourceKeys": "", + "metadata": { + "defaultValue": "product" + } + }, + { + "destKey": "content_name", + "sourceKeys": ["properties.product_name", "properties.name"], + "metadata": { + "defaultValue": "" + } + }, + { + "destKey": "content_category", + "sourceKeys": "properties.category", + "metadata": { + "defaultValue": "" + } + }, + { + "destKey": "currency", + "sourceKeys": "properties.currency", + "metadata": { + "defaultValue": "USD" + } + }, + { + "destKey": "value", + "sourceKeys": "properties.value", + "metadata": { + "type": "numberForRevenue" + } + }, + { + "destKey": "contents", + "sourceKeys": "", + "metadata": { + "defaultValue": [] + } + } +] diff --git a/src/v0/destinations/facebook_pixel/data/FBPIXELProductListViewedCustomData.json b/src/v0/destinations/facebook_pixel/data/FBPIXELProductListViewedCustomData.json new file mode 100644 index 0000000000..d2658f33e7 --- /dev/null +++ b/src/v0/destinations/facebook_pixel/data/FBPIXELProductListViewedCustomData.json @@ -0,0 +1,35 @@ +[ + { + "destKey": "content_ids", + "sourceKeys": "", + "metadata": { + "defaultValue": [] + } + }, + { + "destKey": "content_type", + "sourceKeys": "" + }, + { + "destKey": "contents", + "sourceKeys": "", + "metadata": { + "defaultValue": [] + } + }, + { + "destKey": "content_category", + "sourceKeys": "properties.category" + }, + { + "destKey": "content_name", + "sourceKeys": "properties.contentName" + }, + { + "destKey": "value", + "sourceKeys": "properties.value", + "metadata": { + "type": "numberForRevenue" + } + } +] diff --git a/src/v0/destinations/facebook_pixel/data/FBPIXELProductSearchedCustomData.json b/src/v0/destinations/facebook_pixel/data/FBPIXELProductSearchedCustomData.json new file mode 100644 index 0000000000..15635b7cf3 --- /dev/null +++ b/src/v0/destinations/facebook_pixel/data/FBPIXELProductSearchedCustomData.json @@ -0,0 +1,39 @@ +[ + { + "destKey": "content_ids", + "sourceKeys": ["properties.product_id", "properties.sku", "properties.id"] + }, + { + "destKey": "content_category", + "sourceKeys": "properties.category", + "metadata": { + "defaultValue": "" + } + }, + { + "destKey": "value", + "sourceKeys": "properties.value", + "metadata": { + "type": "numberForRevenue" + } + }, + { + "destKey": "contents.0.id", + "sourceKeys": ["properties.product_id", "properties.sku", "properties.id"] + }, + { + "destKey": "contents.0.quantity", + "sourceKeys": "properties.quantity", + "metadata": { + "defaultValue": 1 + } + }, + { + "destKey": "contents.0.item_price", + "sourceKeys": "properties.price" + }, + { + "destKey": "search_string", + "sourceKeys": "properties.query" + } +] diff --git a/src/v0/destinations/facebook_pixel/data/FBPIXELProductViewedCustomData.json b/src/v0/destinations/facebook_pixel/data/FBPIXELProductViewedCustomData.json new file mode 100644 index 0000000000..e233641b8a --- /dev/null +++ b/src/v0/destinations/facebook_pixel/data/FBPIXELProductViewedCustomData.json @@ -0,0 +1,56 @@ +[ + { + "destKey": "content_ids", + "sourceKeys": ["properties.product_id", "properties.sku", "properties.id"] + }, + { + "destKey": "content_type", + "sourceKeys": "", + "metadata": { + "defaultValue": "product" + } + }, + { + "destKey": "content_name", + "sourceKeys": ["properties.product_name", "properties.name"], + "metadata": { + "defaultValue": "" + } + }, + { + "destKey": "content_category", + "sourceKeys": "properties.category", + "metadata": { + "defaultValue": "" + } + }, + { + "destKey": "currency", + "sourceKeys": "properties.currency", + "metadata": { + "defaultValue": "USD" + } + }, + { + "destKey": "value", + "sourceKeys": "properties.value", + "metadata": { + "type": "numberForRevenue" + } + }, + { + "destKey": "contents.0.id", + "sourceKeys": ["properties.product_id", "properties.sku", "properties.id"] + }, + { + "destKey": "contents.0.quantity", + "sourceKeys": "properties.quantity", + "metadata": { + "defaultValue": 1 + } + }, + { + "destKey": "contents.0.item_price", + "sourceKeys": "properties.price" + } +] diff --git a/src/v0/destinations/facebook_pixel/transform.js b/src/v0/destinations/facebook_pixel/transform.js index c81718dea0..798b3157d9 100644 --- a/src/v0/destinations/facebook_pixel/transform.js +++ b/src/v0/destinations/facebook_pixel/transform.js @@ -17,24 +17,27 @@ const { getIntegrationsObj, getValidDynamicFormConfig, simpleProcessRouterDest, + getHashFromArray, } = require('../../util'); const { transformedPayloadData, getActionSource, fetchUserData, - handleProduct, - handleSearch, - handleProductListViewed, - handleOrder, formingFinalResponse, + populateCustomDataBasedOnCategory, + getCategoryFromEvent, } = require('./utils'); const { InstrumentationError, ConfigurationError } = require('../../util/errorTypes'); -const responseBuilderSimple = (message, category, destination, categoryToContent) => { - const { Config } = destination; +const responseBuilderSimple = (message, category, destination) => { + const { Config, ID } = destination; const { pixelId, accessToken } = Config; + let { categoryToContent } = Config; + if (Array.isArray(categoryToContent)) { + categoryToContent = getValidDynamicFormConfig(categoryToContent, 'from', 'to', 'FB_PIXEL', ID); + } if (!pixelId) { throw new ConfigurationError('Pixel Id not found. Aborting'); @@ -59,12 +62,15 @@ const responseBuilderSimple = (message, category, destination, categoryToContent const userData = fetchUserData(message, Config); - let customData = {}; - let commonData = {}; - - commonData = constructPayload(message, MAPPING_CONFIG[CONFIG_CATEGORIES.COMMON.name], 'fb_pixel'); + const commonData = constructPayload( + message, + MAPPING_CONFIG[CONFIG_CATEGORIES.COMMON.name], + 'fb_pixel', + ); commonData.action_source = getActionSource(commonData, message?.channel); + let customData = {}; + if (category.type !== 'identify') { customData = flattenJson( extractCustomFields(message, customData, ['properties'], FB_PIXEL_DEFAULT_EXCLUSION), @@ -85,71 +91,17 @@ const responseBuilderSimple = (message, category, destination, categoryToContent category.standard, integrationsObj, ); - message.properties = message.properties || {}; if (category.standard) { - switch (category.type) { - case 'product list viewed': - customData = { - ...customData, - ...handleProductListViewed(message, categoryToContent), - }; - commonData.event_name = 'ViewContent'; - break; - case 'product viewed': - customData = { - ...customData, - ...handleProduct(message, categoryToContent, valueFieldIdentifier), - }; - commonData.event_name = 'ViewContent'; - break; - case 'product added': - customData = { - ...customData, - ...handleProduct(message, categoryToContent, valueFieldIdentifier), - }; - commonData.event_name = 'AddToCart'; - break; - case 'order completed': - customData = { - ...customData, - ...handleOrder(message, categoryToContent), - }; - commonData.event_name = 'Purchase'; - break; - case 'products searched': { - customData = { - ...customData, - ...handleSearch(message), - }; - commonData.event_name = 'Search'; - break; - } - case 'checkout started': { - const orderPayload = handleOrder(message, categoryToContent); - delete orderPayload.content_name; - customData = { - ...customData, - ...orderPayload, - }; - commonData.event_name = 'InitiateCheckout'; - break; - } - case 'page_view': // executed when sending track calls but with standard type PageView - case 'page': // executed when page call is done with standard PageView turned on - customData = { ...customData }; - commonData.event_name = 'PageView'; - break; - case 'otherStandard': - customData = { ...customData }; - commonData.event_name = category.event; - break; - default: - throw new InstrumentationError( - `${category.standard} type of standard event does not exist`, - ); - } + commonData.event_name = category.eventName; + customData = populateCustomDataBasedOnCategory( + customData, + message, + category, + categoryToContent, + valueFieldIdentifier, + ); customData.currency = STANDARD_ECOMM_EVENTS_TYPE.includes(category.type) - ? message.properties.currency || 'USD' + ? message.properties?.currency || 'USD' : undefined; } else { const { type } = category; @@ -159,7 +111,7 @@ const responseBuilderSimple = (message, category, destination, categoryToContent : `Viewed a ${type}`; } if (type === 'simple track') { - customData.value = message.properties ? message.properties.revenue : undefined; + customData.value = message.properties?.revenue; delete customData.revenue; } } @@ -189,57 +141,6 @@ const responseBuilderSimple = (message, category, destination, categoryToContent ); }; -function getCategoryFromEvent(checkEvent) { - let category; - switch (checkEvent) { - case CONFIG_CATEGORIES.PRODUCT_LIST_VIEWED.type: - case 'ViewContent': - category = CONFIG_CATEGORIES.PRODUCT_LIST_VIEWED; - break; - case CONFIG_CATEGORIES.PRODUCT_VIEWED.type: - category = CONFIG_CATEGORIES.PRODUCT_VIEWED; - break; - case CONFIG_CATEGORIES.PRODUCT_ADDED.type: - case 'AddToCart': - category = CONFIG_CATEGORIES.PRODUCT_ADDED; - break; - case CONFIG_CATEGORIES.ORDER_COMPLETED.type: - case 'Purchase': - category = CONFIG_CATEGORIES.ORDER_COMPLETED; - break; - case CONFIG_CATEGORIES.PRODUCTS_SEARCHED.type: - case 'Search': - category = CONFIG_CATEGORIES.PRODUCTS_SEARCHED; - break; - case CONFIG_CATEGORIES.CHECKOUT_STARTED.type: - case 'InitiateCheckout': - category = CONFIG_CATEGORIES.CHECKOUT_STARTED; - break; - case 'AddToWishlist': - case 'AddPaymentInfo': - case 'Lead': - case 'CompleteRegistration': - case 'Contact': - case 'CustomizeProduct': - case 'Donate': - case 'FindLocation': - case 'Schedule': - case 'StartTrial': - case 'SubmitApplication': - case 'Subscribe': - category = CONFIG_CATEGORIES.OTHER_STANDARD; - category.event = checkEvent; - break; - case 'PageView': - category = CONFIG_CATEGORIES.PAGE_VIEW; - break; - default: - category = CONFIG_CATEGORIES.SIMPLE_TRACK; - break; - } - return category; -} - const processEvent = (message, destination) => { if (!message.type) { throw new InstrumentationError("'type' is missing"); @@ -265,7 +166,7 @@ const processEvent = (message, destination) => { } let eventsToEvents; - if (destination.Config.eventsToEvents) + if (Array.isArray(destination.Config.eventsToEvents)) { eventsToEvents = getValidDynamicFormConfig( destination.Config.eventsToEvents, 'from', @@ -273,21 +174,12 @@ const processEvent = (message, destination) => { 'FB_PIXEL', destination.ID, ); - let categoryToContent; - if (destination.Config.categoryToContent) - categoryToContent = getValidDynamicFormConfig( - destination.Config.categoryToContent, - 'from', - 'to', - 'FB_PIXEL', - destination.ID, - ); + } + const { advancedMapping } = destination.Config; - let standard; - let standardTo = ''; - let checkEvent; const messageType = message.type.toLowerCase(); let category; + let mappedEvent; switch (messageType) { case EventType.IDENTIFY: if (advancedMapping) { @@ -309,24 +201,17 @@ const processEvent = (message, destination) => { if (typeof message.event !== 'string') { throw new InstrumentationError('event name should be string'); } - standard = eventsToEvents; - if (standard) { - standardTo = standard.reduce((filtered, standards) => { - if (standards.from.toLowerCase() === message.event.toLowerCase()) { - filtered = standards.to; - } - return filtered; - }, ''); + if (eventsToEvents) { + const eventMappingHash = getHashFromArray(eventsToEvents); + mappedEvent = eventMappingHash[message.event.toLowerCase()]; } - checkEvent = standardTo !== '' ? standardTo : message.event.toLowerCase(); - - category = getCategoryFromEvent(checkEvent); + category = getCategoryFromEvent(mappedEvent || message.event.toLowerCase()); break; default: throw new InstrumentationError(`Message type ${messageType} not supported`); } // build the response - return responseBuilderSimple(message, category, destination, categoryToContent); + return responseBuilderSimple(message, category, destination); }; const process = (event) => processEvent(event.message, event.destination); @@ -338,9 +223,5 @@ const processRouterDest = async (inputs, reqMetadata) => { module.exports = { process, - processRouterDest, - handleSearch, - handleProductListViewed, - handleProduct, - handleOrder, + processRouterDest }; diff --git a/src/v0/destinations/facebook_pixel/transform.test.js b/src/v0/destinations/facebook_pixel/transform.test.js deleted file mode 100644 index 25332d770c..0000000000 --- a/src/v0/destinations/facebook_pixel/transform.test.js +++ /dev/null @@ -1,201 +0,0 @@ -const { - handleSearch, - handleProductListViewed, - handleProduct, - handleOrder, -} = require('../../../../src/v0/destinations/facebook_pixel/transform'); - -const getTestMessage = () => { - let message = { - properties: { - currency: 'CAD', - quantity: 1, - price: 24.75, - value: 30, - name: 'my product 1', - category: 'clothing', - sku: 'p-298', - testDimension: true, - testMetric: true, - position: 4.5, - query: 'HDMI Cable', - }, - }; - return message; -}; - -const getTestCategoryToContent = () => { - let categoryToContent = [ - { - from: 'spin_result', - to: 'Schedule', - }, - ]; - return categoryToContent; -}; - -describe('Unit test cases for facebook_pixel handle search', () => { - it('should return content with all fields not null', async () => { - const expectedOutput = { - content_ids: ['p-298'], - content_category: 'clothing', - value: 30, - search_string: 'HDMI Cable', - contents: [ - { - id: 'p-298', - quantity: 1, - item_price: 24.75, - }, - ], - }; - expect(handleSearch(getTestMessage())).toEqual(expectedOutput); - }); - - it("mapping 'product_id' with contentId", async () => { - let message = getTestMessage(); - message.properties.product_id = 'prd-123'; - - const expectedOutput = { - content_ids: ['prd-123'], - content_category: 'clothing', - value: 30, - search_string: 'HDMI Cable', - contents: [ - { - id: 'prd-123', - quantity: 1, - item_price: 24.75, - }, - ], - }; - expect(handleSearch(message)).toEqual(expectedOutput); - }); - - it("null/undefined 'properties'", async () => { - let message = getTestMessage(); - message.properties = null; - - const expectedOutput = { - content_category: '', - content_ids: [], - contents: [], - search_string: undefined, - value: 0, - }; - expect(handleSearch(message)).toEqual(expectedOutput); - }); -}); - -describe('Unit test cases for facebook_pixel handleProductListViewed', () => { - it('without product array', () => { - let expectedOutput = { - content_category: 'clothing', - content_ids: ['clothing'], - content_name: undefined, - content_type: 'product_group', - contents: [{ id: 'clothing', quantity: 1 }], - value: 30, - }; - expect(handleProductListViewed(getTestMessage(), getTestCategoryToContent())).toEqual( - expectedOutput, - ); - }); - - it('with product array', () => { - let fittingPayload = { ...getTestMessage() }; - fittingPayload.properties.products = [{ id: 'clothing', quantity: 2 }]; - let expectedOutput = { - content_category: 'clothing', - content_ids: ['clothing'], - content_name: undefined, - content_type: 'product', - contents: [{ id: 'clothing', item_price: undefined, quantity: 2 }], - value: 30, - }; - expect(handleProductListViewed(fittingPayload, getTestCategoryToContent())).toEqual( - expectedOutput, - ); - }); -}); - -describe('Unit test cases for facebook_pixel handleProduct', () => { - it('with valueFieldIdentifier properties.value', () => { - let expectedOutput = { - content_category: 'clothing', - content_ids: ['p-298'], - content_name: 'my product 1', - content_type: 'product', - contents: [{ id: 'p-298', item_price: 24.75, quantity: 1 }], - currency: 'CAD', - value: 30, - }; - expect(handleProduct(getTestMessage(), getTestCategoryToContent(), 'properties.value')).toEqual( - expectedOutput, - ); - }); - - it('with valueFieldIdentifier properties.price', () => { - let expectedOutput = { - content_category: 'clothing', - content_ids: ['p-298'], - content_name: 'my product 1', - content_type: 'product', - contents: [{ id: 'p-298', item_price: 24.75, quantity: 1 }], - currency: 'CAD', - value: 24.75, - }; - expect(handleProduct(getTestMessage(), getTestCategoryToContent(), 'properties.price')).toEqual( - expectedOutput, - ); - }); -}); - -describe('Unit test cases for facebook_pixel handleOrder', () => { - it('without product array', () => { - let expectedOutput = { - content_category: 'clothing', - content_ids: [], - content_name: undefined, - content_type: 'product', - contents: [], - currency: 'CAD', - num_items: 0, - value: 0, - }; - expect(handleOrder(getTestMessage(), getTestCategoryToContent())).toEqual(expectedOutput); - }); - - it('with product array without revenue', () => { - let fittingPayload = { ...getTestMessage() }; - fittingPayload.properties.products = [{ id: 'clothing', quantity: 2 }]; - let expectedOutput = { - content_category: 'clothing', - content_ids: ['clothing'], - content_name: undefined, - content_type: 'product', - contents: [{ id: 'clothing', item_price: 24.75, quantity: 2 }], - currency: 'CAD', - num_items: 1, - value: 0, - }; - expect(handleOrder(fittingPayload, getTestCategoryToContent())).toEqual(expectedOutput); - }); - - it('with product array with revenue', () => { - let fittingPayload = { ...getTestMessage() }; - fittingPayload.properties.products = [{ id: 'clothing', quantity: 2 }]; - fittingPayload.properties.revenue = 124; - let expectedOutput = { - content_category: 'clothing', - content_ids: ['clothing'], - content_name: undefined, - content_type: 'product', - contents: [{ id: 'clothing', item_price: 24.75, quantity: 2 }], - currency: 'CAD', - num_items: 1, - value: 124, - }; - expect(handleOrder(fittingPayload, getTestCategoryToContent())).toEqual(expectedOutput); - }); -}); diff --git a/src/v0/destinations/facebook_pixel/utils.js b/src/v0/destinations/facebook_pixel/utils.js index e1347278bf..4788065c3d 100644 --- a/src/v0/destinations/facebook_pixel/utils.js +++ b/src/v0/destinations/facebook_pixel/utils.js @@ -7,8 +7,14 @@ const { constructPayload, defaultPostRequestConfig, defaultRequestConfig, + getHashFromArray, } = require('../../util'); -const { ACTION_SOURCES_VALUES, CONFIG_CATEGORIES, MAPPING_CONFIG } = require('./config'); +const { + ACTION_SOURCES_VALUES, + CONFIG_CATEGORIES, + MAPPING_CONFIG, + OTHER_STANDARD_EVENTS, +} = require('./config'); const { InstrumentationError, TransformationError } = require('../../util/errorTypes'); @@ -18,7 +24,7 @@ const { InstrumentationError, TransformationError } = require('../../util/errorT */ const formatRevenue = (revenue) => { - const formattedRevenue = parseFloat(parseFloat(revenue || 0).toFixed(2)); + const formattedRevenue = parseFloat(parseFloat(revenue || '0').toFixed(2)); if (!Number.isNaN(formattedRevenue)) { return formattedRevenue; } @@ -29,7 +35,7 @@ const formatRevenue = (revenue) => { * * @param {*} message Rudder Payload * @param {*} defaultValue product / product_group - * @param {*} categoryToContent [ { from: 'clothing', to: 'product' } ] + * @param {*} categoryToContent example: [ { from: 'clothing', to: 'product' } ] * * We will be mapping properties.category to user provided content else taking the default value as per ecomm spec * If category is clothing it will be set to ["product"] @@ -37,7 +43,6 @@ const formatRevenue = (revenue) => { * - https://developers.facebook.com/docs/facebook-pixel/reference/#object-properties */ const getContentType = (message, defaultValue, categoryToContent) => { - let tempCategoryToContent = categoryToContent; const { properties } = message; const integrationsObj = getIntegrationsObj(message, 'fb_pixel'); @@ -51,22 +56,15 @@ const getContentType = (message, defaultValue, categoryToContent) => { if (products && products.length > 0 && Array.isArray(products) && isObject(products[0])) { category = products[0].category; } - } else { - if (tempCategoryToContent === undefined) { - tempCategoryToContent = []; - } - const mapped = tempCategoryToContent; - const mappedTo = mapped.reduce((filtered, map) => { - let filter = filtered; - if (map.from === category) { - filter = map.to; - } - return filter; - }, ''); - if (mappedTo.length > 0) { - return mappedTo; + } + + if (Array.isArray(categoryToContent) && category) { + const categoryToContentHash = getHashFromArray(categoryToContent, 'from', 'to', false); + if (categoryToContentHash[category]) { + return categoryToContentHash[category]; } } + return defaultValue; }; @@ -190,7 +188,7 @@ const transformedPayloadData = ( /** * * @param {*} message - * @returns fbc parameter which is a combined string of the parameters below + * @returns string which is fbc parameter * * version : "fb" (default) * @@ -302,21 +300,11 @@ const fetchUserData = (message, Config) => { return userData; }; -/** - * - * @param {*} message Rudder element - * @param {*} categoryToContent [ { from: 'clothing', to: 'product' } ] - * - * Handles order completed and checkout started types of specific events - */ -const handleOrder = (message, categoryToContent) => { - const { products, revenue } = message.properties; - const value = formatRevenue(revenue); - - const contentType = getContentType(message, 'product', categoryToContent); +const updateCustomDataForOrderCompletedAndCheckoutStarted = (customData, message, categoryToContent) => { + const { products } = message.properties; const contentIds = []; const contents = []; - const { category, quantity, price, currency, contentName } = message.properties; + const { quantity, price} = message.properties; if (products) { if (products.length > 0 && Array.isArray(products)) { products.forEach((singleProduct) => { @@ -338,30 +326,20 @@ const handleOrder = (message, categoryToContent) => { } } - return { - content_category: getContentCategory(category), - content_ids: contentIds, - content_type: contentType, - currency: currency || 'USD', - value, - contents, - num_items: contentIds.length, - content_name: contentName, - }; + const updatedCustomData = { ...customData } + updatedCustomData.content_category = getContentCategory(customData.content_category); + updatedCustomData.content_ids = contentIds; + updatedCustomData.content_type = getContentType(message, customData.content_type, categoryToContent); + updatedCustomData.contents = contents; + updatedCustomData.num_items = contentIds.length; + + return updatedCustomData; }; -/** - * - * @param {*} message Rudder element - * @param {*} categoryToContent [ { from: 'clothing', to: 'product' } ] - * - * Handles product list viewed - */ -const handleProductListViewed = (message, categoryToContent) => { - let contentType; +const updateCustomDataForProductListViewed = (customData, message, categoryToContent) => { const contentIds = []; const contents = []; - const { products, category, quantity, value, contentName } = message.properties; + const { products, quantity } = message.properties; if (products && products.length > 0 && Array.isArray(products)) { products.forEach((product, index) => { if (isObject(product)) { @@ -380,6 +358,8 @@ const handleProductListViewed = (message, categoryToContent) => { }); } + let contentType; + const category = customData.content_category; if (contentIds.length > 0) { contentType = 'product'; // for viewContent event content_ids and content arrays are not mandatory @@ -392,35 +372,19 @@ const handleProductListViewed = (message, categoryToContent) => { contentType = 'product_group'; } - return { - content_ids: contentIds, - content_type: getContentType(message, contentType, categoryToContent), - contents, - content_category: getContentCategory(category), - content_name: contentName, - value: formatRevenue(value), - }; + const updatedCustomData = { ...customData } + updatedCustomData.content_ids = contentIds; + updatedCustomData.content_type = getContentType(message, contentType, categoryToContent); + updatedCustomData.contents = contents; + updatedCustomData.content_category = getContentCategory(category); + + return updatedCustomData; }; -/** - * - * @param {*} message Rudder Payload - * @param {*} categoryToContent [ { from: 'clothing', to: 'product' } ] - * @param {*} valueFieldIdentifier it can be either value or price which will be matched from properties and assigned to value for fb payload - */ -const handleProduct = (message, categoryToContent, valueFieldIdentifier) => { +const updateCustomDataForProductAddedAndProductViewed = (customData, message, categoryToContent, valueFieldIdentifier) => { const contentIds = []; const contents = []; - const useValue = valueFieldIdentifier === 'properties.value'; - const contentId = - message.properties?.product_id || message.properties?.sku || message.properties?.id; - const contentType = getContentType(message, 'product', categoryToContent); - const contentName = message.properties.product_name || message.properties.name || ''; - const contentCategory = message.properties.category || ''; - const currency = message.properties.currency || 'USD'; - const value = useValue - ? formatRevenue(message.properties.value) - : formatRevenue(message.properties.price); + const contentId = customData.content_ids; if (contentId) { contentIds.push(contentId); contents.push({ @@ -429,19 +393,22 @@ const handleProduct = (message, categoryToContent, valueFieldIdentifier) => { item_price: message.properties.price, }); } - return { - content_ids: contentIds, - content_type: contentType, - content_name: contentName, - content_category: getContentCategory(contentCategory), - currency, - value, - contents, - }; + + const updatedCustomData = { ...customData } + updatedCustomData.content_ids = contentIds; + updatedCustomData.content_type = getContentType(message, customData.content_type, categoryToContent); + updatedCustomData.content_category = getContentCategory(customData.content_category); + const useValue = valueFieldIdentifier === 'properties.value'; + if (!useValue) { + updatedCustomData.value = formatRevenue(message?.properties?.price); + } + updatedCustomData.contents = contents; + + return updatedCustomData; }; -const handleSearch = (message) => { - const query = message?.properties?.query; +const updateCustomDataForProductSearched = (customData, message) => { + const query = customData.search_string; /** * Facebook Pixel states "search_string" a string type * ref: https://developers.facebook.com/docs/meta-pixel/reference#:~:text=an%20exact%20value.-,search_string,-String @@ -455,10 +422,7 @@ const handleSearch = (message) => { const contentIds = []; const contents = []; - const contentId = - message.properties?.product_id || message.properties?.sku || message.properties?.id; - const contentCategory = message?.properties?.category || ''; - const value = message?.properties?.value; + const contentId = customData.content_ids; if (contentId) { contentIds.push(contentId); contents.push({ @@ -467,13 +431,90 @@ const handleSearch = (message) => { item_price: message?.properties?.price, }); } - return { - content_ids: contentIds, - content_category: getContentCategory(contentCategory), - value: formatRevenue(value), - contents, - search_string: query, - }; + + const updatedCustomData = { ...customData } + updatedCustomData.content_ids = contentIds; + updatedCustomData.content_category = getContentCategory(customData.content_category); + updatedCustomData.contents = contents; + + return updatedCustomData; +}; + +const populateCustomDataBasedOnCategory = ( + customData, + message, + category, + categoryToContent, + valueFieldIdentifier, +) => { + let eventTypeCustomData = constructPayload(message, MAPPING_CONFIG[category.name]); + switch (category.type) { + case 'product list viewed': + eventTypeCustomData = updateCustomDataForProductListViewed(eventTypeCustomData, message, categoryToContent); + break; + case 'product viewed': + case 'product added': + eventTypeCustomData = updateCustomDataForProductAddedAndProductViewed(eventTypeCustomData, message, categoryToContent, valueFieldIdentifier); + break; + case 'order completed': + case 'checkout started': + eventTypeCustomData = updateCustomDataForOrderCompletedAndCheckoutStarted(eventTypeCustomData, message, categoryToContent); + break; + case 'products searched': { + eventTypeCustomData = updateCustomDataForProductSearched(eventTypeCustomData, message); + break; + } + case 'page_view': // executed when sending track calls but with standard type PageView + case 'page': // executed when page call is done with standard PageView turned on + case 'otherStandard': + eventTypeCustomData = { ...eventTypeCustomData }; + break; + default: + throw new InstrumentationError(`${category.standard} type of standard event does not exist`); + } + return { ...customData, ...eventTypeCustomData }; +}; + +const getCategoryFromEvent = (eventName) => { + let category; + switch (eventName) { + case CONFIG_CATEGORIES.PRODUCT_LIST_VIEWED.type: + case CONFIG_CATEGORIES.PRODUCT_LIST_VIEWED.eventName: + category = CONFIG_CATEGORIES.PRODUCT_LIST_VIEWED; + break; + case CONFIG_CATEGORIES.PRODUCT_VIEWED.type: + category = CONFIG_CATEGORIES.PRODUCT_VIEWED; + break; + case CONFIG_CATEGORIES.PRODUCT_ADDED.type: + case CONFIG_CATEGORIES.PRODUCT_ADDED.eventName: + category = CONFIG_CATEGORIES.PRODUCT_ADDED; + break; + case CONFIG_CATEGORIES.ORDER_COMPLETED.type: + case CONFIG_CATEGORIES.ORDER_COMPLETED.eventName: + category = CONFIG_CATEGORIES.ORDER_COMPLETED; + break; + case CONFIG_CATEGORIES.PRODUCTS_SEARCHED.type: + case CONFIG_CATEGORIES.PRODUCTS_SEARCHED.eventName: + category = CONFIG_CATEGORIES.PRODUCTS_SEARCHED; + break; + case CONFIG_CATEGORIES.CHECKOUT_STARTED.type: + case CONFIG_CATEGORIES.CHECKOUT_STARTED.eventName: + category = CONFIG_CATEGORIES.CHECKOUT_STARTED; + break; + case CONFIG_CATEGORIES.PAGE_VIEW.eventName: + category = CONFIG_CATEGORIES.PAGE_VIEW; + break; + default: + category = CONFIG_CATEGORIES.SIMPLE_TRACK; + break; + } + + if (OTHER_STANDARD_EVENTS.includes(eventName)) { + category = CONFIG_CATEGORIES.OTHER_STANDARD; + category.eventName = eventName; + } + + return category; }; const formingFinalResponse = ( @@ -516,9 +557,7 @@ module.exports = { transformedPayloadData, getActionSource, fetchUserData, - handleProduct, - handleSearch, - handleProductListViewed, - handleOrder, formingFinalResponse, + populateCustomDataBasedOnCategory, + getCategoryFromEvent, };