Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: onboard tune destination #3795

Merged
merged 11 commits into from
Oct 22, 2024
97 changes: 97 additions & 0 deletions src/v0/destinations/tune/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const get = require('get-value');
const { InstrumentationError } = require('@rudderstack/integrations-lib');
const { defaultRequestConfig, simpleProcessRouterDest, getHashFromArray } = require('../../util');

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);
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved

if (tuneEvent) {
const standardHashMap = getHashFromArray(tuneEvent.standardMapping);
const advSubIdHashMap = getHashFromArray(tuneEvent.advSubIdMapping);
const advUniqueIdHashMap = getHashFromArray(tuneEvent.advUniqueIdMapping);

const mapPropertiesWithNestedSupport = (msg, mappings) => {
const newParams = {}; // Create a new object for parameters
Object.entries(mappings).forEach(([key, value]) => {
let data; // Declare data variable

if (key.split('.').length > 1) {
// Handle nested keys
data = get(msg, key); // Use `get` to retrieve nested data

Check warning on line 24 in src/v0/destinations/tune/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/tune/transform.js#L24

Added line #L24 was not covered by tests
if (data) {
newParams[value] = data; // Map to the corresponding destination key

Check warning on line 26 in src/v0/destinations/tune/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/tune/transform.js#L26

Added line #L26 was not covered by tests
}
} else {
// Handle non-nested keys
data = get(properties, key); // Retrieve data from properties directly
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
if (data) {
newParams[value] = data; // Map to the corresponding destination key
}
}
});
return newParams; // Return the new params object
};
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved

const params = {
...mapPropertiesWithNestedSupport(message, standardHashMap),
...mapPropertiesWithNestedSupport(message, advSubIdHashMap),
...mapPropertiesWithNestedSupport(message, advUniqueIdHashMap),
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
};

// Prepare the response
const response = defaultRequestConfig();
response.params = params; // Set only the mapped params
response.endpoint = tuneEvent.url; // Use the user-defined URL

// Add query parameters from the URL to params
const urlParams = new URLSearchParams(new URL(tuneEvent.url).search);
urlParams.forEach((value, key) => {
params[key] = value; // Add each query parameter to params
});
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved

// Include the event name in the response, not in params
response.event = tuneEvent.eventName;

return response;
}

throw new InstrumentationError('No matching tune event found for the provided event.', 400);

Check warning on line 62 in src/v0/destinations/tune/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/tune/transform.js#L62

Added line #L62 was not covered by tests
};

const processEvent = (message, destination) => {
// Validate message type
if (!message.type) {
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
throw new InstrumentationError('Message Type is not present. Aborting message.', 400);
}
const messageType = message.type.toLowerCase();
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved

// 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);
yashasvibajpai marked this conversation as resolved.
Show resolved Hide resolved
}

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 = {
responseBuilder,
processEvent,
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
process,
processRouterDest,
};
177 changes: 177 additions & 0 deletions src/v0/destinations/tune/transform.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
const { InstrumentationError } = require('@rudderstack/integrations-lib');
const { responseBuilder, processEvent } = require('./transform');

describe('responseBuilder', () => {
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
// Correctly maps properties to params using standard, advSubId, and advUniqueId mappings
it('should construct response with default config when tune event found', () => {
const message = {
properties: { key1: 'value1', key2: 'value2' },
event: 'testEvent',
};
const Config = {
tuneEvents: [
{
eventName: 'testEvent',
standardMapping: [{ from: 'key1', to: 'mappedKey1' }],
advSubIdMapping: [{ from: 'key2', to: 'mappedKey2' }],
advUniqueIdMapping: [{ from: 'key3', to: 'mappedKey3' }],
url: 'https://example.com/api',
},
],
};

const response = responseBuilder(message, { Config });

expect(response).toEqual({
version: '1',
type: 'REST',
method: 'POST',
endpoint: 'https://example.com/api',
headers: {},
params: { mappedKey1: 'value1', mappedKey2: 'value2' },
body: {
JSON: {},
JSON_ARRAY: {},
XML: {},
FORM: {},
},
files: {},
event: 'testEvent',
});
});

it('should extract and use tuneEvents from Config', () => {
const message = {
properties: { key1: 'value1', key2: 'value2' },
event: 'testEvent',
};
const Config = {
tuneEvents: [
{
eventName: 'testEvent',
standardMapping: [{ from: 'key1', to: 'mappedKey1' }],
advSubIdMapping: [{ from: 'key2', to: 'mappedKey2' }],
advUniqueIdMapping: [{ from: 'key3', to: 'mappedKey3' }],
url: 'https://example.com/api',
},
],
};

const response = responseBuilder(message, { Config });

expect(response).toBeDefined();
});

it('should extract parameters from the provided URL', () => {
const message = {
event: 'testEvent',
};
const Config = {
tuneEvents: [
{
eventName: 'testEvent',
standardMapping: [{ from: 'key1', to: 'mappedKey1' }],
advSubIdMapping: [{ from: 'key2', to: 'mappedKey2' }],
advUniqueIdMapping: [{ from: 'key3', to: 'mappedKey3' }],
url: 'https://demo.go2cloud.org/aff_l?offer_id=45&aff_id=1029',
},
],
};

const response = responseBuilder(message, { Config });

expect(response).toBeDefined();
expect(response.endpoint).toEqual('https://demo.go2cloud.org/aff_l?offer_id=45&aff_id=1029');
expect(response.params).toEqual({
offer_id: '45',
aff_id: '1029',
});
});
});

describe('processEvent', () => {
// Processes 'track' messages correctly using responseBuilder
it('should process "track" messages correctly using responseBuilder', () => {
const message = {
type: 'track',
event: 'product list viewed',
properties: {
platform: 'meta',
conversions: 1,
ad_unit_id: 221187,
ad_interaction_time: '1652826278',
},
};
const destination = {
Config: {
tuneEvents: [
{
eventName: 'product list viewed',
url: 'https://example.com/track',
eventsMapping: [
{ from: 'platform', to: 'platform_key' },
{ from: 'conversions', to: 'conversions_key' },
],
},
],
},
};
const expectedResponse = {
body: {
FORM: {},
JSON: {},
JSON_ARRAY: {},
XML: {},
},
params: {},
endpoint: 'https://example.com/track',
event: 'product list viewed',
files: {},
headers: {},
method: 'POST',
type: 'REST',
version: '1',
};
const response = processEvent(message, destination);
expect(response).toEqual(expectedResponse);
});

// Throws an error if message type is missing
it('should throw an error if message type is missing', () => {
const message = {
event: 'product list viewed',
properties: {
platform: 'meta',
conversions: 1,
},
};
const destination = {
Config: {
tuneEvents: [],
},
};
expect(() => processEvent(message, destination)).toThrowError(
new InstrumentationError('Message Type is not present. Aborting message.', 400),
);
});

it('should throw an error when message type is not "track"', () => {
const message = {
type: 'identify',
event: 'product list viewed',
properties: {
platform: 'meta',
conversions: 1,
},
};
const destination = {
Config: {
tuneEvents: [],
},
};

expect(() => processEvent(message, destination)).toThrowError(
new InstrumentationError('Message type not supported. Only "track" is allowed.', 400),
);
});
});
Loading
Loading