diff --git a/src/cdk/v2/destinations/the_trade_desk/config.js b/src/cdk/v2/destinations/the_trade_desk/config.js index 828bab3714..300325223b 100644 --- a/src/cdk/v2/destinations/the_trade_desk/config.js +++ b/src/cdk/v2/destinations/the_trade_desk/config.js @@ -1,6 +1,4 @@ -const { getMappingConfig } = require('../../../../v0/util'); - -const SUPPORTED_EVENT_TYPE = ['record', 'track']; +const SUPPORTED_EVENT_TYPE = ['record']; const ACTION_TYPES = ['insert', 'delete']; const DATA_PROVIDER_ID = 'rudderstack'; @@ -15,66 +13,10 @@ const DATA_SERVERS_BASE_ENDPOINTS_MAP = { china: 'https://data-cn2.adsrvr.cn', }; -// ref:- https://partner.thetradedesk.com/v3/portal/data/doc/DataConversionEventsApi -const REAL_TIME_CONVERSION_ENDPOINT = 'https://insight.adsrvr.org/track/realtimeconversion'; - -const CONVERSION_SUPPORTED_ID_TYPES = [ - 'TDID', - 'IDFA', - 'AAID', - 'DAID', - 'NAID', - 'IDL', - 'EUID', - 'UID2', -]; - -const ECOMM_EVENT_MAP = { - 'product added': { - event: 'addtocart', - rootLevelPriceSupported: true, - }, - 'order completed': { - event: 'purchase', - itemsArray: true, - revenueFieldSupported: true, - }, - 'product viewed': { - event: 'viewitem', - rootLevelPriceSupported: true, - }, - 'checkout started': { - event: 'startcheckout', - itemsArray: true, - revenueFieldSupported: true, - }, - 'cart viewed': { - event: 'viewcart', - itemsArray: true, - }, - 'product added to wishlist': { - event: 'wishlistitem', - rootLevelPriceSupported: true, - }, -}; - -const CONFIG_CATEGORIES = { - COMMON_CONFIGS: { name: 'TTDCommonConfig' }, - ITEM_CONFIGS: { name: 'TTDItemConfig' }, -}; - -const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); - module.exports = { SUPPORTED_EVENT_TYPE, ACTION_TYPES, DATA_PROVIDER_ID, MAX_REQUEST_SIZE_IN_BYTES: 2500000, DATA_SERVERS_BASE_ENDPOINTS_MAP, - CONVERSION_SUPPORTED_ID_TYPES, - CONFIG_CATEGORIES, - COMMON_CONFIGS: MAPPING_CONFIG[CONFIG_CATEGORIES.COMMON_CONFIGS.name], - ITEM_CONFIGS: MAPPING_CONFIG[CONFIG_CATEGORIES.ITEM_CONFIGS.name], - ECOMM_EVENT_MAP, - REAL_TIME_CONVERSION_ENDPOINT, }; diff --git a/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml b/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml index ee05ecd967..5f0476dd62 100644 --- a/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml +++ b/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml @@ -3,20 +3,19 @@ bindings: path: ../../../../constants - name: processRecordInputs path: ./transformRecord - - name: processConversionInputs - path: ./transformConversion - name: handleRtTfSingleEventError path: ../../../../v0/util/index - name: InstrumentationError path: '@rudderstack/integrations-lib' steps: - - name: validateCommonConfig - description: | - validate common config for first party data and realtime conversion flow + - name: validateConfig template: | const config = ^[0].destination.Config + $.assertConfig(config.audienceId, "Segment name/Audience ID is not present. Aborting") $.assertConfig(config.advertiserId, "Advertiser ID is not present. Aborting") + $.assertConfig(config.advertiserSecretKey, "Advertiser Secret Key is not present. Aborting") + config.ttlInDays ? $.assertConfig(config.ttlInDays >=0 && config.ttlInDays <= 180, "TTL is out of range. Allowed values are 0 to 180 days") - name: validateInput template: | @@ -26,22 +25,17 @@ steps: template: | $.processRecordInputs(^.{.message.type === $.EventType.RECORD}[], ^[0].destination) - - name: processConversionEvents - template: | - $.processConversionInputs(^.{.message.type === $.EventType.TRACK}[]) - - name: failOtherEvents template: | - const otherEvents = ^.{.message.type !== $.EventType.TRACK && .message.type !== $.EventType.RECORD}[] + const otherEvents = ^.{.message.type !== $.EventType.RECORD}[] let failedEvents = otherEvents.map( function(event) { const error = new $.InstrumentationError("Event type " + event.message.type + " is not supported"); $.handleRtTfSingleEventError(event, error, {}) } ) - failedEvents ?? [] - name: finalPayload template: | - [...$.outputs.processRecordEvents, ...$.outputs.processConversionEvents, ...$.outputs.failOtherEvents] + [...$.outputs.processRecordEvents, ...$.outputs.failOtherEvents] diff --git a/src/cdk/v2/destinations/the_trade_desk/transformConversion.js b/src/cdk/v2/destinations/the_trade_desk/transformConversion.js deleted file mode 100644 index b282c43151..0000000000 --- a/src/cdk/v2/destinations/the_trade_desk/transformConversion.js +++ /dev/null @@ -1,98 +0,0 @@ -const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); -const { - defaultRequestConfig, - simpleProcessRouterDest, - defaultPostRequestConfig, - removeUndefinedAndNullValues, -} = require('../../../../v0/util'); -const { EventType } = require('../../../../constants'); -const { REAL_TIME_CONVERSION_ENDPOINT } = require('./config'); -const { - prepareFromConfig, - prepareCommonPayload, - getRevenue, - prepareItemsPayload, - getAdvertisingId, - prepareCustomProperties, - populateEventName, - getDataProcessingOptions, - getPrivacySetting, - enrichTrackPayload, -} = require('./utils'); - -const responseBuilder = (payload) => { - const response = defaultRequestConfig(); - response.endpoint = REAL_TIME_CONVERSION_ENDPOINT; - response.method = defaultPostRequestConfig.requestMethod; - response.body.JSON = payload; - return response; -}; - -const validateInputAndConfig = (message, destination) => { - const { Config } = destination; - if (!Config.trackerId) { - throw new ConfigurationError('Tracking Tag ID is not present. Aborting'); - } - - if (!message.type) { - throw new InstrumentationError('Event type is required'); - } - - const messageType = message.type.toLowerCase(); - if (messageType !== EventType.TRACK) { - throw new InstrumentationError(`Event type "${messageType}" is not supported`); - } - - if (!message.event) { - throw new InstrumentationError('Event name is not present. Aborting.'); - } -}; - -const prepareTrackPayload = (message, destination) => { - const configPayload = prepareFromConfig(destination); - const commonPayload = prepareCommonPayload(message); - // prepare items array - const items = prepareItemsPayload(message); - const { id, type } = getAdvertisingId(message); - // get td1-td10 custom properties - const customProperties = prepareCustomProperties(message, destination); - const eventName = populateEventName(message, destination); - const value = getRevenue(message); - let payload = { - ...configPayload, - ...commonPayload, - event_name: eventName, - value, - items, - adid: id, - adid_type: type, - ...customProperties, - data_processing_option: getDataProcessingOptions(message), - privacy_settings: getPrivacySetting(message), - }; - - payload = enrichTrackPayload(message, payload); - return { data: [removeUndefinedAndNullValues(payload)] }; -}; - -const trackResponseBuilder = (message, destination) => { - const payload = prepareTrackPayload(message, destination); - return responseBuilder(payload); -}; - -const processEvent = (message, destination) => { - validateInputAndConfig(message, destination); - return trackResponseBuilder(message, destination); -}; - -const process = (event) => processEvent(event.message, event.destination); - -const processConversionInputs = async (inputs, reqMetadata) => { - if (!inputs || inputs.length === 0) { - return []; - } - const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); - return respList; -}; - -module.exports = { processConversionInputs }; diff --git a/src/cdk/v2/destinations/the_trade_desk/transformRecord.js b/src/cdk/v2/destinations/the_trade_desk/transformRecord.js index d571e11b7a..b452f8d7bc 100644 --- a/src/cdk/v2/destinations/the_trade_desk/transformRecord.js +++ b/src/cdk/v2/destinations/the_trade_desk/transformRecord.js @@ -1,4 +1,4 @@ -const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); +const { InstrumentationError } = require('@rudderstack/integrations-lib'); const { BatchUtils } = require('@rudderstack/workflow-engine'); const { defaultPostRequestConfig, @@ -13,20 +13,6 @@ const tradeDeskConfig = require('./config'); const { DATA_PROVIDER_ID } = tradeDeskConfig; -const validateConfig = (config) => { - if (!config.advertiserSecretKey) { - throw new ConfigurationError('Advertiser Secret Key is not present. Aborting'); - } - - if (config.ttlInDays && !(config.ttlInDays >= 0 && config.ttlInDays <= 180)) { - throw new ConfigurationError('TTL is out of range. Allowed values are 0 to 180 days'); - } - - if (!config.audienceId) { - throw new ConfigurationError('Segment name/Audience ID is not present. Aborting'); - } -}; - const responseBuilder = (items, config) => { const { advertiserId, dataServer } = config; @@ -64,8 +50,6 @@ const processRecordInputs = (inputs, destination) => { return []; } - validateConfig(Config); - const invalidActionTypeError = new InstrumentationError( 'Invalid action type. You can only add or remove IDs from the audience/segment', ); diff --git a/src/cdk/v2/destinations/the_trade_desk/utils.js b/src/cdk/v2/destinations/the_trade_desk/utils.js index f51d8dc3ff..71b479438c 100644 --- a/src/cdk/v2/destinations/the_trade_desk/utils.js +++ b/src/cdk/v2/destinations/the_trade_desk/utils.js @@ -1,28 +1,10 @@ -const lodash = require('lodash'); -const get = require('get-value'); const CryptoJS = require('crypto-js'); -const { InstrumentationError, AbortedError } = require('@rudderstack/integrations-lib'); -const { - constructPayload, - getHashFromArray, - isDefinedAndNotNull, - isAppleFamily, - getIntegrationsObj, - extractCustomFields, - generateExclusionList, -} = require('../../../../v0/util'); -const { - DATA_SERVERS_BASE_ENDPOINTS_MAP, - CONVERSION_SUPPORTED_ID_TYPES, - COMMON_CONFIGS, - ITEM_CONFIGS, - ECOMM_EVENT_MAP, -} = require('./config'); +const { AbortedError } = require('@rudderstack/integrations-lib'); +const { DATA_SERVERS_BASE_ENDPOINTS_MAP } = require('./config'); const getTTLInMin = (ttl) => parseInt(ttl, 10) * 1440; const getBaseEndpoint = (dataServer) => DATA_SERVERS_BASE_ENDPOINTS_MAP[dataServer]; const getFirstPartyEndpoint = (dataServer) => `${getBaseEndpoint(dataServer)}/data/advertiser`; -const prepareCommonPayload = (message) => constructPayload(message, COMMON_CONFIGS); /** * Generates a signature header for a given request using a secret key. @@ -41,300 +23,8 @@ const getSignatureHeader = (request, secretKey) => { return base; }; -const prepareFromConfig = (destination) => ({ - tracker_id: destination.Config?.trackerId, - adv: destination.Config?.advertiserId, -}); - -/** - * Calculates the revenue based on the given message. - * - * @param {Object} message - The message object containing the event and properties. - * @returns {number} - The calculated revenue. - * @throws {InstrumentationError} - If the event is 'Order Completed' and revenue is not provided. - */ -const getRevenue = (message) => { - const { event, properties } = message; - let revenue = properties?.value; - const eventsMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()]; - if (eventsMapInfo?.rootLevelPriceSupported) { - const { price, quantity = 1 } = properties; - if (price && !Number.isNaN(parseFloat(price)) && !Number.isNaN(parseInt(quantity, 10))) { - revenue = parseFloat(price) * parseInt(quantity, 10); - } - } else if (eventsMapInfo?.revenueFieldSupported) { - revenue = properties?.revenue || revenue; - if (event.toLowerCase() === 'order completed' && !revenue) { - throw new InstrumentationError('value is required for `Order Completed` event'); - } - } - - return revenue; -}; - -/** - * Generates items from properties of a given message. - * - * @param {Object} message - The message object containing properties. - * @returns {Array} - An array of items generated from the properties. - */ -const prepareItemsFromProperties = (message) => { - const { properties } = message; - const items = []; - const item = constructPayload(properties, ITEM_CONFIGS); - items.push(item); - return items; -}; - -/** - * Generates items payload from products. - * - * @param {Object} message - The message object. - * @returns {Array} - The items payload. - */ -const prepareItemsFromProducts = (message) => { - const products = get(message, 'properties.products'); - const items = []; - products.forEach((product) => { - const item = constructPayload(product, ITEM_CONFIGS); - const itemExclusionList = generateExclusionList(ITEM_CONFIGS); - extractCustomFields(product, item, 'root', itemExclusionList); - items.push(item); - }); - return items; -}; - -/** - * Generates items payload from root properties or products. - * - * @param {Object} message - The message object containing event and properties. - * @returns {Array} - The array of items payload. - */ -const prepareItemsPayload = (message) => { - const { event } = message; - let items; - const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()]; - if (eventMapInfo?.itemsArray) { - // if event is one of the supported ecommerce events and products array is present - items = prepareItemsFromProducts(message); - } else if (eventMapInfo) { - // if event is one of the supported ecommerce events and products array is not present - items = prepareItemsFromProperties(message); - } - return items; -}; - -/** - * Retrieves the device advertising ID and type based on the provided message. - * - * @param {Object} message - The message object containing the context. - * @returns {Object} - An object containing the device advertising ID and type. - */ -const getDeviceAdvertisingId = (message) => { - const { context } = message; - const deviceId = context?.device?.advertisingId; - const osName = context?.os?.name?.toLowerCase(); - - let type; - switch (osName) { - case 'android': - type = 'AAID'; - break; - case 'windows': - type = 'NAID'; - break; - default: - type = isAppleFamily(osName) ? 'IDFA' : undefined; - break; - } - - return { deviceId, type }; -}; - -/** - * Retrieves the external ID object from the given message context. - * - * @param {Object} message - The message object containing the context. - * @returns {Object|undefined} - The external ID object, or undefined if not found. - */ -const getDestinationExternalIDObject = (message) => { - const { context } = message; - const externalIdArray = context?.externalId || []; - - let externalIdObj; - - if (Array.isArray(externalIdArray)) { - externalIdObj = externalIdArray.find( - (extIdObj) => - CONVERSION_SUPPORTED_ID_TYPES.includes(extIdObj?.type?.toUpperCase()) && extIdObj?.id, - ); - } - return externalIdObj; -}; - -/** - * Retrieves the advertising ID and type from the given message. - * - * @param {Object} message - The message object containing the context. - * @returns {Object} - An object containing the advertising ID and type. - * If the advertising ID and type are found in the device context, they are returned. - * If not, the external ID object is checked and if found, its ID and type are returned. - * If neither the device context nor the external ID object contain the required information, - * an object with null values for ID and type is returned. - */ -const getAdvertisingId = (message) => { - const { deviceId, type } = getDeviceAdvertisingId(message); - if (deviceId && type) { - return { id: deviceId, type }; - } - const externalIdObj = getDestinationExternalIDObject(message); - if (externalIdObj?.id && externalIdObj?.type) { - return { id: externalIdObj.id, type: externalIdObj.type.toUpperCase() }; - } - - return { id: null, type: null }; -}; - -/** - * Prepares custom properties (td1-td10) for a given message and destination. - * - * @param {object} message - The message object. - * @param {object} destination - The destination object. - * @returns {object} - The prepared payload object. - */ -const prepareCustomProperties = (message, destination) => { - const { customProperties } = destination.Config; - const payload = {}; - if (customProperties) { - customProperties.forEach((customProperty) => { - const { rudderProperty, tradeDeskProperty } = customProperty; - const value = get(message, rudderProperty); - if (value) { - payload[tradeDeskProperty] = value; - // unset the rudder property from the message, since it is already mapped to a trade desk property - lodash.unset(message, rudderProperty); - } - }); - } - return payload; -}; - -/** - * Retrieves the event name based on the provided message and destination. - * - * @param {object} message - The message object containing the event. - * @param {object} destination - The destination object containing the events mapping configuration. - * @returns {string} - The event name. - */ -const populateEventName = (message, destination) => { - let eventName; - const { event } = message; - const { eventsMapping } = destination.Config; - - // if event is mapped on dashboard, use the mapped event name - if (Array.isArray(eventsMapping) && eventsMapping.length > 0) { - const keyMap = getHashFromArray(eventsMapping, 'from', 'to'); - eventName = keyMap[event.toLowerCase()]; - } - - if (eventName) { - return eventName; - } - - // if event is one of the supported ecommerce events, use the mapped trade desk event name - const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()]; - if (isDefinedAndNotNull(eventMapInfo)) { - return eventMapInfo.event; - } - - // else return the event name as it is - return event; -}; - -/** - * Retrieves the data processing options based on the provided message. - * - * @param {string} message - The message to process. - * @throws {InstrumentationError} - Throws an error if the region is not supported, if no policies are provided, if multiple policies are provided, or if the policy is not supported. - * @returns {Object} - The data processing options, including the policies and region. - */ -const getDataProcessingOptions = (message) => { - const integrationObj = getIntegrationsObj(message, 'THE_TRADE_DESK') || {}; - let { policies } = integrationObj; - const { region } = integrationObj; - let dataProcessingOptions; - - if (region && !region.toLowerCase().startsWith('us')) { - throw new InstrumentationError('Only US states are supported'); - } - - if (!policies || (Array.isArray(policies) && policies.length === 0)) { - policies = ['LDU']; - } - - if (policies.length > 1) { - throw new InstrumentationError('Only one policy is allowed'); - } - - if (policies[0] !== 'LDU') { - throw new InstrumentationError('Only LDU policy is supported'); - } - - if (policies && region) { - dataProcessingOptions = { policies, region }; - } - - return dataProcessingOptions; -}; - -const getPrivacySetting = (message) => { - const integrationObj = getIntegrationsObj(message, 'THE_TRADE_DESK'); - return integrationObj?.privacy_settings; -}; - -/** - * Enriches the track payload with extra properties present in 'properties' other than the ones defined in TTDCommonConfig.json and TTDItemConfig.json - * - * @param {Object} message - The message object containing the event information. - * @param {Object} payload - The payload object to be enriched. - * @returns {Object} - The enriched payload object. - */ -const enrichTrackPayload = (message, payload) => { - let rawPayload = { ...payload }; - const eventsMapInfo = ECOMM_EVENT_MAP[message.event.toLowerCase()]; - // checking if event is an ecomm one and itemsArray/products support is not present. e.g Product Added event - if (eventsMapInfo && !eventsMapInfo.itemsArray) { - const itemExclusionList = generateExclusionList(ITEM_CONFIGS); - rawPayload = extractCustomFields(message, rawPayload, ['properties'], itemExclusionList); - } else if (eventsMapInfo) { - // for ecomm events with products array supports. e.g Order Completed event - rawPayload = extractCustomFields( - message, - rawPayload, - ['properties'], - ['products', 'revenue', 'value'], - ); - } else { - // for custom events - rawPayload = extractCustomFields(message, rawPayload, ['properties'], ['value']); - } - return rawPayload; -}; - module.exports = { getTTLInMin, getFirstPartyEndpoint, getSignatureHeader, - prepareFromConfig, - getRevenue, - prepareCommonPayload, - prepareItemsPayload, - getDeviceAdvertisingId, - getDestinationExternalIDObject, - getAdvertisingId, - prepareCustomProperties, - populateEventName, - getDataProcessingOptions, - getPrivacySetting, - enrichTrackPayload, }; diff --git a/src/cdk/v2/destinations/the_trade_desk/utils.test.js b/src/cdk/v2/destinations/the_trade_desk/utils.test.js index 029c3004ae..81fd7cf17d 100644 --- a/src/cdk/v2/destinations/the_trade_desk/utils.test.js +++ b/src/cdk/v2/destinations/the_trade_desk/utils.test.js @@ -1,16 +1,5 @@ -const { AbortedError, InstrumentationError } = require('@rudderstack/integrations-lib'); -const { - getSignatureHeader, - getRevenue, - getDeviceAdvertisingId, - getDestinationExternalIDObject, - getAdvertisingId, - prepareCustomProperties, - populateEventName, - getDataProcessingOptions, - getPrivacySetting, - enrichTrackPayload, -} = require('./utils'); +const { AbortedError } = require('@rudderstack/integrations-lib'); +const { getSignatureHeader } = require('./utils'); describe('getSignatureHeader', () => { it('should calculate the signature header for a valid request and secret key', () => { @@ -58,612 +47,3 @@ describe('getSignatureHeader', () => { }).toThrow(AbortedError); }); }); - -describe('getRevenue', () => { - it('should return revenue value from message properties for custom events', () => { - const message = { - event: 'customEvent', - properties: { - value: 100, - }, - }; - const result = getRevenue(message); - expect(result).toBe(100); - }); - - it('should calculate revenue based on price and quantity from message properties if ecomm event is supported for price calculation', () => { - const message = { - event: 'Product Added', - properties: { - price: 10, - quantity: 5, - }, - }; - const result = getRevenue(message); - expect(result).toBe(50); - }); - - it('should return revenue value from message properties if ecomm event is supported for revenue calculation', () => { - const message = { - event: 'Order Completed', - properties: { - revenue: 200, - }, - }; - const result = getRevenue(message); - expect(result).toBe(200); - }); - - it('should return default revenue value from properties.value for ecomm events', () => { - let message = { - event: 'Product Added', - properties: { - price: '', - value: 200, - }, - }; - let result = getRevenue(message); - expect(result).toBe(200); - - message = { - event: 'Order Completed', - properties: { - value: 200, - }, - }; - result = getRevenue(message); - expect(result).toBe(200); - }); - - it('should throw an Instrumentation error if revenue is missing for `Order Completed` event', () => { - const message = { - event: 'Order Completed', - properties: {}, - }; - expect(() => { - getRevenue(message); - }).toThrow(InstrumentationError); - }); -}); - -describe('getDeviceAdvertisingId', () => { - it('should return an object with deviceId and type properties when context.device.advertisingId and context.os.name are present', () => { - let message = { - context: { - device: { - advertisingId: '123456789', - }, - os: { - name: 'android', - }, - }, - }; - let result = getDeviceAdvertisingId(message); - expect(result).toEqual({ deviceId: '123456789', type: 'AAID' }); - - message = { - context: { - device: { - advertisingId: '123456789', - }, - os: { - name: 'ios', - }, - }, - }; - result = getDeviceAdvertisingId(message); - expect(result).toEqual({ deviceId: '123456789', type: 'IDFA' }); - - message = { - context: { - device: { - advertisingId: '123456789', - }, - os: { - name: 'windows', - }, - }, - }; - result = getDeviceAdvertisingId(message); - expect(result).toEqual({ deviceId: '123456789', type: 'NAID' }); - }); - - it('should return an object with undefined type property when osName is not "android", "windows", or an Apple OS', () => { - const message = { - context: { - device: { - advertisingId: '123456789', - }, - os: { - name: 'linux', - }, - }, - }; - const result = getDeviceAdvertisingId(message); - expect(result).toEqual({ deviceId: '123456789', type: undefined }); - }); - - it('should return an object with undefined deviceId and type properties when context is undefined', () => { - let message = {}; - let result = getDeviceAdvertisingId(message); - expect(result).toEqual({ deviceId: undefined, type: undefined }); - - message = { - context: {}, - }; - result = getDeviceAdvertisingId(message); - expect(result).toEqual({ deviceId: undefined, type: undefined }); - - message = { - context: { - device: {}, - }, - }; - result = getDeviceAdvertisingId(message); - expect(result).toEqual({ deviceId: undefined, type: undefined }); - }); -}); - -describe('getDestinationExternalIDObject', () => { - it('should return the external ID object when it exists in the message context', () => { - const message = { - context: { - externalId: [ - { id: '123', type: 'daid' }, - { id: '456', type: 'type123' }, - ], - }, - }; - const result = getDestinationExternalIDObject(message); - expect(result).toEqual({ id: '123', type: 'daid' }); - }); - - it('should return undefined when no external ID object exists in the message context', () => { - let message = { - context: { - externalId: [], - }, - }; - let result = getDestinationExternalIDObject(message); - expect(result).toBeUndefined(); - - message = { - context: {}, - }; - result = getDestinationExternalIDObject(message); - expect(result).toBeUndefined(); - }); - - it('should return the first matching external ID object in the array', () => { - const message = { - context: { - externalId: [ - { id: '', type: 'daid' }, - { id: '456', type: 'tdid' }, - { id: '789', type: 'UID2' }, - ], - }, - }; - const result = getDestinationExternalIDObject(message); - expect(result).toEqual({ id: '456', type: 'tdid' }); - }); -}); - -describe('getAdvertisingId', () => { - it('should return an object with the ID and type when the message contains a valid device advertising ID and OS type', () => { - const message = { - context: { - device: { - advertisingId: '1234567890', - }, - os: { - name: 'android', - }, - }, - }; - - const result = getAdvertisingId(message); - expect(result).toEqual({ id: '1234567890', type: 'AAID' }); - }); - - it('should return an object with the ID and type when the message contains a valid external ID object with a supported type', () => { - const message = { - context: { - externalId: [ - { - type: 'IDFA', - id: 'abcdefg', - }, - ], - }, - }; - - const result = getAdvertisingId(message); - expect(result).toEqual({ id: 'abcdefg', type: 'IDFA' }); - }); - - it('should return an object with undefined ID and type when the message contains a valid external ID object with an unsupported type', () => { - let message = { - context: { - externalId: [ - { - type: 'unsupported', - id: '1234567890', - }, - ], - }, - }; - - let result = getAdvertisingId(message); - expect(result).toEqual({ id: null, type: null }); - - message = { - context: { - device: { - advertisingId: '1234567890', - }, - }, - }; - - result = getAdvertisingId(message); - expect(result).toEqual({ id: null, type: null }); - }); - - it('should return an object with undefined ID and type when the message contains an external ID object with a supported type but no ID or missing externalId', () => { - let message = { - context: { - externalId: [ - { - type: 'IDFA', - }, - ], - }, - }; - let result = getAdvertisingId(message); - expect(result).toEqual({ id: null, type: null }); - - message = { - context: {}, - }; - result = getAdvertisingId(message); - expect(result).toEqual({ id: null, type: null }); - }); -}); - -describe('prepareCustomProperties', () => { - it('should return an empty object when customProperties is an empty array', () => { - const message = {}; - let destination = { Config: { customProperties: [] } }; - let result = prepareCustomProperties(message, destination); - expect(result).toEqual({}); - - destination = { Config: { customProperties: [{ rudderProperty: '', tradeDeskProperty: '' }] } }; - result = prepareCustomProperties(message, destination); - expect(result).toEqual({}); - - destination = { Config: { customProperties: undefined } }; - result = prepareCustomProperties(message, destination); - expect(result).toEqual({}); - }); - - it('should return an object with `tradeDeskProperty` as key and `rudderProperty` value as value when `rudderProperty` exists in message', () => { - const message = { - rudderProperty1: 'value1', - rudderProperty2: 'value2', - }; - const destination = { - Config: { - customProperties: [ - { - rudderProperty: 'rudderProperty1', - tradeDeskProperty: 'tradeDeskProperty1', - }, - { - rudderProperty: 'rudderProperty2', - tradeDeskProperty: 'tradeDeskProperty2', - }, - { - rudderProperty: 'rudderProperty3', - tradeDeskProperty: 'tradeDeskProperty3', - }, - ], - }, - }; - const result = prepareCustomProperties(message, destination); - expect(result).toEqual({ - tradeDeskProperty1: 'value1', - tradeDeskProperty2: 'value2', - }); - }); -}); - -describe('populateEventName', () => { - it('should return the eventName if it exists in the eventsMapping of destination.Config', () => { - const message = { event: 'someEvent' }; - const destination = { Config: { eventsMapping: [{ from: 'someEvent', to: 'mappedEvent' }] } }; - const result = populateEventName(message, destination); - expect(result).toBe('mappedEvent'); - }); - - it('should return the eventName if it exists in the ECOMM_EVENT_MAP', () => { - const message = { event: 'product added' }; - let destination = { Config: { eventsMapping: [{ from: 'someEvent', to: 'mappedEvent' }] } }; - let result = populateEventName(message, destination); - expect(result).toBe('addtocart'); - - destination = { Config: { eventsMapping: [] } }; - result = populateEventName(message, destination); - expect(result).toBe('addtocart'); - }); - - it('should return undefined if eventsMapping is an empty array', () => { - const message = { event: 'someEvent' }; - const destination = { Config: { eventsMapping: [] } }; - const result = populateEventName(message, destination); - expect(result).toBe('someEvent'); - }); -}); - -describe('getDataProcessingOptions', () => { - it('should return an object with policies and region when provided a integrationObj in message', () => { - const message = { - integrations: { - All: true, - THE_TRADE_DESK: { - policies: ['LDU'], - region: 'US-CO', - }, - }, - }; - const expected = { - policies: ['LDU'], - region: 'US-CO', - }; - const result = getDataProcessingOptions(message); - expect(result).toEqual(expected); - }); - - it('should throw an InstrumentationError if the region is not a US state', () => { - const message = { - integrations: { - All: true, - THE_TRADE_DESK: { - policies: ['LDU'], - region: 'EU-abc', - }, - }, - }; - expect(() => { - getDataProcessingOptions(message); - }).toThrow(InstrumentationError); - }); - - it('should throw an InstrumentationError if multiple policies are provided', () => { - const message = { - integrations: { - All: true, - THE_TRADE_DESK: { - policies: ['LDU', 'Policy2'], - region: 'US-CO', - }, - }, - }; - - expect(() => { - getDataProcessingOptions(message); - }).toThrow(InstrumentationError); - }); - - it('should throw an InstrumentationError if a policy other than `LDU` is provided', () => { - const message = { - integrations: { - All: true, - THE_TRADE_DESK: { - policies: ['Policy1'], - region: 'US-CO', - }, - }, - }; - - expect(() => { - getDataProcessingOptions(message); - }).toThrow(InstrumentationError); - }); - - it('should return an object with default policy `LDU` when policies are not provided', () => { - const message = { - integrations: { - All: true, - THE_TRADE_DESK: { - policies: [], - region: 'US-CO', - }, - }, - }; - - const expected = { - policies: ['LDU'], - region: 'US-CO', - }; - - expect(getDataProcessingOptions(message)).toEqual(expected); - }); - - it('should handle empty cases', () => { - let message = { - integrations: { - All: true, - THE_TRADE_DESK: {}, - }, - }; - - expect(getDataProcessingOptions(message)).toBeUndefined(); - - message = { - integrations: { - All: true, - }, - }; - - expect(getDataProcessingOptions(message)).toBeUndefined(); - - message = { - integrations: { - All: true, - THE_TRADE_DESK: { region: 'US-CO' }, - }, - }; - - expect(getDataProcessingOptions(message)).toEqual({ policies: ['LDU'], region: 'US-CO' }); - }); -}); - -describe('getPrivacySetting', () => { - it('should return the privacy settings object when it exists in the integration object', () => { - const message = { - integrations: { - All: true, - THE_TRADE_DESK: { - privacy_settings: [ - { - privacy_type: 'GDPR', - is_applicable: 1, - consent_string: 'ok', - }, - ], - }, - }, - }; - const expected = [ - { - privacy_type: 'GDPR', - is_applicable: 1, - consent_string: 'ok', - }, - ]; - const result = getPrivacySetting(message); - expect(result).toEqual(expected); - }); - - it('should return null when the privacy settings object does not exist in the integration object', () => { - let message = { integrations: {} }; - expect(getPrivacySetting(message)).toBeUndefined(); - - message = { integrations: { THE_TRADE_DESK: {} } }; - expect(getPrivacySetting(message)).toBeUndefined(); - - message = { integrations: { THE_TRADE_DESK: { privacy_settings: null } } }; - expect(getPrivacySetting(message)).toBeNull(); - }); -}); - -describe('enrichTrackPayload', () => { - it('should correctly enrich the payload with custom fields for ecomm events where product array is not supported', () => { - const message = { - event: 'Product Added', - properties: { - product_id: 'prd123', - sku: 'sku123', - brand: 'brand123', - property1: 'value1', - property2: 'value2', - }, - }; - const payload = { - items: [ - { - item_code: 'prd123', - }, - ], - property1: 'value1', - property2: 'value2', - }; - const expectedPayload = { - items: [ - { - item_code: 'prd123', - }, - ], - brand: 'brand123', - property1: 'value1', - property2: 'value2', - }; - - const result = enrichTrackPayload(message, payload); - expect(result).toEqual(expectedPayload); - }); - - it('should correctly enrich the payload with custom fields when the for ecomm events with products array support', () => { - const message = { - event: 'order completed', - properties: { - order_id: 'ord123', - total: 52.0, - subtotal: 45.0, - revenue: 50.0, - products: [{ product_id: 'prd123', sku: 'sku123', brand: 'brand123' }], - property1: 'value1', - property2: 'value2', - }, - }; - const payload = { - order_id: 'ord123', - value: 50.0, - items: [{ item_code: 'prd123', brand: 'brand123' }], - property1: 'value1', - property2: 'value2', - }; - const expectedPayload = { - order_id: 'ord123', - total: 52.0, - subtotal: 45.0, - value: 50.0, - items: [{ item_code: 'prd123', brand: 'brand123' }], - property1: 'value1', - property2: 'value2', - }; - - const result = enrichTrackPayload(message, payload); - - expect(result).toEqual(expectedPayload); - }); - - it('should return the enriched payload for custom event', () => { - const message = { - event: 'someEvent', - properties: { - order_id: 'ord123', - property1: 'value1', - property2: 'value2', - revenue: 10, - value: 11, - products: [ - { - product_id: 'prd123', - test: 'test', - }, - ], - }, - }; - const payload = { - order_id: 'ord123', - value: 11, - }; - const expectedPayload = { - order_id: 'ord123', - property1: 'value1', - property2: 'value2', - revenue: 10, - value: 11, - products: [ - { - product_id: 'prd123', - test: 'test', - }, - ], - }; - - const result = enrichTrackPayload(message, payload); - expect(result).toEqual(expectedPayload); - }); -}); diff --git a/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/config.js b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/config.js new file mode 100644 index 0000000000..7af732185f --- /dev/null +++ b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/config.js @@ -0,0 +1,63 @@ +const { getMappingConfig } = require('../../../../v0/util'); + +const SUPPORTED_EVENT_TYPE = ['track']; + +// ref:- https://partner.thetradedesk.com/v3/portal/data/doc/DataConversionEventsApi +const REAL_TIME_CONVERSION_ENDPOINT = 'https://insight.adsrvr.org/track/realtimeconversion'; + +const CONVERSION_SUPPORTED_ID_TYPES = [ + 'TDID', + 'IDFA', + 'AAID', + 'DAID', + 'NAID', + 'IDL', + 'EUID', + 'UID2', +]; + +const ECOMM_EVENT_MAP = { + 'product added': { + event: 'addtocart', + rootLevelPriceSupported: true, + }, + 'order completed': { + event: 'purchase', + itemsArray: true, + revenueFieldSupported: true, + }, + 'product viewed': { + event: 'viewitem', + rootLevelPriceSupported: true, + }, + 'checkout started': { + event: 'startcheckout', + itemsArray: true, + revenueFieldSupported: true, + }, + 'cart viewed': { + event: 'viewcart', + itemsArray: true, + }, + 'product added to wishlist': { + event: 'wishlistitem', + rootLevelPriceSupported: true, + }, +}; + +const CONFIG_CATEGORIES = { + COMMON_CONFIGS: { name: 'TTDCommonConfig' }, + ITEM_CONFIGS: { name: 'TTDItemConfig' }, +}; + +const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); + +module.exports = { + SUPPORTED_EVENT_TYPE, + CONFIG_CATEGORIES, + CONVERSION_SUPPORTED_ID_TYPES, + COMMON_CONFIGS: MAPPING_CONFIG[CONFIG_CATEGORIES.COMMON_CONFIGS.name], + ITEM_CONFIGS: MAPPING_CONFIG[CONFIG_CATEGORIES.ITEM_CONFIGS.name], + ECOMM_EVENT_MAP, + REAL_TIME_CONVERSION_ENDPOINT, +}; diff --git a/src/cdk/v2/destinations/the_trade_desk/data/TTDCommonConfig.json b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/data/TTDCommonConfig.json similarity index 100% rename from src/cdk/v2/destinations/the_trade_desk/data/TTDCommonConfig.json rename to src/cdk/v2/destinations/the_trade_desk_real_time_conversions/data/TTDCommonConfig.json diff --git a/src/cdk/v2/destinations/the_trade_desk/data/TTDItemConfig.json b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/data/TTDItemConfig.json similarity index 100% rename from src/cdk/v2/destinations/the_trade_desk/data/TTDItemConfig.json rename to src/cdk/v2/destinations/the_trade_desk_real_time_conversions/data/TTDItemConfig.json diff --git a/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/procWorkflow.yaml b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/procWorkflow.yaml new file mode 100644 index 0000000000..5191320cdc --- /dev/null +++ b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/procWorkflow.yaml @@ -0,0 +1,59 @@ +bindings: + - name: EventType + path: ../../../../constants + - path: ../../bindings/jsontemplate + - name: defaultRequestConfig + path: ../../../../v0/util + - name: removeUndefinedAndNullValues + path: ../../../../v0/util + - path: ./config + exportAll: true + - path: ./utils + exportAll: true + +steps: + - name: validateConfig + template: | + $.assertConfig(.destination.Config.advertiserId, "Advertiser ID is not present. Aborting") + $.assertConfig(.destination.Config.trackerId, "Tracking Tag ID is not present. Aborting") + + - name: validateInput + template: | + let messageType = .message.type; + $.assert(messageType, "message Type is not present. Aborting."); + $.assert(messageType.toLowerCase() === $.EventType.TRACK, "Event type " + messageType + " is not supported"); + $.assert(.message.event, "Event is not present. Aborting."); + + - name: prepareTrackPayload + template: | + const configPayload = $.prepareFromConfig(.destination); + const commonPayload = $.prepareCommonPayload(.message); + const { id, type } = $.getAdvertisingId(.message); + const items = $.prepareItemsPayload(.message); + const customProperties = $.prepareCustomProperties(.message, .destination); + const eventName = $.populateEventName(.message, .destination); + const value = $.getRevenue(.message); + let payload = { + ...configPayload, + ...commonPayload, + event_name: eventName, + value, + items, + adid: id, + adid_type: type, + ...customProperties, + data_processing_option: $.getDataProcessingOptions(.message), + privacy_settings: $.getPrivacySetting(.message), + }; + payload = $.enrichTrackPayload(.message, payload); + payload; + + - name: buildResponseForProcessTransformation + template: | + const response = $.defaultRequestConfig(); + response.body.JSON = {data: [$.removeUndefinedAndNullValues($.outputs.prepareTrackPayload)]}; + response.endpoint = $.REAL_TIME_CONVERSION_ENDPOINT; + response.headers = { + "Content-Type": "application/json" + }; + response; diff --git a/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.js b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.js new file mode 100644 index 0000000000..2232be61f0 --- /dev/null +++ b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.js @@ -0,0 +1,315 @@ +const lodash = require('lodash'); +const get = require('get-value'); +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { + constructPayload, + getHashFromArray, + isDefinedAndNotNull, + isAppleFamily, + getIntegrationsObj, + extractCustomFields, + generateExclusionList, +} = require('../../../../v0/util'); +const { + CONVERSION_SUPPORTED_ID_TYPES, + COMMON_CONFIGS, + ITEM_CONFIGS, + ECOMM_EVENT_MAP, +} = require('./config'); + +const prepareCommonPayload = (message) => constructPayload(message, COMMON_CONFIGS); + +const prepareFromConfig = (destination) => ({ + tracker_id: destination.Config?.trackerId, + adv: destination.Config?.advertiserId, +}); + +/** + * Calculates the revenue based on the given message. + * + * @param {Object} message - The message object containing the event and properties. + * @returns {number} - The calculated revenue. + * @throws {InstrumentationError} - If the event is 'Order Completed' and revenue is not provided. + */ +const getRevenue = (message) => { + const { event, properties } = message; + let revenue = properties?.value; + const eventsMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()]; + if (eventsMapInfo?.rootLevelPriceSupported) { + const { price, quantity = 1 } = properties; + if (price && !Number.isNaN(parseFloat(price)) && !Number.isNaN(parseInt(quantity, 10))) { + revenue = parseFloat(price) * parseInt(quantity, 10); + } + } else if (eventsMapInfo?.revenueFieldSupported) { + revenue = properties?.revenue || revenue; + if (event.toLowerCase() === 'order completed' && !revenue) { + throw new InstrumentationError('value is required for `Order Completed` event'); + } + } + + return revenue; +}; + +/** + * Generates items from properties of a given message. + * + * @param {Object} message - The message object containing properties. + * @returns {Array} - An array of items generated from the properties. + */ +const prepareItemsFromProperties = (message) => { + const { properties } = message; + const items = []; + const item = constructPayload(properties, ITEM_CONFIGS); + items.push(item); + return items; +}; + +/** + * Generates items payload from products. + * + * @param {Object} message - The message object. + * @returns {Array} - The items payload. + */ +const prepareItemsFromProducts = (message) => { + const products = get(message, 'properties.products'); + const items = []; + products.forEach((product) => { + const item = constructPayload(product, ITEM_CONFIGS); + const itemExclusionList = generateExclusionList(ITEM_CONFIGS); + extractCustomFields(product, item, 'root', itemExclusionList); + items.push(item); + }); + return items; +}; + +/** + * Generates items payload from root properties or products. + * + * @param {Object} message - The message object containing event and properties. + * @returns {Array} - The array of items payload. + */ +const prepareItemsPayload = (message) => { + const { event } = message; + let items; + const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()]; + if (eventMapInfo?.itemsArray) { + // if event is one of the supported ecommerce events and products array is present + items = prepareItemsFromProducts(message); + } else if (eventMapInfo) { + // if event is one of the supported ecommerce events and products array is not present + items = prepareItemsFromProperties(message); + } + return items; +}; + +/** + * Retrieves the device advertising ID and type based on the provided message. + * + * @param {Object} message - The message object containing the context. + * @returns {Object} - An object containing the device advertising ID and type. + */ +const getDeviceAdvertisingId = (message) => { + const { context } = message; + const deviceId = context?.device?.advertisingId; + const osName = context?.os?.name?.toLowerCase(); + + let type; + switch (osName) { + case 'android': + type = 'AAID'; + break; + case 'windows': + type = 'NAID'; + break; + default: + type = isAppleFamily(osName) ? 'IDFA' : undefined; + break; + } + + return { deviceId, type }; +}; + +/** + * Retrieves the external ID object from the given message context. + * + * @param {Object} message - The message object containing the context. + * @returns {Object|undefined} - The external ID object, or undefined if not found. + */ +const getDestinationExternalIDObject = (message) => { + const { context } = message; + const externalIdArray = context?.externalId || []; + + let externalIdObj; + + if (Array.isArray(externalIdArray)) { + externalIdObj = externalIdArray.find( + (extIdObj) => + CONVERSION_SUPPORTED_ID_TYPES.includes(extIdObj?.type?.toUpperCase()) && extIdObj?.id, + ); + } + return externalIdObj; +}; + +/** + * Retrieves the advertising ID and type from the given message. + * + * @param {Object} message - The message object containing the context. + * @returns {Object} - An object containing the advertising ID and type. + * If the advertising ID and type are found in the device context, they are returned. + * If not, the external ID object is checked and if found, its ID and type are returned. + * If neither the device context nor the external ID object contain the required information, + * an object with null values for ID and type is returned. + */ +const getAdvertisingId = (message) => { + const { deviceId, type } = getDeviceAdvertisingId(message); + if (deviceId && type) { + return { id: deviceId, type }; + } + const externalIdObj = getDestinationExternalIDObject(message); + if (externalIdObj?.id && externalIdObj?.type) { + return { id: externalIdObj.id, type: externalIdObj.type.toUpperCase() }; + } + + return { id: null, type: null }; +}; + +/** + * Prepares custom properties (td1-td10) for a given message and destination. + * + * @param {object} message - The message object. + * @param {object} destination - The destination object. + * @returns {object} - The prepared payload object. + */ +const prepareCustomProperties = (message, destination) => { + const { customProperties } = destination.Config; + const payload = {}; + if (customProperties) { + customProperties.forEach((customProperty) => { + const { rudderProperty, tradeDeskProperty } = customProperty; + const value = get(message, rudderProperty); + if (value) { + payload[tradeDeskProperty] = value; + // unset the rudder property from the message, since it is already mapped to a trade desk property + lodash.unset(message, rudderProperty); + } + }); + } + return payload; +}; + +/** + * Retrieves the event name based on the provided message and destination. + * + * @param {object} message - The message object containing the event. + * @param {object} destination - The destination object containing the events mapping configuration. + * @returns {string} - The event name. + */ +const populateEventName = (message, destination) => { + let eventName; + const { event } = message; + const { eventsMapping } = destination.Config; + + // if event is mapped on dashboard, use the mapped event name + if (Array.isArray(eventsMapping) && eventsMapping.length > 0) { + const keyMap = getHashFromArray(eventsMapping, 'from', 'to'); + eventName = keyMap[event.toLowerCase()]; + } + + if (eventName) { + return eventName; + } + + // if event is one of the supported ecommerce events, use the mapped trade desk event name + const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()]; + if (isDefinedAndNotNull(eventMapInfo)) { + return eventMapInfo.event; + } + + // else return the event name as it is + return event; +}; + +/** + * Retrieves the data processing options based on the provided message. + * + * @param {string} message - The message to process. + * @throws {InstrumentationError} - Throws an error if the region is not supported, if no policies are provided, if multiple policies are provided, or if the policy is not supported. + * @returns {Object} - The data processing options, including the policies and region. + */ +const getDataProcessingOptions = (message) => { + const integrationObj = getIntegrationsObj(message, 'THE_TRADE_DESK') || {}; + let { policies } = integrationObj; + const { region } = integrationObj; + let dataProcessingOptions; + + if (region && !region.toLowerCase().startsWith('us')) { + throw new InstrumentationError('Only US states are supported'); + } + + if (!policies || (Array.isArray(policies) && policies.length === 0)) { + policies = ['LDU']; + } + + if (policies.length > 1) { + throw new InstrumentationError('Only one policy is allowed'); + } + + if (policies[0] !== 'LDU') { + throw new InstrumentationError('Only LDU policy is supported'); + } + + if (policies && region) { + dataProcessingOptions = { policies, region }; + } + + return dataProcessingOptions; +}; + +const getPrivacySetting = (message) => { + const integrationObj = getIntegrationsObj(message, 'THE_TRADE_DESK'); + return integrationObj?.privacy_settings; +}; + +/** + * Enriches the track payload with extra properties present in 'properties' other than the ones defined in TTDCommonConfig.json and TTDItemConfig.json + * + * @param {Object} message - The message object containing the event information. + * @param {Object} payload - The payload object to be enriched. + * @returns {Object} - The enriched payload object. + */ +const enrichTrackPayload = (message, payload) => { + let rawPayload = { ...payload }; + const eventsMapInfo = ECOMM_EVENT_MAP[message.event.toLowerCase()]; + // checking if event is an ecomm one and itemsArray/products support is not present. e.g Product Added event + if (eventsMapInfo && !eventsMapInfo.itemsArray) { + const itemExclusionList = generateExclusionList(ITEM_CONFIGS); + rawPayload = extractCustomFields(message, rawPayload, ['properties'], itemExclusionList); + } else if (eventsMapInfo) { + // for ecomm events with products array supports. e.g Order Completed event + rawPayload = extractCustomFields( + message, + rawPayload, + ['properties'], + ['products', 'revenue', 'value'], + ); + } else { + // for custom events + rawPayload = extractCustomFields(message, rawPayload, ['properties'], ['value']); + } + return rawPayload; +}; + +module.exports = { + prepareFromConfig, + getRevenue, + prepareCommonPayload, + prepareItemsPayload, + getDeviceAdvertisingId, + getDestinationExternalIDObject, + getAdvertisingId, + prepareCustomProperties, + populateEventName, + getDataProcessingOptions, + getPrivacySetting, + enrichTrackPayload, +}; diff --git a/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.test.js b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.test.js new file mode 100644 index 0000000000..20b39fab93 --- /dev/null +++ b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.test.js @@ -0,0 +1,621 @@ +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { + getRevenue, + getDeviceAdvertisingId, + getDestinationExternalIDObject, + getAdvertisingId, + prepareCustomProperties, + populateEventName, + getDataProcessingOptions, + getPrivacySetting, + enrichTrackPayload, +} = require('./utils'); + +describe('getRevenue', () => { + it('should return revenue value from message properties for custom events', () => { + const message = { + event: 'customEvent', + properties: { + value: 100, + }, + }; + const result = getRevenue(message); + expect(result).toBe(100); + }); + + it('should calculate revenue based on price and quantity from message properties if ecomm event is supported for price calculation', () => { + const message = { + event: 'Product Added', + properties: { + price: 10, + quantity: 5, + }, + }; + const result = getRevenue(message); + expect(result).toBe(50); + }); + + it('should return revenue value from message properties if ecomm event is supported for revenue calculation', () => { + const message = { + event: 'Order Completed', + properties: { + revenue: 200, + }, + }; + const result = getRevenue(message); + expect(result).toBe(200); + }); + + it('should return default revenue value from properties.value for ecomm events', () => { + let message = { + event: 'Product Added', + properties: { + price: '', + value: 200, + }, + }; + let result = getRevenue(message); + expect(result).toBe(200); + + message = { + event: 'Order Completed', + properties: { + value: 200, + }, + }; + result = getRevenue(message); + expect(result).toBe(200); + }); + + it('should throw an Instrumentation error if revenue is missing for `Order Completed` event', () => { + const message = { + event: 'Order Completed', + properties: {}, + }; + expect(() => { + getRevenue(message); + }).toThrow(InstrumentationError); + }); +}); + +describe('getDeviceAdvertisingId', () => { + it('should return an object with deviceId and type properties when context.device.advertisingId and context.os.name are present', () => { + let message = { + context: { + device: { + advertisingId: '123456789', + }, + os: { + name: 'android', + }, + }, + }; + let result = getDeviceAdvertisingId(message); + expect(result).toEqual({ deviceId: '123456789', type: 'AAID' }); + + message = { + context: { + device: { + advertisingId: '123456789', + }, + os: { + name: 'ios', + }, + }, + }; + result = getDeviceAdvertisingId(message); + expect(result).toEqual({ deviceId: '123456789', type: 'IDFA' }); + + message = { + context: { + device: { + advertisingId: '123456789', + }, + os: { + name: 'windows', + }, + }, + }; + result = getDeviceAdvertisingId(message); + expect(result).toEqual({ deviceId: '123456789', type: 'NAID' }); + }); + + it('should return an object with undefined type property when osName is not "android", "windows", or an Apple OS', () => { + const message = { + context: { + device: { + advertisingId: '123456789', + }, + os: { + name: 'linux', + }, + }, + }; + const result = getDeviceAdvertisingId(message); + expect(result).toEqual({ deviceId: '123456789', type: undefined }); + }); + + it('should return an object with undefined deviceId and type properties when context is undefined', () => { + let message = {}; + let result = getDeviceAdvertisingId(message); + expect(result).toEqual({ deviceId: undefined, type: undefined }); + + message = { + context: {}, + }; + result = getDeviceAdvertisingId(message); + expect(result).toEqual({ deviceId: undefined, type: undefined }); + + message = { + context: { + device: {}, + }, + }; + result = getDeviceAdvertisingId(message); + expect(result).toEqual({ deviceId: undefined, type: undefined }); + }); +}); + +describe('getDestinationExternalIDObject', () => { + it('should return the external ID object when it exists in the message context', () => { + const message = { + context: { + externalId: [ + { id: '123', type: 'daid' }, + { id: '456', type: 'type123' }, + ], + }, + }; + const result = getDestinationExternalIDObject(message); + expect(result).toEqual({ id: '123', type: 'daid' }); + }); + + it('should return undefined when no external ID object exists in the message context', () => { + let message = { + context: { + externalId: [], + }, + }; + let result = getDestinationExternalIDObject(message); + expect(result).toBeUndefined(); + + message = { + context: {}, + }; + result = getDestinationExternalIDObject(message); + expect(result).toBeUndefined(); + }); + + it('should return the first matching external ID object in the array', () => { + const message = { + context: { + externalId: [ + { id: '', type: 'daid' }, + { id: '456', type: 'tdid' }, + { id: '789', type: 'UID2' }, + ], + }, + }; + const result = getDestinationExternalIDObject(message); + expect(result).toEqual({ id: '456', type: 'tdid' }); + }); +}); + +describe('getAdvertisingId', () => { + it('should return an object with the ID and type when the message contains a valid device advertising ID and OS type', () => { + const message = { + context: { + device: { + advertisingId: '1234567890', + }, + os: { + name: 'android', + }, + }, + }; + + const result = getAdvertisingId(message); + expect(result).toEqual({ id: '1234567890', type: 'AAID' }); + }); + + it('should return an object with the ID and type when the message contains a valid external ID object with a supported type', () => { + const message = { + context: { + externalId: [ + { + type: 'IDFA', + id: 'abcdefg', + }, + ], + }, + }; + + const result = getAdvertisingId(message); + expect(result).toEqual({ id: 'abcdefg', type: 'IDFA' }); + }); + + it('should return an object with undefined ID and type when the message contains a valid external ID object with an unsupported type', () => { + let message = { + context: { + externalId: [ + { + type: 'unsupported', + id: '1234567890', + }, + ], + }, + }; + + let result = getAdvertisingId(message); + expect(result).toEqual({ id: null, type: null }); + + message = { + context: { + device: { + advertisingId: '1234567890', + }, + }, + }; + + result = getAdvertisingId(message); + expect(result).toEqual({ id: null, type: null }); + }); + + it('should return an object with undefined ID and type when the message contains an external ID object with a supported type but no ID or missing externalId', () => { + let message = { + context: { + externalId: [ + { + type: 'IDFA', + }, + ], + }, + }; + let result = getAdvertisingId(message); + expect(result).toEqual({ id: null, type: null }); + + message = { + context: {}, + }; + result = getAdvertisingId(message); + expect(result).toEqual({ id: null, type: null }); + }); +}); + +describe('prepareCustomProperties', () => { + it('should return an empty object when customProperties is an empty array', () => { + const message = {}; + let destination = { Config: { customProperties: [] } }; + let result = prepareCustomProperties(message, destination); + expect(result).toEqual({}); + + destination = { Config: { customProperties: [{ rudderProperty: '', tradeDeskProperty: '' }] } }; + result = prepareCustomProperties(message, destination); + expect(result).toEqual({}); + + destination = { Config: { customProperties: undefined } }; + result = prepareCustomProperties(message, destination); + expect(result).toEqual({}); + }); + + it('should return an object with `tradeDeskProperty` as key and `rudderProperty` value as value when `rudderProperty` exists in message', () => { + const message = { + rudderProperty1: 'value1', + rudderProperty2: 'value2', + }; + const destination = { + Config: { + customProperties: [ + { + rudderProperty: 'rudderProperty1', + tradeDeskProperty: 'tradeDeskProperty1', + }, + { + rudderProperty: 'rudderProperty2', + tradeDeskProperty: 'tradeDeskProperty2', + }, + { + rudderProperty: 'rudderProperty3', + tradeDeskProperty: 'tradeDeskProperty3', + }, + ], + }, + }; + const result = prepareCustomProperties(message, destination); + expect(result).toEqual({ + tradeDeskProperty1: 'value1', + tradeDeskProperty2: 'value2', + }); + }); +}); + +describe('populateEventName', () => { + it('should return the eventName if it exists in the eventsMapping of destination.Config', () => { + const message = { event: 'someEvent' }; + const destination = { Config: { eventsMapping: [{ from: 'someEvent', to: 'mappedEvent' }] } }; + const result = populateEventName(message, destination); + expect(result).toBe('mappedEvent'); + }); + + it('should return the eventName if it exists in the ECOMM_EVENT_MAP', () => { + const message = { event: 'product added' }; + let destination = { Config: { eventsMapping: [{ from: 'someEvent', to: 'mappedEvent' }] } }; + let result = populateEventName(message, destination); + expect(result).toBe('addtocart'); + + destination = { Config: { eventsMapping: [] } }; + result = populateEventName(message, destination); + expect(result).toBe('addtocart'); + }); + + it('should return undefined if eventsMapping is an empty array', () => { + const message = { event: 'someEvent' }; + const destination = { Config: { eventsMapping: [] } }; + const result = populateEventName(message, destination); + expect(result).toBe('someEvent'); + }); +}); + +describe('getDataProcessingOptions', () => { + it('should return an object with policies and region when provided a integrationObj in message', () => { + const message = { + integrations: { + All: true, + THE_TRADE_DESK: { + policies: ['LDU'], + region: 'US-CO', + }, + }, + }; + const expected = { + policies: ['LDU'], + region: 'US-CO', + }; + const result = getDataProcessingOptions(message); + expect(result).toEqual(expected); + }); + + it('should throw an InstrumentationError if the region is not a US state', () => { + const message = { + integrations: { + All: true, + THE_TRADE_DESK: { + policies: ['LDU'], + region: 'EU-abc', + }, + }, + }; + expect(() => { + getDataProcessingOptions(message); + }).toThrow(InstrumentationError); + }); + + it('should throw an InstrumentationError if multiple policies are provided', () => { + const message = { + integrations: { + All: true, + THE_TRADE_DESK: { + policies: ['LDU', 'Policy2'], + region: 'US-CO', + }, + }, + }; + + expect(() => { + getDataProcessingOptions(message); + }).toThrow(InstrumentationError); + }); + + it('should throw an InstrumentationError if a policy other than `LDU` is provided', () => { + const message = { + integrations: { + All: true, + THE_TRADE_DESK: { + policies: ['Policy1'], + region: 'US-CO', + }, + }, + }; + + expect(() => { + getDataProcessingOptions(message); + }).toThrow(InstrumentationError); + }); + + it('should return an object with default policy `LDU` when policies are not provided', () => { + const message = { + integrations: { + All: true, + THE_TRADE_DESK: { + policies: [], + region: 'US-CO', + }, + }, + }; + + const expected = { + policies: ['LDU'], + region: 'US-CO', + }; + + expect(getDataProcessingOptions(message)).toEqual(expected); + }); + + it('should handle empty cases', () => { + let message = { + integrations: { + All: true, + THE_TRADE_DESK: {}, + }, + }; + + expect(getDataProcessingOptions(message)).toBeUndefined(); + + message = { + integrations: { + All: true, + }, + }; + + expect(getDataProcessingOptions(message)).toBeUndefined(); + + message = { + integrations: { + All: true, + THE_TRADE_DESK: { region: 'US-CO' }, + }, + }; + + expect(getDataProcessingOptions(message)).toEqual({ policies: ['LDU'], region: 'US-CO' }); + }); +}); + +describe('getPrivacySetting', () => { + it('should return the privacy settings object when it exists in the integration object', () => { + const message = { + integrations: { + All: true, + THE_TRADE_DESK: { + privacy_settings: [ + { + privacy_type: 'GDPR', + is_applicable: 1, + consent_string: 'ok', + }, + ], + }, + }, + }; + const expected = [ + { + privacy_type: 'GDPR', + is_applicable: 1, + consent_string: 'ok', + }, + ]; + const result = getPrivacySetting(message); + expect(result).toEqual(expected); + }); + + it('should return null when the privacy settings object does not exist in the integration object', () => { + let message = { integrations: {} }; + expect(getPrivacySetting(message)).toBeUndefined(); + + message = { integrations: { THE_TRADE_DESK: {} } }; + expect(getPrivacySetting(message)).toBeUndefined(); + + message = { integrations: { THE_TRADE_DESK: { privacy_settings: null } } }; + expect(getPrivacySetting(message)).toBeNull(); + }); +}); + +describe('enrichTrackPayload', () => { + it('should correctly enrich the payload with custom fields for ecomm events where product array is not supported', () => { + const message = { + event: 'Product Added', + properties: { + product_id: 'prd123', + sku: 'sku123', + brand: 'brand123', + property1: 'value1', + property2: 'value2', + }, + }; + const payload = { + items: [ + { + item_code: 'prd123', + }, + ], + property1: 'value1', + property2: 'value2', + }; + const expectedPayload = { + items: [ + { + item_code: 'prd123', + }, + ], + brand: 'brand123', + property1: 'value1', + property2: 'value2', + }; + + const result = enrichTrackPayload(message, payload); + expect(result).toEqual(expectedPayload); + }); + + it('should correctly enrich the payload with custom fields when the for ecomm events with products array support', () => { + const message = { + event: 'order completed', + properties: { + order_id: 'ord123', + total: 52.0, + subtotal: 45.0, + revenue: 50.0, + products: [{ product_id: 'prd123', sku: 'sku123', brand: 'brand123' }], + property1: 'value1', + property2: 'value2', + }, + }; + const payload = { + order_id: 'ord123', + value: 50.0, + items: [{ item_code: 'prd123', brand: 'brand123' }], + property1: 'value1', + property2: 'value2', + }; + const expectedPayload = { + order_id: 'ord123', + total: 52.0, + subtotal: 45.0, + value: 50.0, + items: [{ item_code: 'prd123', brand: 'brand123' }], + property1: 'value1', + property2: 'value2', + }; + + const result = enrichTrackPayload(message, payload); + + expect(result).toEqual(expectedPayload); + }); + + it('should return the enriched payload for custom event', () => { + const message = { + event: 'someEvent', + properties: { + order_id: 'ord123', + property1: 'value1', + property2: 'value2', + revenue: 10, + value: 11, + products: [ + { + product_id: 'prd123', + test: 'test', + }, + ], + }, + }; + const payload = { + order_id: 'ord123', + value: 11, + }; + const expectedPayload = { + order_id: 'ord123', + property1: 'value1', + property2: 'value2', + revenue: 10, + value: 11, + products: [ + { + product_id: 'prd123', + test: 'test', + }, + ], + }; + + const result = enrichTrackPayload(message, payload); + expect(result).toEqual(expectedPayload); + }); +}); diff --git a/src/v0/destinations/the_trade_desk/networkHandler.js b/src/v0/destinations/the_trade_desk/networkHandler.js index e9693e8132..30378e5ace 100644 --- a/src/v0/destinations/the_trade_desk/networkHandler.js +++ b/src/v0/destinations/the_trade_desk/networkHandler.js @@ -8,37 +8,28 @@ const { getSignatureHeader } = require('../../../cdk/v2/destinations/the_trade_d const { isHttpStatusSuccess } = require('../../util/index'); const tags = require('../../util/tags'); const { JSON_MIME_TYPE } = require('../../util/constant'); -const { - REAL_TIME_CONVERSION_ENDPOINT, -} = require('../../../cdk/v2/destinations/the_trade_desk/config'); const proxyRequest = async (request) => { const { endpoint, data, method, params, headers, config } = prepareProxyRequest(request); - let ProxyHeaders = { - ...headers, - 'Content-Type': JSON_MIME_TYPE, - }; - - // For first party data flow - if (endpoint !== REAL_TIME_CONVERSION_ENDPOINT) { - if (!config?.advertiserSecretKey) { - throw new PlatformError('Advertiser secret key is missing in destination config. Aborting'); - } - if (!process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY) { - throw new PlatformError('Data provider secret key is missing. Aborting'); - } + if (!config?.advertiserSecretKey) { + throw new PlatformError('Advertiser secret key is missing in destination config. Aborting'); + } - ProxyHeaders = { - ...ProxyHeaders, - TtdSignature: getSignatureHeader(data, config.advertiserSecretKey), - 'TtdSignature-dp': getSignatureHeader( - data, - process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY, - ), - }; + if (!process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY) { + throw new PlatformError('Data provider secret key is missing. Aborting'); } + const ProxyHeaders = { + ...headers, + 'Content-Type': JSON_MIME_TYPE, + TtdSignature: getSignatureHeader(data, config.advertiserSecretKey), + 'TtdSignature-dp': getSignatureHeader( + data, + process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY, + ), + }; + const requestOptions = { url: endpoint, data, @@ -69,7 +60,6 @@ const responseHandler = (destinationResponse) => { // Trade desk first party data api returns 200 with an error in case of "Failed to parse TDID, DAID, UID2, IDL, EUID, or failed to decrypt UID2Token or EUIDToken" // https://partner.thetradedesk.com/v3/portal/data/doc/post-data-advertiser-external // {"FailedLines":[{"ErrorCode":"MissingUserId","Message":"Invalid DAID, item #1"}]} - // For real time conversion api we don't have separate response handling, trade desk always return 400 for bad events. if ('FailedLines' in response && response.FailedLines.length > 0) { throw new AbortedError( `Request failed with status: ${status} due to ${JSON.stringify(response)}`, diff --git a/test/integrations/destinations/the_trade_desk/common.ts b/test/integrations/destinations/the_trade_desk/common.ts index 8deaf60034..d792c7faae 100644 --- a/test/integrations/destinations/the_trade_desk/common.ts +++ b/test/integrations/destinations/the_trade_desk/common.ts @@ -3,7 +3,6 @@ const destTypeInUpperCase = 'THE_TRADE_DESK'; const advertiserId = 'test-advertiser-id'; const dataProviderId = 'rudderstack'; const segmentName = 'test-segment'; -const trackerId = 'test-trackerId'; const sampleDestination = { Config: { advertiserId, @@ -11,7 +10,6 @@ const sampleDestination = { dataServer: 'apac', ttlInDays: 30, audienceId: segmentName, - trackerId, }, DestinationDefinition: { Config: { cdkV2Enabled: true } }, }; @@ -35,73 +33,12 @@ const sampleContext = { sources: sampleSource, }; -const sampleContextForConversion = { - app: { - build: '1.0.0', - name: 'RudderLabs Android SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.5', - }, - device: { - adTrackingEnabled: true, - advertisingId: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', - id: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', - manufacturer: 'Google', - model: 'AOSP on IA Emulator', - name: 'generic_x86_arm', - type: 'ios', - attTrackingStatus: 3, - }, - externalId: [ - { - type: 'daid', - id: 'test-daid', - }, - ], - ip: '0.0.0.0', - page: { - referrer: 'https://docs.rudderstack.com/destinations/trade_desk', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.5', - }, - locale: 'en-GB', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', -}; - -const integrationObject = { - All: true, - THE_TRADE_DESK: { - policies: ['LDU'], - region: 'US-CA', - privacy_settings: [ - { - privacy_type: 'GDPR', - is_applicable: 1, - consent_string: 'ok', - }, - ], - }, -}; - export { destType, destTypeInUpperCase, advertiserId, dataProviderId, segmentName, - trackerId, sampleDestination, sampleContext, - sampleContextForConversion, - integrationObject, }; diff --git a/test/integrations/destinations/the_trade_desk/delivery/data.ts b/test/integrations/destinations/the_trade_desk/delivery/data.ts index da8f60972e..320eb6dcfe 100644 --- a/test/integrations/destinations/the_trade_desk/delivery/data.ts +++ b/test/integrations/destinations/the_trade_desk/delivery/data.ts @@ -5,7 +5,6 @@ import { dataProviderId, segmentName, sampleDestination, - trackerId, } from '../common'; beforeAll(() => { @@ -246,169 +245,4 @@ export const data = [ }, }, }, - { - name: destType, - description: 'Successful delivery of realtime conversion event to Trade Desk', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - currency: 'USD', - event_name: 'viewitem', - value: 249.95000000000002, - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - qty: 5, - price: 49.99, - }, - ], - category: 'Games', - brand: 'Wyatt Games', - variant: 'exapansion pack', - coupon: 'PREORDER15', - position: 1, - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.webp', - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - destinationResponse: { - response: { - Message: null, - EventResponses: [], - }, - status: 200, - }, - message: 'Request Processed Successfully', - status: 200, - }, - }, - }, - }, - }, - { - name: destType, - description: - 'Error response from the Trade Desk due to invalid real time conversion event payload', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - currency: 'USD', - event_name: 'viewitem', - value: 249.95000000000002, - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - qty: 5, - price: 49.99, - }, - ], - category: 'Games', - brand: 'Wyatt Games', - variant: 'exapansion pack', - coupon: 'PREORDER15', - position: 1, - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.webp', - privacy_settings: [{}], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 400, - body: { - output: { - destinationResponse: { - response: { - Message: null, - EventResponses: [ - { - EventIndex: 0, - EventErrors: [ - { - Error: 'InvalidPrivacySetting', - ErrorMessage: 'The request has an invalid privacy setting.', - }, - ], - EventWarnings: [], - Successful: false, - }, - ], - }, - status: 400, - }, - message: - 'Request failed with status: 400 due to {"Message":null,"EventResponses":[{"EventIndex":0,"EventErrors":[{"Error":"InvalidPrivacySetting","ErrorMessage":"The request has an invalid privacy setting."}],"EventWarnings":[],"Successful":false}]}', - statTags: { - destType: destTypeInUpperCase, - destinationId: 'Non-determininable', - errorCategory: 'network', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - workspaceId: 'Non-determininable', - }, - status: 400, - }, - }, - }, - }, - }, ]; diff --git a/test/integrations/destinations/the_trade_desk/network.ts b/test/integrations/destinations/the_trade_desk/network.ts index 5908cbf8f5..ed6bdf4c7d 100644 --- a/test/integrations/destinations/the_trade_desk/network.ts +++ b/test/integrations/destinations/the_trade_desk/network.ts @@ -1,4 +1,4 @@ -import { destType, advertiserId, dataProviderId, segmentName, trackerId } from './common'; +import { destType, advertiserId, dataProviderId, segmentName } from './common'; export const networkCallsData = [ { @@ -103,107 +103,4 @@ export const networkCallsData = [ statusText: 'Ok', }, }, - { - httpReq: { - url: 'https://insight.adsrvr.org/track/realtimeconversion', - data: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - currency: 'USD', - event_name: 'viewitem', - value: 249.95000000000002, - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - qty: 5, - price: 49.99, - }, - ], - category: 'Games', - brand: 'Wyatt Games', - variant: 'exapansion pack', - coupon: 'PREORDER15', - position: 1, - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.webp', - }, - ], - }, - params: { destination: destType }, - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'POST', - }, - httpRes: { - data: { - Message: null, - EventResponses: [], - }, - status: 200, - statusText: 'OK', - }, - }, - { - httpReq: { - url: 'https://insight.adsrvr.org/track/realtimeconversion', - data: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - currency: 'USD', - event_name: 'viewitem', - value: 249.95000000000002, - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - qty: 5, - price: 49.99, - }, - ], - category: 'Games', - brand: 'Wyatt Games', - variant: 'exapansion pack', - coupon: 'PREORDER15', - position: 1, - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.webp', - privacy_settings: [{}], - }, - ], - }, - params: { destination: destType }, - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'POST', - }, - httpRes: { - data: { - Message: null, - EventResponses: [ - { - EventIndex: 0, - EventErrors: [ - { - Error: 'InvalidPrivacySetting', - ErrorMessage: 'The request has an invalid privacy setting.', - }, - ], - EventWarnings: [], - Successful: false, - }, - ], - }, - status: 400, - statusText: 'Bad Request', - }, - }, ]; diff --git a/test/integrations/destinations/the_trade_desk/router/data.ts b/test/integrations/destinations/the_trade_desk/router/data.ts index 691ec703b9..f095f561db 100644 --- a/test/integrations/destinations/the_trade_desk/router/data.ts +++ b/test/integrations/destinations/the_trade_desk/router/data.ts @@ -6,11 +6,8 @@ import { advertiserId, dataProviderId, segmentName, - trackerId, sampleDestination, sampleContext, - sampleContextForConversion, - integrationObject, } from '../common'; export const data = [ @@ -868,786 +865,6 @@ export const data = [ }, mockFns: defaultMockFns, }, - { - name: destType, - description: - 'Mapped Ecommerce events (product added, product viewed, product added to wishlist, cart viewed, checkout started, order completed)', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'track', - event: 'Product Added', - messageId: 'messageId123', - context: sampleContextForConversion, - properties: { - product_id: '622c6f5d5cf86a4c77358033', - sku: '8472-998-0112', - category: 'Games', - name: 'Cones of Dunshire', - brand: 'Wyatt Games', - variant: 'exapansion pack', - price: 49.99, - quantity: 5, - coupon: 'PREORDER15', - position: 1, - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.webp', - key1: 'value1', - }, - integrations: integrationObject, - }, - destination: overrideDestination(sampleDestination, { - customProperties: [ - { - rudderProperty: 'properties.key1', - tradeDeskProperty: 'td1', - }, - { - rudderProperty: 'properties.key2', - tradeDeskProperty: 'td2', - }, - ], - }), - metadata: { - jobId: 1, - }, - }, - { - message: { - type: 'track', - event: 'Product Viewed', - properties: { - product_id: '622c6f5d5cf86a4c77358033', - sku: '8472-998-0112', - category: 'Games', - name: 'Cones of Dunshire', - brand: 'Wyatt Games', - variant: 'exapansion pack', - price: 49.99, - quantity: 5, - coupon: 'PREORDER15', - currency: 'USD', - position: 1, - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.webp', - }, - }, - destination: sampleDestination, - metadata: { - jobId: 2, - }, - }, - { - message: { - type: 'track', - event: 'Product Added to Wishlist', - properties: { - wishlist_id: '74fkdjfl0jfdkdj29j030', - wishlist_name: 'New Games', - product_id: '622c6f5d5cf86a4c77358033', - sku: '8472-998-0112', - category: 'Games', - name: 'Cones of Dunshire', - brand: 'Wyatt Games', - variant: 'exapansion pack', - price: 49.99, - quantity: 1, - coupon: 'PREORDER15', - position: 1, - url: 'https://www.site.com/product/path', - image_url: 'https://www.site.com/product/path.jpg', - }, - }, - destination: sampleDestination, - metadata: { - jobId: 3, - }, - }, - { - message: { - type: 'track', - event: 'Cart Viewed', - properties: { - cart_id: '6b2c6f5aecf86a4ae77358ae3', - products: [ - { - product_id: '622c6f5d5cf86a4c77358033', - sku: '8472-998-0112', - name: 'Cones of Dunshire', - price: 49.99, - position: 5, - category: 'Games', - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.jpg', - }, - { - product_id: '577c6f5d5cf86a4c7735ba03', - sku: '3309-483-2201', - name: 'Five Crowns', - price: 5.99, - position: 2, - category: 'Games', - }, - ], - }, - }, - destination: sampleDestination, - metadata: { - jobId: 4, - }, - }, - { - message: { - type: 'track', - event: 'Checkout Started', - properties: { - order_id: '40684e8f0eaf000000000000', - affiliation: 'Vandelay Games', - value: 52, - revenue: 50.0, - shipping: 4, - tax: 3, - discount: 5, - coupon: 'NEWCUST5', - currency: 'USD', - products: [ - { - product_id: '622c6f5d5cf86a4c77358033', - sku: '8472-998-0112', - name: 'Cones of Dunshire', - price: 40, - position: 1, - category: 'Games', - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.jpg', - }, - { - product_id: '577c6f5d5cf86a4c7735ba03', - sku: '3309-483-2201', - name: 'Five Crowns', - price: 5, - position: 2, - category: 'Games', - }, - ], - }, - }, - destination: sampleDestination, - metadata: { - jobId: 5, - }, - }, - { - message: { - type: 'track', - event: 'Order Completed', - properties: { - checkout_id: '70324a1f0eaf000000000000', - order_id: '40684e8f0eaf000000000000', - affiliation: 'Vandelay Games', - total: 52.0, - subtotal: 45.0, - revenue: 50.0, - shipping: 4.0, - tax: 3.0, - discount: 5.0, - coupon: 'NEWCUST5', - currency: 'USD', - products: [ - { - product_id: '622c6f5d5cf86a4c77358033', - sku: '8472-998-0112', - name: 'Cones of Dunshire', - price: 40, - position: 1, - category: 'Games', - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.jpg', - }, - { - product_id: '577c6f5d5cf86a4c7735ba03', - sku: '3309-483-2201', - name: 'Five Crowns', - price: 5, - position: 2, - category: 'Games', - }, - ], - }, - }, - destination: sampleDestination, - metadata: { - jobId: 6, - }, - }, - ], - destType, - }, - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - event_name: 'addtocart', - value: 249.95000000000002, - adid: 'test-daid', - adid_type: 'DAID', - client_ip: '0.0.0.0', - referrer_url: 'https://docs.rudderstack.com/destinations/trade_desk', - imp: 'messageId123', - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - qty: 5, - price: 49.99, - }, - ], - td1: 'value1', - category: 'Games', - brand: 'Wyatt Games', - variant: 'exapansion pack', - coupon: 'PREORDER15', - position: 1, - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.webp', - data_processing_option: { - policies: ['LDU'], - region: 'US-CA', - }, - privacy_settings: [ - { - privacy_type: 'GDPR', - is_applicable: 1, - consent_string: 'ok', - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 1, - }, - ], - batched: false, - statusCode: 200, - destination: overrideDestination(sampleDestination, { - customProperties: [ - { - rudderProperty: 'properties.key1', - tradeDeskProperty: 'td1', - }, - { - rudderProperty: 'properties.key2', - tradeDeskProperty: 'td2', - }, - ], - }), - }, - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - currency: 'USD', - event_name: 'viewitem', - value: 249.95000000000002, - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - qty: 5, - price: 49.99, - }, - ], - category: 'Games', - brand: 'Wyatt Games', - variant: 'exapansion pack', - coupon: 'PREORDER15', - position: 1, - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.webp', - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 2, - }, - ], - batched: false, - statusCode: 200, - destination: sampleDestination, - }, - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - event_name: 'wishlistitem', - value: 49.99, - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - qty: 1, - price: 49.99, - }, - ], - wishlist_id: '74fkdjfl0jfdkdj29j030', - wishlist_name: 'New Games', - category: 'Games', - brand: 'Wyatt Games', - variant: 'exapansion pack', - coupon: 'PREORDER15', - position: 1, - url: 'https://www.site.com/product/path', - image_url: 'https://www.site.com/product/path.jpg', - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 3, - }, - ], - batched: false, - statusCode: 200, - destination: sampleDestination, - }, - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - event_name: 'viewcart', - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - price: 49.99, - position: 5, - category: 'Games', - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.jpg', - }, - { - item_code: '577c6f5d5cf86a4c7735ba03', - name: 'Five Crowns', - price: 5.99, - position: 2, - category: 'Games', - }, - ], - cart_id: '6b2c6f5aecf86a4ae77358ae3', - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 4, - }, - ], - batched: false, - statusCode: 200, - destination: sampleDestination, - }, - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - currency: 'USD', - order_id: '40684e8f0eaf000000000000', - event_name: 'startcheckout', - value: 50, - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - price: 40, - position: 1, - category: 'Games', - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.jpg', - }, - { - item_code: '577c6f5d5cf86a4c7735ba03', - name: 'Five Crowns', - price: 5, - position: 2, - category: 'Games', - }, - ], - affiliation: 'Vandelay Games', - shipping: 4, - tax: 3, - discount: 5, - coupon: 'NEWCUST5', - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 5, - }, - ], - batched: false, - statusCode: 200, - destination: sampleDestination, - }, - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - currency: 'USD', - order_id: '40684e8f0eaf000000000000', - event_name: 'purchase', - value: 50, - items: [ - { - item_code: '622c6f5d5cf86a4c77358033', - name: 'Cones of Dunshire', - price: 40, - position: 1, - category: 'Games', - url: 'https://www.website.com/product/path', - image_url: 'https://www.website.com/product/path.jpg', - }, - { - item_code: '577c6f5d5cf86a4c7735ba03', - name: 'Five Crowns', - price: 5, - position: 2, - category: 'Games', - }, - ], - checkout_id: '70324a1f0eaf000000000000', - affiliation: 'Vandelay Games', - total: 52.0, - subtotal: 45.0, - shipping: 4.0, - tax: 3.0, - discount: 5.0, - coupon: 'NEWCUST5', - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 6, - }, - ], - batched: false, - statusCode: 200, - destination: sampleDestination, - }, - ], - }, - }, - }, - }, - { - name: destType, - description: 'Custom event', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'track', - event: 'custom event abc', - properties: { - key1: 'value1', - value: 25, - product_id: 'prd123', - key2: true, - test: 'test123', - }, - }, - destination: overrideDestination(sampleDestination, { - customProperties: [ - { - rudderProperty: 'properties.key1', - tradeDeskProperty: 'td1', - }, - { - rudderProperty: 'properties.key2', - tradeDeskProperty: 'td2', - }, - ], - }), - metadata: { - jobId: 1, - }, - }, - ], - destType, - }, - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - event_name: 'custom event abc', - value: 25, - product_id: 'prd123', - test: 'test123', - td1: 'value1', - td2: true, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 1, - }, - ], - batched: false, - statusCode: 200, - destination: overrideDestination(sampleDestination, { - customProperties: [ - { - rudderProperty: 'properties.key1', - tradeDeskProperty: 'td1', - }, - { - rudderProperty: 'properties.key2', - tradeDeskProperty: 'td2', - }, - ], - }), - }, - ], - }, - }, - }, - }, - { - name: destType, - description: 'Mapped standard trade desk event', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'track', - event: 'custom event abc', - properties: { - key1: 'value1', - value: 25, - product_id: 'prd123', - key2: true, - test: 'test123', - }, - }, - destination: overrideDestination(sampleDestination, { - eventsMapping: [ - { - from: 'custom event abc', - to: 'direction', - }, - ], - }), - metadata: { - jobId: 1, - }, - }, - ], - destType, - }, - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - event_name: 'direction', - value: 25, - product_id: 'prd123', - test: 'test123', - key1: 'value1', - key2: true, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 1, - }, - ], - batched: false, - statusCode: 200, - destination: overrideDestination(sampleDestination, { - eventsMapping: [ - { - from: 'custom event abc', - to: 'direction', - }, - ], - }), - }, - ], - }, - }, - }, - }, { name: destType, description: 'Batch call with different event types', @@ -1674,30 +891,6 @@ export const data = [ jobId: 1, }, }, - { - message: { - type: 'track', - event: 'custom event abc', - properties: { - key1: 'value1', - value: 25, - revenue: 10, - product_id: 'prd123', - key2: true, - test: 'test123', - products: [ - { - product_id: 'prd123', - test: 'test', - }, - ], - }, - }, - destination: sampleDestination, - metadata: { - jobId: 2, - }, - }, { message: { type: 'identify', @@ -1711,7 +904,7 @@ export const data = [ }, destination: sampleDestination, metadata: { - jobId: 3, + jobId: 2, }, }, ], @@ -1767,53 +960,8 @@ export const data = [ destination: sampleDestination, }, { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', - headers: {}, - params: {}, - body: { - JSON: { - data: [ - { - tracker_id: trackerId, - adv: advertiserId, - event_name: 'custom event abc', - value: 25, - product_id: 'prd123', - test: 'test123', - key1: 'value1', - key2: true, - revenue: 10, - products: [ - { - product_id: 'prd123', - test: 'test', - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [ - { - jobId: 2, - }, - ], batched: false, - statusCode: 200, - destination: sampleDestination, - }, - { - batched: false, - metadata: [{ jobId: 3 }], + metadata: [{ jobId: 2 }], statusCode: 400, error: 'Event type identify is not supported', statTags: { @@ -1831,119 +979,4 @@ export const data = [ }, }, }, - { - name: destType, - description: 'Tracker id is not present', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: overrideDestination(sampleDestination, { trackerId: '' }), - metadata: { - jobId: 1, - }, - }, - { - message: { - type: 'track', - event: 'custom event abc', - properties: { - key1: 'value1', - value: 25, - product_id: 'prd123', - key2: true, - test: 'test123', - }, - }, - destination: overrideDestination(sampleDestination, { trackerId: '' }), - metadata: { - jobId: 2, - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: [ - { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://sin-data.adsrvr.org/data/advertiser', - headers: {}, - params: {}, - body: { - JSON: { - DataProviderId: dataProviderId, - AdvertiserId: advertiserId, - Items: [ - { - DAID: 'test-daid-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - ], - metadata: [ - { - jobId: 1, - }, - ], - batched: true, - statusCode: 200, - destination: overrideDestination(sampleDestination, { trackerId: '' }), - }, - { - batched: false, - metadata: [{ jobId: 2 }], - statusCode: 400, - error: 'Tracking Tag ID is not present. Aborting', - statTags: { - errorCategory: 'dataValidation', - errorType: 'configuration', - destType: 'THE_TRADE_DESK', - module: 'destination', - implementation: 'cdkV2', - feature: 'router', - }, - destination: overrideDestination(sampleDestination, { trackerId: '' }), - }, - ], - }, - }, - }, - }, ]; diff --git a/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts b/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts new file mode 100644 index 0000000000..3af7791ec8 --- /dev/null +++ b/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts @@ -0,0 +1,79 @@ +const destType = 'the_trade_desk_real_time_conversions'; +const destTypeInUpperCase = 'THE_TRADE_DESK_REAL_TIME_CONVERSIONS'; +const advertiserId = 'test-advertiser-id'; +const trackerId = 'test-trackerId'; +const sampleDestination = { + Config: { + advertiserId, + trackerId, + }, + DestinationDefinition: { Config: { cdkV2Enabled: true } }, +}; + +const sampleContextForConversion = { + app: { + build: '1.0.0', + name: 'RudderLabs Android SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.5', + }, + device: { + adTrackingEnabled: true, + advertisingId: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + id: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + manufacturer: 'Google', + model: 'AOSP on IA Emulator', + name: 'generic_x86_arm', + type: 'ios', + attTrackingStatus: 3, + }, + externalId: [ + { + type: 'daid', + id: 'test-daid', + }, + ], + ip: '0.0.0.0', + page: { + referrer: 'https://docs.rudderstack.com/destinations/trade_desk', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.5', + }, + locale: 'en-GB', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', +}; + +const integrationObject = { + All: true, + THE_TRADE_DESK: { + policies: ['LDU'], + region: 'US-CA', + privacy_settings: [ + { + privacy_type: 'GDPR', + is_applicable: 1, + consent_string: 'ok', + }, + ], + }, +}; + +export { + destType, + destTypeInUpperCase, + advertiserId, + trackerId, + sampleDestination, + sampleContextForConversion, + integrationObject, +}; diff --git a/test/integrations/destinations/the_trade_desk_real_time_conversions/processor/data.ts b/test/integrations/destinations/the_trade_desk_real_time_conversions/processor/data.ts new file mode 100644 index 0000000000..264c760088 --- /dev/null +++ b/test/integrations/destinations/the_trade_desk_real_time_conversions/processor/data.ts @@ -0,0 +1,984 @@ +import { overrideDestination } from '../../../testUtils'; +import { + destType, + destTypeInUpperCase, + advertiserId, + trackerId, + sampleDestination, + sampleContextForConversion, + integrationObject, +} from '../common'; + +export const data = [ + { + name: destType, + description: 'Missing advertiser ID in the config', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'custom event abc', + properties: { + key1: 'value1', + value: 25, + product_id: 'prd123', + key2: true, + test: 'test123', + }, + }, + destination: overrideDestination(sampleDestination, { advertiserId: '' }), + metadata: { + jobId: 1, + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Advertiser ID is not present. Aborting: Workflow: procWorkflow, Step: validateConfig, ChildStep: undefined, OriginalError: Advertiser ID is not present. Aborting', + statTags: { + destType: destTypeInUpperCase, + implementation: 'cdkV2', + feature: 'processor', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + metadata: { + jobId: 1, + }, + statusCode: 400, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Tracker id is not present', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'custom event abc', + properties: { + key1: 'value1', + value: 25, + product_id: 'prd123', + key2: true, + test: 'test123', + }, + }, + destination: overrideDestination(sampleDestination, { trackerId: '' }), + metadata: { + jobId: 1, + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Tracking Tag ID is not present. Aborting: Workflow: procWorkflow, Step: validateConfig, ChildStep: undefined, OriginalError: Tracking Tag ID is not present. Aborting', + statTags: { + destType: destTypeInUpperCase, + implementation: 'cdkV2', + feature: 'processor', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + metadata: { + jobId: 1, + }, + statusCode: 400, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Unsupported event type', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'identify', + context: { + traits: { + name: 'John Doe', + email: 'johndoe@gmail.com', + age: 25, + }, + }, + }, + destination: sampleDestination, + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Event type identify is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Event type identify is not supported', + statTags: { + destType: destTypeInUpperCase, + implementation: 'cdkV2', + feature: 'processor', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + }, + metadata: { + jobId: 1, + }, + statusCode: 400, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Product Added', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'Product Added', + messageId: 'messageId123', + context: sampleContextForConversion, + properties: { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + category: 'Games', + name: 'Cones of Dunshire', + brand: 'Wyatt Games', + variant: 'expansion pack', + price: 49.99, + quantity: 5, + coupon: 'PREORDER15', + position: 1, + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.webp', + key1: 'value1', + }, + integrations: integrationObject, + }, + destination: overrideDestination(sampleDestination, { + customProperties: [ + { + rudderProperty: 'properties.key1', + tradeDeskProperty: 'td1', + }, + { + rudderProperty: 'properties.key2', + tradeDeskProperty: 'td2', + }, + ], + }), + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + data: [ + { + tracker_id: trackerId, + adv: advertiserId, + event_name: 'addtocart', + value: 249.95000000000002, + adid: 'test-daid', + adid_type: 'DAID', + client_ip: '0.0.0.0', + referrer_url: 'https://docs.rudderstack.com/destinations/trade_desk', + imp: 'messageId123', + items: [ + { + item_code: '622c6f5d5cf86a4c77358033', + name: 'Cones of Dunshire', + qty: 5, + price: 49.99, + }, + ], + td1: 'value1', + category: 'Games', + brand: 'Wyatt Games', + variant: 'expansion pack', + coupon: 'PREORDER15', + position: 1, + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.webp', + data_processing_option: { + policies: ['LDU'], + region: 'US-CA', + }, + privacy_settings: [ + { + privacy_type: 'GDPR', + is_applicable: 1, + consent_string: 'ok', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + jobId: 1, + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Product Viewed', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'Product Viewed', + properties: { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + category: 'Games', + name: 'Cones of Dunshire', + brand: 'Wyatt Games', + variant: 'expansion pack', + price: 49.99, + quantity: 5, + coupon: 'PREORDER15', + currency: 'USD', + position: 1, + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.webp', + }, + }, + destination: sampleDestination, + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + data: [ + { + tracker_id: trackerId, + adv: advertiserId, + currency: 'USD', + event_name: 'viewitem', + value: 249.95000000000002, + items: [ + { + item_code: '622c6f5d5cf86a4c77358033', + name: 'Cones of Dunshire', + qty: 5, + price: 49.99, + }, + ], + category: 'Games', + brand: 'Wyatt Games', + variant: 'expansion pack', + coupon: 'PREORDER15', + position: 1, + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.webp', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + jobId: 1, + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Product Added to Wishlist', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'Product Added to Wishlist', + properties: { + wishlist_id: '74fkdjfl0jfdkdj29j030', + wishlist_name: 'New Games', + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + category: 'Games', + name: 'Cones of Dunshire', + brand: 'Wyatt Games', + variant: 'expansion pack', + price: 49.99, + quantity: 1, + coupon: 'PREORDER15', + position: 1, + url: 'https://www.site.com/product/path', + image_url: 'https://www.site.com/product/path.jpg', + }, + }, + destination: sampleDestination, + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + data: [ + { + tracker_id: trackerId, + adv: advertiserId, + event_name: 'wishlistitem', + value: 49.99, + items: [ + { + item_code: '622c6f5d5cf86a4c77358033', + name: 'Cones of Dunshire', + qty: 1, + price: 49.99, + }, + ], + wishlist_id: '74fkdjfl0jfdkdj29j030', + wishlist_name: 'New Games', + category: 'Games', + brand: 'Wyatt Games', + variant: 'expansion pack', + coupon: 'PREORDER15', + position: 1, + url: 'https://www.site.com/product/path', + image_url: 'https://www.site.com/product/path.jpg', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + jobId: 1, + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Cart Viewed', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'Cart Viewed', + properties: { + cart_id: '6b2c6f5aecf86a4ae77358ae3', + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + name: 'Cones of Dunshire', + price: 49.99, + position: 5, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + name: 'Five Crowns', + price: 5.99, + position: 2, + category: 'Games', + }, + ], + }, + }, + destination: sampleDestination, + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + data: [ + { + tracker_id: trackerId, + adv: advertiserId, + event_name: 'viewcart', + items: [ + { + item_code: '622c6f5d5cf86a4c77358033', + name: 'Cones of Dunshire', + price: 49.99, + position: 5, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { + item_code: '577c6f5d5cf86a4c7735ba03', + name: 'Five Crowns', + price: 5.99, + position: 2, + category: 'Games', + }, + ], + cart_id: '6b2c6f5aecf86a4ae77358ae3', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + jobId: 1, + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Checkout Started', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'Checkout Started', + properties: { + order_id: '40684e8f0eaf000000000000', + affiliation: 'Vandelay Games', + value: 52, + revenue: 50.0, + shipping: 4, + tax: 3, + discount: 5, + coupon: 'NEWCUST5', + currency: 'USD', + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + name: 'Cones of Dunshire', + price: 40, + position: 1, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + name: 'Five Crowns', + price: 5, + position: 2, + category: 'Games', + }, + ], + }, + }, + destination: sampleDestination, + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + data: [ + { + tracker_id: trackerId, + adv: advertiserId, + currency: 'USD', + order_id: '40684e8f0eaf000000000000', + event_name: 'startcheckout', + value: 50, + items: [ + { + item_code: '622c6f5d5cf86a4c77358033', + name: 'Cones of Dunshire', + price: 40, + position: 1, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { + item_code: '577c6f5d5cf86a4c7735ba03', + name: 'Five Crowns', + price: 5, + position: 2, + category: 'Games', + }, + ], + affiliation: 'Vandelay Games', + shipping: 4, + tax: 3, + discount: 5, + coupon: 'NEWCUST5', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + jobId: 1, + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Order Completed', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'Order Completed', + properties: { + checkout_id: '70324a1f0eaf000000000000', + order_id: '40684e8f0eaf000000000000', + affiliation: 'Vandelay Games', + total: 52.0, + subtotal: 45.0, + revenue: 50.0, + shipping: 4.0, + tax: 3.0, + discount: 5.0, + coupon: 'NEWCUST5', + currency: 'USD', + products: [ + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + name: 'Cones of Dunshire', + price: 40, + position: 1, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + name: 'Five Crowns', + price: 5, + position: 2, + category: 'Games', + }, + ], + }, + }, + destination: sampleDestination, + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + data: [ + { + tracker_id: trackerId, + adv: advertiserId, + currency: 'USD', + order_id: '40684e8f0eaf000000000000', + event_name: 'purchase', + value: 50, + items: [ + { + item_code: '622c6f5d5cf86a4c77358033', + name: 'Cones of Dunshire', + price: 40, + position: 1, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { + item_code: '577c6f5d5cf86a4c7735ba03', + name: 'Five Crowns', + price: 5, + position: 2, + category: 'Games', + }, + ], + checkout_id: '70324a1f0eaf000000000000', + affiliation: 'Vandelay Games', + total: 52.0, + subtotal: 45.0, + shipping: 4.0, + tax: 3.0, + discount: 5.0, + coupon: 'NEWCUST5', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + jobId: 1, + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Custom event', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'custom event abc', + properties: { + key1: 'value1', + value: 25, + product_id: 'prd123', + key2: true, + test: 'test123', + }, + }, + destination: overrideDestination(sampleDestination, { + customProperties: [ + { + rudderProperty: 'properties.key1', + tradeDeskProperty: 'td1', + }, + { + rudderProperty: 'properties.key2', + tradeDeskProperty: 'td2', + }, + ], + }), + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + data: [ + { + tracker_id: trackerId, + adv: advertiserId, + event_name: 'custom event abc', + value: 25, + product_id: 'prd123', + test: 'test123', + td1: 'value1', + td2: true, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + jobId: 1, + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: destType, + description: 'Mapped standard trade desk event', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'custom event abc', + properties: { + key1: 'value1', + value: 25, + product_id: 'prd123', + key2: true, + test: 'test123', + }, + }, + destination: overrideDestination(sampleDestination, { + eventsMapping: [ + { + from: 'custom event abc', + to: 'direction', + }, + ], + }), + metadata: { + jobId: 1, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insight.adsrvr.org/track/realtimeconversion', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + data: [ + { + tracker_id: trackerId, + adv: advertiserId, + event_name: 'direction', + value: 25, + product_id: 'prd123', + test: 'test123', + key1: 'value1', + key2: true, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + jobId: 1, + }, + statusCode: 200, + }, + ], + }, + }, + }, +];