diff --git a/src/v0/destinations/tune/transform.js b/src/v0/destinations/tune/transform.js new file mode 100644 index 0000000000..97dad0e3d3 --- /dev/null +++ b/src/v0/destinations/tune/transform.js @@ -0,0 +1,90 @@ +const get = require('get-value'); +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { + defaultRequestConfig, + simpleProcessRouterDest, + getHashFromArray, + isDefinedAndNotNull, + isNotEmpty, +} = require('../../util'); + +const mapPropertiesWithNestedSupport = (msg, properties, mappings) => { + const mappedObj = {}; // Create a new object for parameters + Object.entries(mappings).forEach(([key, value]) => { + const keyStr = `${key}`; + const args = { object: properties, key: keyStr }; + if (args.key.split('.').length > 1) { + // Handle nested keys + args.object = msg; // This line modifies the object property of args + } + const data = get(args.object, args.key); + if (isDefinedAndNotNull(data) && isNotEmpty(data)) { + mappedObj[value] = data; // Map to the corresponding destination key + } + }); + return mappedObj; // Return the new params object +}; + +const responseBuilder = (message, { Config }) => { + const { tuneEvents } = Config; // Extract tuneEvents from config + const { properties, event: messageEvent } = message; // Destructure properties and event from message + + // Find the relevant tune event based on the message's event name + const tuneEvent = tuneEvents.find((event) => event.eventName === messageEvent); + + if (tuneEvent) { + const standardHashMap = getHashFromArray(tuneEvent.standardMapping, 'from', 'to', false); + const advSubIdHashMap = getHashFromArray(tuneEvent.advSubIdMapping, 'from', 'to', false); + const advUniqueIdHashMap = getHashFromArray(tuneEvent.advUniqueIdMapping, 'from', 'to', false); + + const params = { + ...mapPropertiesWithNestedSupport(message, properties, standardHashMap), + ...mapPropertiesWithNestedSupport(message, properties, advSubIdHashMap), + ...mapPropertiesWithNestedSupport(message, properties, advUniqueIdHashMap), + }; + + // Prepare the response + const response = defaultRequestConfig(); + response.params = params; // Set only the mapped params + response.endpoint = tuneEvent.url; // Use the user-defined URL + + return response; + } + + throw new InstrumentationError('No matching tune event found for the provided event.', 400); +}; + +const processEvent = (message, destination) => { + // Validate message type + if (!isDefinedAndNotNull(message.type) || typeof message.type !== 'string') { + throw new InstrumentationError( + 'Message Type is not present or is not a string. Aborting message.', + 400, + ); + } + const messageType = message.type.toLowerCase(); + + // Initialize response variable + let response; + + // Process 'track' messages using the responseBuilder + if (messageType === 'track') { + response = responseBuilder(message, destination); + } else { + throw new InstrumentationError('Message type not supported. Only "track" is allowed.', 400); + } + + return response; +}; + +const process = (event) => processEvent(event.message, event.destination); + +const processRouterDest = async (inputs, reqMetadata) => { + const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); + return respList; +}; + +module.exports = { + process, + processRouterDest, +}; diff --git a/test/integrations/destinations/tune/processor/data.ts b/test/integrations/destinations/tune/processor/data.ts new file mode 100644 index 0000000000..aa818d9b83 --- /dev/null +++ b/test/integrations/destinations/tune/processor/data.ts @@ -0,0 +1,3 @@ +import { trackTestdata } from './trackTestData'; + +export const data = [...trackTestdata]; diff --git a/test/integrations/destinations/tune/processor/trackTestData.ts b/test/integrations/destinations/tune/processor/trackTestData.ts new file mode 100644 index 0000000000..c19ffdd84c --- /dev/null +++ b/test/integrations/destinations/tune/processor/trackTestData.ts @@ -0,0 +1,228 @@ +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; +import { + generateMetadata, + generateSimplifiedTrackPayload, + overrideDestination, + transformResultBuilder, +} from '../../../testUtils'; + +const destination: Destination = { + ID: '123', + Name: 'tune', + DestinationDefinition: { + ID: '123', + Name: 'tune', + DisplayName: 'tune', + Config: {}, + }, + Config: { + connectionMode: { + web: 'cloud', + }, + consentManagement: {}, + oneTrustCookieCategories: {}, + ketchConsentPurposes: {}, + tuneEvents: [ + { + url: 'https://demo.go2cloud.org/aff_l?offer_id=45&aff_id=1029', + eventName: 'Product added', + standardMapping: [ + { to: 'aff_id', from: 'affId' }, + { to: 'promo_code', from: 'promoCode' }, + { to: 'security_token', from: 'securityToken' }, + { to: 'status', from: 'status' }, + { to: 'transaction_id', from: 'mytransactionId' }, + ], + advSubIdMapping: [{ from: 'context.traits.ip', to: 'adv_sub2' }], + advUniqueIdMapping: [{ from: 'context.traits.customProperty1', to: 'adv_unique1' }], + }, + ], + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +export const trackTestdata: ProcessorTestData[] = [ + { + id: 'Test 0', + name: 'tune', + description: 'Track call with standard properties mapping', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200 and correctly map the properties to the specified parameters.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Product added', + properties: { + securityToken: '1123', + mytransactionId: 'test-123', + }, + context: { + traits: { + customProperty1: 'customValue', + firstName: 'David', + logins: 2, + ip: '0.0.0.0', + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + endpoint: 'https://demo.go2cloud.org/aff_l?offer_id=45&aff_id=1029', + event: 'Product added', + headers: {}, + params: { + security_token: '1123', + transaction_id: 'test-123', + adv_sub2: '0.0.0.0', + adv_unique1: 'customValue', + }, + userId: '', + JSON: {}, + }), + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'Test 1', + name: 'tune', + description: 'Test case for handling a missing tune event for a given event name', + scenario: 'Business', + successCriteria: + 'The response should return a 400 status code with an appropriate error message indicating no matching tune event was found.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateSimplifiedTrackPayload({ + type: 'track', + event: 'Purchase event', + properties: { + securityToken: '1123', + mytransactionId: 'test-123', + }, + context: { + traits: { + customProperty1: 'customValue', + firstName: 'David', + logins: 2, + }, + }, + anonymousId: 'david_bowie_anonId', + }), + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: 'No matching tune event found for the provided event.', + statTags: { + destType: 'TUNE', + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'native', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + metadata: generateMetadata(1), + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'Test 2', + name: 'tune', + description: 'Incorrect message type', + scenario: 'Business', + successCriteria: 'The response should return a 400 status code due to invalid message type.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'abc', + event: 'Product added', + properties: { + securityToken: '1123', + mytransactionId: 'test-123', + }, + context: { + traits: { + customProperty1: 'customValue', + firstName: 'David', + logins: 2, + }, + }, + anonymousId: 'david_bowie_anonId', + }, + metadata: generateMetadata(1), + destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: 'Message type not supported. Only "track" is allowed.', + statTags: { + destType: 'TUNE', + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'native', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + metadata: generateMetadata(1), + statusCode: 400, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/tune/router/data.ts b/test/integrations/destinations/tune/router/data.ts new file mode 100644 index 0000000000..65bfee4ade --- /dev/null +++ b/test/integrations/destinations/tune/router/data.ts @@ -0,0 +1,190 @@ +import { Destination } from '../../../../../src/types'; +import { RouterTestData } from '../../../testTypes'; +import { generateMetadata } from '../../../testUtils'; + +const destination: Destination = { + ID: '123', + Name: 'tune', + DestinationDefinition: { + ID: '123', + Name: 'tune', + DisplayName: 'tune', + Config: {}, + }, + Config: { + connectionMode: { + web: 'cloud', + }, + consentManagement: {}, + oneTrustCookieCategories: {}, + ketchConsentPurposes: {}, + tuneEvents: [ + { + url: 'https://demo.go2cloud.org/aff_l?offer_id=45&aff_id=1029', + eventName: 'Product added', + standardMapping: [ + { to: 'aff_id', from: 'affId' }, + { to: 'promo_code', from: 'promoCode' }, + { to: 'security_token', from: 'securityToken' }, + { to: 'status', from: 'status' }, + { to: 'transaction_id', from: 'mytransactionId' }, + ], + advSubIdMapping: [{ from: 'context.ip', to: 'adv_sub2' }], + advUniqueIdMapping: [{ from: 'context.traits.anonymousId', to: 'adv_unique1' }], + }, + ], + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +export const data: RouterTestData[] = [ + { + id: 'tune-router-test-1', + name: 'tune', + description: 'Basic Router Test for track call with standard properties mapping.', + scenario: 'Business', + successCriteria: + 'The response should have a status code of 200, and the output should correctly map the properties.', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + metadata: generateMetadata(1), + message: { + type: 'track', + event: 'Product added', + anonymousId: 'sampath', + channel: 'web', + context: { + app: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + ip: '0.0.0.0', + traits: { anonymousId: 'sampath', email: 'sampath@gmail.com' }, + }, + integrations: { All: true }, + properties: { + securityToken: '1123', + mytransactionId: 'test-123', + }, + }, + }, + ], + destType: 'tune', + }, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://demo.go2cloud.org/aff_l?offer_id=45&aff_id=1029', + headers: {}, + params: { + security_token: '1123', + transaction_id: 'test-123', + adv_sub2: '0.0.0.0', + adv_unique1: 'sampath', + }, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1)], + batched: false, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + }, + { + id: 'tune-router-test-2', + name: 'tune', + description: 'Basic Router Test with incorrect message type ', + scenario: 'Business', + successCriteria: + 'The response should return a 400 status code due to an invalid message type, with an appropriate error message indicating that the message type is not present or is not a string.', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + metadata: generateMetadata(1), + message: { + type: 123, + event: 'Product added', + anonymousId: 'sampath', + channel: 'web', + context: { + app: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + ip: '0.0.0.0', + traits: { anonymousId: 'sampath', email: 'sampath@gmail.com' }, + }, + integrations: { All: true }, + properties: { + securityToken: '1123', + mytransactionId: 'test-123', + }, + }, + }, + ], + destType: 'tune', + }, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + error: 'Message Type is not present or is not a string. Aborting message.', + statTags: { + destType: 'TUNE', + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'router', + implementation: 'native', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + metadata: [generateMetadata(1)], + batched: false, + statusCode: 400, + destination, + }, + ], + }, + }, + }, + }, +];