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 topsort destination #3913

Merged
merged 15 commits into from
Dec 13, 2024
30 changes: 30 additions & 0 deletions src/v0/destinations/topsort/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { getMappingConfig } = require('../../util');

const BASE_URL = 'https://api.topsort.com/v2/events';

const ConfigCategories = {
TRACK: {
type: 'track',
name: 'TopsortTrackConfig',
},
PLACEMENT: { name: 'TopsortPlacementConfig' },
ITEM: { name: 'TopsortItemConfig' },
};

const ECOMM_EVENTS_WITH_PRODUCT_ARRAY = [
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
'Product Clicked',
'Product added',
'Product Viewed',
'Product Removed',
'Checkout Started',
'Payment Info Entered',
];

const mappingConfig = getMappingConfig(ConfigCategories, __dirname);

module.exports = {
mappingConfig,
ConfigCategories,
BASE_URL,
ECOMM_EVENTS_WITH_PRODUCT_ARRAY,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[
{
"destKey": "productId",
"sourceKeys": ["productId", "properties.productId"]
},
{
"destKey": "unitPrice",
"sourceKeys": ["price", "properties.price"],
"metadata": {
"type": "toNumber"
}
},
{
"destKey": "quantity",
"sourceKeys": ["quantity", "properties.quantity"]
},
{
"destKey": "vendorId",
"sourceKeys": "properties.vendorId"
}
]
12 changes: 12 additions & 0 deletions src/v0/destinations/topsort/data/TopsortItemConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"destKey": "properties.position",
"sourceKeys": "position",
"required": false
},
{
"destKey": "properties.product_id",
"sourceKeys": "productId",
"required": false
}
]
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
30 changes: 30 additions & 0 deletions src/v0/destinations/topsort/data/TopsortPlacementConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"destKey": "path",
"sourceKeys": "context.page.path",
"required": true
},
{
"destKey": "searchQuery",
"sourceKeys": "properties.query",
"required": false
},
{
"destKey": "page",
"sourceKeys": "properties.pageNumber",
"required": false
},
{
"destKey": "pageSize",
"sourceKeys": "properties.pageSize",
"required": false
},
{
"destKey": "categoryIds",
"sourceKeys": "properties.category_id",
"required": false,
"metadata": {
"toArray": true
}
}
]
30 changes: 30 additions & 0 deletions src/v0/destinations/topsort/data/TopsortTrackConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"destKey": "occurredAt",
"sourceKeys": ["originalTimestamp", "timestamp"],
"metadata": {
"type": "timestamp"
},
"required": true
},
{
"destKey": "anonymousId",
"sourceKeys": "opaqueUserId",
"required": true
},
{
"destKey": "resolvedBidId",
"sourceKeys": "properties.resolvedBidId",
"required": false
},
{
"destKey": "entity",
"sourceKeys": "properties.entity",
"required": false
},
{
"destKey": "additionalAttribution",
"sourceKeys": "properties.additionalAttribution",
"required": false
}
]
132 changes: 132 additions & 0 deletions src/v0/destinations/topsort/transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const {
InstrumentationError,
ConfigurationError,
getHashFromArrayWithDuplicate,
} = require('@rudderstack/integrations-lib');
const { ConfigCategory, mappingConfig, ECOMM_EVENTS_WITH_PRODUCT_ARRAY } = require('./config');
const { constructPayload, simpleProcessRouterDest } = require('../../util');
const {
constructItemPayloads,
createEventData,
isProductArrayValid,
getMappedEventName,
addFinalPayload,
} = require('./utils');

// Function to process events with a product array
const processProductArray = (
products,
basePayload,
placementPayload,
topsortEvent,
apiKey,
finalPayloads,
) => {
const itemPayloads = constructItemPayloads(products, mappingConfig[ConfigCategory.ITEM.name]);
itemPayloads.forEach((itemPayload) => {
const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent);
addFinalPayload(eventData, apiKey, finalPayloads);

Check warning on line 28 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L24-L28

Added lines #L24 - L28 were not covered by tests
});
};

// Function to process events with a single product or no product data
const processSingleProduct = (
basePayload,
placementPayload,
message,
topsortEvent,
apiKey,
finalPayloads,
messageId,
) => {
const itemPayload = constructPayload(message, mappingConfig[ConfigCategory.ITEM.name]);
const eventData = createEventData(basePayload, placementPayload, itemPayload, topsortEvent);

Check warning on line 43 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L41-L43

Added lines #L41 - L43 were not covered by tests

// Ensure messageId is used instead of generating a UUID for single product events
eventData.data.id = messageId;

Check warning on line 46 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L46

Added line #L46 was not covered by tests

// Add final payload with appropriate ID and other headers
addFinalPayload(eventData, apiKey, finalPayloads);

Check warning on line 49 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L49

Added line #L49 was not covered by tests
};

const responseBuilder = (message, { Config }) => {
const { apiKey, topsortEvents } = Config;
const { event, properties } = message;

// Parse Topsort event mappings
const parsedTopsortEventMappings = getHashFromArrayWithDuplicate(topsortEvents);
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
const mappedEventName = getMappedEventName(parsedTopsortEventMappings, event);

if (!mappedEventName) {
throw new InstrumentationError("Event not mapped in 'topsortEvents'. Dropping the event.");

Check warning on line 61 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L61

Added line #L61 was not covered by tests
}

const topsortEvent = mappedEventName;
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved

// Construct base and placement payloads
const basePayload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]);
const placementPayload = constructPayload(message, mappingConfig[ConfigCategory.PLACEMENT.name]);

Check warning on line 68 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L68

Added line #L68 was not covered by tests

// Check if the event involves a product array (using ECOMM_EVENTS_WITH_PRODUCT_ARRAY)
const isProductArrayAvailable =
ECOMM_EVENTS_WITH_PRODUCT_ARRAY.includes(event) && isProductArrayValid(event, properties);

const finalPayloads = [];

Check warning on line 74 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L74

Added line #L74 was not covered by tests

// Handle events based on the presence of a product array
if (isProductArrayAvailable) {
processProductArray(

Check warning on line 78 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L78

Added line #L78 was not covered by tests
properties.products,
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
basePayload,
placementPayload,
topsortEvent,
apiKey,
finalPayloads,
);
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
} else {
processSingleProduct(

Check warning on line 87 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L86-L87

Added lines #L86 - L87 were not covered by tests
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved
basePayload,
placementPayload,
message,
topsortEvent,
apiKey,
finalPayloads,
);
}
aanshi07 marked this conversation as resolved.
Show resolved Hide resolved

return finalPayloads;

Check warning on line 97 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L97

Added line #L97 was not covered by tests
};

const processEvent = (message, destination) => {
// Check for missing API Key or missing Advertiser ID
if (!destination.Config.apiKey) {
throw new ConfigurationError('API Key is missing. Aborting message.', 400);

Check warning on line 103 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L103

Added line #L103 was not covered by tests
}
if (!message.type) {
throw new InstrumentationError('Message Type is missing. Aborting message.', 400);

Check warning on line 106 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L106

Added line #L106 was not covered by tests
}

const messageType = message.type.toLowerCase();

let response;
// Handle 'track' event type
if (messageType === 'track') {
response = responseBuilder(message, destination); // Call responseBuilder to handle the event
} else {
throw new InstrumentationError('Only "track" events are supported. Dropping event.', 400);

Check warning on line 116 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L115-L116

Added lines #L115 - L116 were not covered by tests
}

return response;

Check warning on line 119 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L119

Added line #L119 was not covered by tests
};

// Process function that is called per event
const process = (event) => processEvent(event.message, event.destination);

// Router destination handler to process a batch of events
const processRouterDest = async (inputs, reqMetadata) => {
// Process all events through the simpleProcessRouterDest utility
const respList = await simpleProcessRouterDest(inputs, process, reqMetadata);
return respList;

Check warning on line 129 in src/v0/destinations/topsort/transform.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/transform.js#L128-L129

Added lines #L128 - L129 were not covered by tests
};

module.exports = { process, processRouterDest };
61 changes: 61 additions & 0 deletions src/v0/destinations/topsort/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const { generateUUID } = require('@rudderstack/integrations-lib');
const { constructPayload } = require('../../util');
const { BASE_URL } = require('./config');

// Function to check if a product array is valid
const isProductArrayValid = (event, properties) =>
Array.isArray(properties?.products) && properties?.products.length > 0;

// Function to construct item payloads for each product
const constructItemPayloads = (products, mappingConfig) =>
products.map((product) => constructPayload(product, mappingConfig));

Check warning on line 11 in src/v0/destinations/topsort/utils.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/utils.js#L11

Added line #L11 was not covered by tests

// Function to create a single event data structure
const createEventData = (basePayload, placementPayload, itemPayload, event) => ({
data: {
...basePayload,
placement: {
...placementPayload,
...itemPayload,
},
id: generateUUID(),
},
event,
});

// Function to add the structured event data to the final payloads array
const addFinalPayload = (eventData, apiKey, finalPayloads) => {
finalPayloads.push({

Check warning on line 28 in src/v0/destinations/topsort/utils.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/utils.js#L28

Added line #L28 was not covered by tests
...eventData,
endpoint: BASE_URL, // Set the destination API URL
headers: {
'Content-Type': 'application/json',
api_key: apiKey, // Add the API key here for authentication
},
});
};

// Function to retrieve mapped event name from Topsort event mappings.
const getMappedEventName = (parsedTopsortEventMappings, event) => {
const mappedEventNames = parsedTopsortEventMappings[event];

// Check if mapping exists
if (!mappedEventNames) {
throw new Error(`Event '${event}' not found in Topsort event mappings`);

Check warning on line 44 in src/v0/destinations/topsort/utils.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/utils.js#L44

Added line #L44 was not covered by tests
}

// If there are multiple mappings, pick the first one or apply your logic
if (Array.isArray(mappedEventNames)) {
return mappedEventNames[0]; // Return the first mapping

Check warning on line 49 in src/v0/destinations/topsort/utils.js

View check run for this annotation

Codecov / codecov/patch

src/v0/destinations/topsort/utils.js#L49

Added line #L49 was not covered by tests
}

return mappedEventNames; // Return the single mapping if not an array
};

module.exports = {
isProductArrayValid,
constructItemPayloads,
createEventData,
addFinalPayload,
getMappedEventName,
};
8 changes: 8 additions & 0 deletions src/v0/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@
strictMultiMap,
validateTimestamp,
allowedKeyCheck,
toArray,
} = metadata;

// if value is null and defaultValue is supplied - use that
Expand Down Expand Up @@ -1037,6 +1038,13 @@
}
}

if (toArray) {
if (Array.isArray(formattedVal)) {
return formattedVal;

Check warning on line 1043 in src/v0/util/index.js

View check run for this annotation

Codecov / codecov/patch

src/v0/util/index.js#L1043

Added line #L1043 was not covered by tests
}
return [formattedVal];

Check warning on line 1045 in src/v0/util/index.js

View check run for this annotation

Codecov / codecov/patch

src/v0/util/index.js#L1045

Added line #L1045 was not covered by tests
}

return formattedVal;
};

Expand Down
3 changes: 3 additions & 0 deletions test/integrations/destinations/topsort/processor/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { trackTestdata } from './trackTestData';

export const data = [...trackTestdata];
Loading
Loading