From 92ed3ff01a337ca2a5e7f7a4b92fd51f22d4cae3 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Wed, 3 Apr 2024 19:38:20 +0530 Subject: [PATCH 1/2] feat: adding product level tracking for awin --- src/v0/destinations/awin/transform.js | 14 +- src/v0/destinations/awin/utils.js | 47 +++ src/v0/destinations/awin/utils.test.js | 165 ++++++++++ test/integrations/destinations/awin/data.ts | 315 ++++++++++++++++++++ 4 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 src/v0/destinations/awin/utils.test.js diff --git a/src/v0/destinations/awin/transform.js b/src/v0/destinations/awin/transform.js index 49a115c1ff..0d7fd95c33 100644 --- a/src/v0/destinations/awin/transform.js +++ b/src/v0/destinations/awin/transform.js @@ -2,10 +2,12 @@ const { InstrumentationError, ConfigurationError } = require('@rudderstack/integ const { BASE_URL, ConfigCategory, mappingConfig } = require('./config'); const { defaultRequestConfig, constructPayload, simpleProcessRouterDest } = require('../../util'); -const { getParams } = require('./utils'); +const { getParams, trackProduct } = require('./utils'); const responseBuilder = (message, { Config }) => { const { advertiserId, eventsToTrack } = Config; + const { event, properties } = message; + let finalParams = {}; const payload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); @@ -17,8 +19,14 @@ const responseBuilder = (message, { Config }) => { }); // if the event is present in eventsList - if (eventsList.includes(message.event)) { + if (eventsList.includes(event)) { params = getParams(payload.params, advertiserId); + const productTrackObject = trackProduct(properties, advertiserId, params.parts); + + finalParams = { + ...params, + ...productTrackObject, + }; } else { throw new InstrumentationError( "Event is not present in 'Events to Track' list. Aborting message.", @@ -27,7 +35,7 @@ const responseBuilder = (message, { Config }) => { } } const response = defaultRequestConfig(); - response.params = params; + response.params = finalParams; response.endpoint = BASE_URL; return response; diff --git a/src/v0/destinations/awin/utils.js b/src/v0/destinations/awin/utils.js index 1616227c55..b9497cdd5d 100644 --- a/src/v0/destinations/awin/utils.js +++ b/src/v0/destinations/awin/utils.js @@ -1,3 +1,5 @@ +const lodash = require('lodash'); + /** * Returns final params * @param {*} params @@ -24,6 +26,51 @@ const getParams = (parameters, advertiserId) => { return params; }; +const areAllValuesDefined = (obj) => + lodash.every(lodash.values(obj), (value) => !lodash.isUndefined(value)); + +const buildProductPayloadString = (payload) => { + // URL-encode each value, and join back with the same key. + const encodedPayload = Object.entries(payload).reduce((acc, [key, value]) => { + // Encode each value. Assuming that all values are either strings or can be + // safely converted to strings. + acc[key] = encodeURIComponent(value); + return acc; + }, {}); + + return `AW:P|${encodedPayload.advertiserId}|${encodedPayload.orderReference}|${encodedPayload.productId}|${encodedPayload.productName}|${encodedPayload.productItemPrice}|${encodedPayload.productQuantity}|${encodedPayload.productSku}|${encodedPayload.commissionGroupCode}|${encodedPayload.productCategory}`; +}; + +// ref: https://wiki.awin.com/index.php/Advertiser_Tracking_Guide/Product_Level_Tracking#PLT_Via_Conversion_Pixel +const trackProduct = (properties, advertiserId, commissionParts) => { + const transformedProductInfoObj = {}; + if (properties?.products && properties.products.length > 0) { + const productsArray = properties.products; + let productIndex = 0; + productsArray.forEach((product) => { + const productPayloadNew = { + advertiserId, + orderReference: properties.order_id || properties.orderId, + productId: product.product_id || product.productId, + productName: product.name, + productItemPrice: product.price, + productQuantity: product.quantity, + productSku: product.sku || '', + commissionGroupCode: commissionParts || 'DEFAULT', + productCategory: product.category || '', + }; + if (areAllValuesDefined(productPayloadNew)) { + transformedProductInfoObj[`bd[${productIndex}]`] = + buildProductPayloadString(productPayloadNew); + productIndex += 1; + } + }); + } + return transformedProductInfoObj; +}; + module.exports = { getParams, + trackProduct, + buildProductPayloadString, }; diff --git a/src/v0/destinations/awin/utils.test.js b/src/v0/destinations/awin/utils.test.js new file mode 100644 index 0000000000..e60c07e96c --- /dev/null +++ b/src/v0/destinations/awin/utils.test.js @@ -0,0 +1,165 @@ +const { buildProductPayloadString, trackProduct } = require('./utils'); + +describe('buildProductPayloadString', () => { + // Should correctly build the payload string with all fields provided + it('should correctly build the payload string with all fields provided', () => { + const payload = { + advertiserId: '123', + orderReference: 'order123', + productId: 'prod123', + productName: 'Product 1', + productItemPrice: '10.99', + productQuantity: '2', + productSku: 'sku123', + commissionGroupCode: 'DEFAULT', + productCategory: 'Category 1', + }; + + const expected = 'AW:P|123|order123|prod123|Product%201|10.99|2|sku123|DEFAULT|Category%201'; + const result = buildProductPayloadString(payload); + + expect(result).toBe(expected); + }); + + // Should correctly handle extremely long string values for all fields + it('should correctly handle extremely long string values for all fields', () => { + const payload = { + advertiserId: '123', + orderReference: 'order123', + productId: 'prod123', + productName: 'Product 1'.repeat(100000), + productItemPrice: '10.99'.repeat(100000), + productQuantity: '2'.repeat(100000), + productSku: 'sku123'.repeat(100000), + commissionGroupCode: 'DEFAULT', + productCategory: 'Category 1'.repeat(100000), + }; + + const expected = `AW:P|123|order123|prod123|${encodeURIComponent('Product 1'.repeat(100000))}|${encodeURIComponent('10.99'.repeat(100000))}|${encodeURIComponent('2'.repeat(100000))}|${encodeURIComponent('sku123'.repeat(100000))}|DEFAULT|${encodeURIComponent('Category 1'.repeat(100000))}`; + const result = buildProductPayloadString(payload); + + expect(result).toBe(expected); + }); +}); + +describe('trackProduct', () => { + // Given a valid 'properties' object with a non-empty 'products' array, it should transform each product into a valid payload string and return an object with the transformed products. + it("should transform each product into a valid payload string and return an object with the transformed products when given a valid 'properties' object with a non-empty 'products' array", () => { + // Arrange + const properties = { + products: [ + { + product_id: '123', + name: 'Product 1', + price: 10, + quantity: 1, + sku: 'SKU123', + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: 'Category 2', + }, + ], + order_id: 'order123', + }; + const advertiserId = 'advertiser123'; + const commissionParts = 'COMMISSION'; + + // Act + const result = trackProduct(properties, advertiserId, commissionParts); + + // Assert + expect(result).toEqual({ + 'bd[0]': 'AW:P|advertiser123|order123|123|Product%201|10|1|SKU123|COMMISSION|Category%201', + 'bd[1]': 'AW:P|advertiser123|order123|456|Product%202|20|2|SKU456|COMMISSION|Category%202', + }); + }); + + // Given an invalid 'properties' object, it should return an empty object. + it("should return an empty object when given an invalid 'properties' object", () => { + // Arrange + const properties = {}; + const advertiserId = 'advertiser123'; + const commissionParts = 'COMMISSION'; + + // Act + const result = trackProduct(properties, advertiserId, commissionParts); + + // Assert + expect(result).toEqual({}); + }); + + it('should ignore the product which has missing properties', () => { + // Arrange + const properties = { + products: [ + { + price: 10, + quantity: 1, + sku: 'SKU123', + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: 'Category 2', + }, + ], + order_id: 'order123', + }; + const advertiserId = 'advertiser123'; + const commissionParts = 'COMMISSION'; + + // Act + const result = trackProduct(properties, advertiserId, commissionParts); + + // Assert + expect(result).toEqual({ + 'bd[0]': 'AW:P|advertiser123|order123|456|Product%202|20|2|SKU456|COMMISSION|Category%202', + }); + }); + + it('category and sku if undefined we put blank', () => { + // Arrange + const properties = { + products: [ + { + product_id: '123', + name: 'Product 1', + price: 10, + quantity: 1, + sku: undefined, + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: undefined, + }, + ], + order_id: 'order123', + }; + const advertiserId = 'advertiser123'; + const commissionParts = 'COMMISSION'; + + // Act + const result = trackProduct(properties, advertiserId, commissionParts); + + // Assert + expect(result).toEqual({ + 'bd[0]': 'AW:P|advertiser123|order123|123|Product%201|10|1||COMMISSION|Category%201', + 'bd[1]': 'AW:P|advertiser123|order123|456|Product%202|20|2|SKU456|COMMISSION|', + }); + }); +}); diff --git a/test/integrations/destinations/awin/data.ts b/test/integrations/destinations/awin/data.ts index 64c8fbc2a1..8ca294b5fb 100644 --- a/test/integrations/destinations/awin/data.ts +++ b/test/integrations/destinations/awin/data.ts @@ -843,4 +843,319 @@ export const data = [ }, }, }, + { + name: 'awin', + description: 'Track call- with product array', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + advertiserId: '1234', + eventsToTrack: [ + { + eventName: 'abc', + }, + { + eventName: 'prop2', + }, + { + eventName: 'prop3', + }, + ], + }, + }, + message: { + type: 'track', + event: 'prop2', + sentAt: '2022-01-20T13:39:21.033Z', + userId: 'user123456001', + channel: 'web', + properties: { + currency: 'INR', + voucherCode: '1bcu1', + amount: 500, + commissionGroup: 'sales', + cks: 'new', + testMode: '1', + order_id: 'QW123', + products: [ + { + product_id: '123', + name: 'Product 1', + price: 10, + quantity: 1, + sku: undefined, + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: undefined, + }, + ], + }, + context: { + os: { + name: '', + version: '', + }, + app: { + name: 'RudderLabs JavaScript SDK', + build: '1.0.0', + version: '1.2.20', + namespace: 'com.rudderlabs.javascript', + }, + page: { + url: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/ourSdk.html', + path: '/Testing/App_for_LaunchDarkly/ourSdk.html', + title: 'Document', + search: '', + tab_url: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/ourSdk.html', + referrer: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/', + initial_referrer: '$direct', + referring_domain: '127.0.0.1:7307', + initial_referring_domain: '', + }, + locale: 'en-US', + screen: { + width: 1440, + height: 900, + density: 2, + innerWidth: 536, + innerHeight: 689, + }, + traits: { + city: 'Pune', + name: 'First User', + email: 'firstUser@testmail.com', + title: 'VP', + gender: 'female', + avatar: 'https://i.pravatar.cc/300', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.2.20', + }, + campaign: {}, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36', + }, + rudderId: '553b5522-c575-40a7-8072-9741c5f9a647', + messageId: '831f1fa5-de84-4f22-880a-4c3f23fc3f04', + anonymousId: 'bf412108-0357-4330-b119-7305e767823c', + integrations: { + All: true, + }, + originalTimestamp: '2022-01-20T13:39:21.032Z', + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.awin1.com/sread.php', + headers: {}, + params: { + amount: 500, + ch: 'aw', + parts: 'sales:500', + cr: 'INR', + tt: 'ss', + tv: '2', + vc: '1bcu1', + cks: 'new', + merchant: '1234', + testmode: '1', + ref: 'QW123', + 'bd[0]': 'AW:P|1234|QW123|123|Product%201|10|1||sales%3A500|Category%201', + 'bd[1]': 'AW:P|1234|QW123|456|Product%202|20|2|SKU456|sales%3A500|', + }, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'awin', + description: 'Track call- with product array where important keys might be missing.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + advertiserId: '1234', + eventsToTrack: [ + { + eventName: 'abc', + }, + { + eventName: 'prop2', + }, + { + eventName: 'prop3', + }, + ], + }, + }, + message: { + type: 'track', + event: 'prop2', + sentAt: '2022-01-20T13:39:21.033Z', + userId: 'user123456001', + channel: 'web', + properties: { + currency: 'INR', + voucherCode: '1bcu1', + amount: 500, + commissionGroup: 'sales', + cks: 'new', + testMode: '1', + order_id: 'QW123', + products: [ + { + price: 10, + quantity: 1, + sku: undefined, + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: undefined, + }, + ], + }, + context: { + os: { + name: '', + version: '', + }, + app: { + name: 'RudderLabs JavaScript SDK', + build: '1.0.0', + version: '1.2.20', + namespace: 'com.rudderlabs.javascript', + }, + page: { + url: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/ourSdk.html', + path: '/Testing/App_for_LaunchDarkly/ourSdk.html', + title: 'Document', + search: '', + tab_url: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/ourSdk.html', + referrer: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/', + initial_referrer: '$direct', + referring_domain: '127.0.0.1:7307', + initial_referring_domain: '', + }, + locale: 'en-US', + screen: { + width: 1440, + height: 900, + density: 2, + innerWidth: 536, + innerHeight: 689, + }, + traits: { + city: 'Pune', + name: 'First User', + email: 'firstUser@testmail.com', + title: 'VP', + gender: 'female', + avatar: 'https://i.pravatar.cc/300', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.2.20', + }, + campaign: {}, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36', + }, + rudderId: '553b5522-c575-40a7-8072-9741c5f9a647', + messageId: '831f1fa5-de84-4f22-880a-4c3f23fc3f04', + anonymousId: 'bf412108-0357-4330-b119-7305e767823c', + integrations: { + All: true, + }, + originalTimestamp: '2022-01-20T13:39:21.032Z', + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.awin1.com/sread.php', + headers: {}, + params: { + amount: 500, + ch: 'aw', + parts: 'sales:500', + cr: 'INR', + tt: 'ss', + tv: '2', + vc: '1bcu1', + cks: 'new', + merchant: '1234', + testmode: '1', + ref: 'QW123', + 'bd[0]': 'AW:P|1234|QW123|456|Product%202|20|2|SKU456|sales%3A500|', + }, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, ]; From e8d191fc90f29e8d8165d5bb469e8a85847a93ef Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Mon, 8 Apr 2024 16:36:24 +0530 Subject: [PATCH 2/2] fix: address review comments --- src/v0/destinations/awin/utils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/v0/destinations/awin/utils.js b/src/v0/destinations/awin/utils.js index b9497cdd5d..be5f22474d 100644 --- a/src/v0/destinations/awin/utils.js +++ b/src/v0/destinations/awin/utils.js @@ -44,7 +44,11 @@ const buildProductPayloadString = (payload) => { // ref: https://wiki.awin.com/index.php/Advertiser_Tracking_Guide/Product_Level_Tracking#PLT_Via_Conversion_Pixel const trackProduct = (properties, advertiserId, commissionParts) => { const transformedProductInfoObj = {}; - if (properties?.products && properties.products.length > 0) { + if ( + properties?.products && + Array.isArray(properties?.products) && + properties.products.length > 0 + ) { const productsArray = properties.products; let productIndex = 0; productsArray.forEach((product) => {