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 koddi destination #3359

Merged
merged 11 commits into from
May 23, 2024
39 changes: 39 additions & 0 deletions src/cdk/v2/destinations/koddi/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const { getMappingConfig } = require('../../../../v0/util');

/**
* ref :- https://developers.koddi.com/reference/winning-ads
* impressions - https://developers.koddi.com/reference/impressions-1
* clicks - https://developers.koddi.com/reference/clicks-1
* conversions - https://developers.koddi.com/reference/conversions-1
*/
const EVENT_TYPES = {
IMPRESSIONS: 'impressions',
CLICKS: 'clicks',
CONVERSIONS: 'conversions',
};

const CONFIG_CATEGORIES = {
manish339k marked this conversation as resolved.
Show resolved Hide resolved
IMPRESSIONS: {
type: 'track',
name: 'ImpressionsConfig',
},
CLICKS: {
type: 'track',
name: 'ClicksConfig',
},
CONVERSIONS: {
type: 'track',
name: 'ConversionsConfig',
},
};

const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname);

module.exports = {
EVENT_TYPES,
CONFIG_CATEGORIES,
MAPPING_CONFIG,
IMPRESSIONS_CONFIG: MAPPING_CONFIG[CONFIG_CATEGORIES.IMPRESSIONS.name],
CLICKS_CONFIG: MAPPING_CONFIG[CONFIG_CATEGORIES.CLICKS.name],
CONVERSIONS_CONFIG: MAPPING_CONFIG[CONFIG_CATEGORIES.CONVERSIONS.name],
};
35 changes: 35 additions & 0 deletions src/cdk/v2/destinations/koddi/data/ClicksConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[
{
"sourceKeys": "properties.tracking_data",
"required": true,
"destKey": "trackingData"
},
{
"sourceKeys": "properties.rank",
"required": true,
"destKey": "rank"
},
{
"sourceKeys": "properties.beacon_issued",
"required": true,
"destKey": "beaconIssued"
},
{
"sourceKeys": "userId",
"sourceFromGenericMap": true,
"required": true,
"destKey": "userGuid"
},
{
"sourceKeys": "properties.test_version_override",
manish339k marked this conversation as resolved.
Show resolved Hide resolved
"destKey": "testVersionOverride"
},
{
"sourceKeys": "properties.destination_url",
manish339k marked this conversation as resolved.
Show resolved Hide resolved
"destKey": "destinationUrl"
},
{
"sourceKeys": "properties.overrides",
"destKey": "overrides"
}
]
53 changes: 53 additions & 0 deletions src/cdk/v2/destinations/koddi/data/ConversionsConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[
{
"sourceKeys": "context.page.referring_domain",
"destKey": "domain"
},
{
"sourceKeys": "context.locale",
"required": true,
"destKey": "culture"
},
{
"sourceKeys": "properties.currency",
"required": true,
"destKey": "currency"
},
{
"sourceKeys": ["context.ip", "request_ip"],
"destKey": "user_ip"
},
{
"sourceKeys": "context.userAgent",
"destKey": "user_agent"
},
{
"sourceKeys": "userId",
"sourceFromGenericMap": true,
"required": true,
"destKey": "user_guid"
},
{
"sourceKeys": "context.device.type",
"destKey": "device_type"
},
{
"sourceKeys": ["properties.order_id", "properties.transaction_id"],
"required": true,
"destKey": "transaction_id"
},
{
"sourceKeys": "properties.conversion_source",
"destKey": "conversion_source"
},
{
"sourceKeys": "timestamp",
"sourceFromGenericMap": true,
"destKey": "unixtime"
},
{
"sourceKeys": "properties.bidders",
"required": true,
"destKey": "bidders"
}
]
22 changes: 22 additions & 0 deletions src/cdk/v2/destinations/koddi/data/ImpressionsConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"sourceKeys": "properties.tracking_data",
"required": true,
"destKey": "trackingData"
},
{
"sourceKeys": "properties.rank",
"required": true,
"destKey": "rank"
},
{
"sourceKeys": "properties.beacon_issued",
"required": true,
"destKey": "beaconIssued"
},
{
"sourceKeys": "timestamp",
"sourceFromGenericMap": true,
"destKey": "ts"
}
]
33 changes: 33 additions & 0 deletions src/cdk/v2/destinations/koddi/procWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
bindings:
- name: EventType
path: ../../../../constants
- path: ../../bindings/jsontemplate
- name: removeUndefinedAndNullValues
path: ../../../../v0/util
- path: ./utils
- path: ./config

steps:
- name: messageType
template: |
.message.type.toLowerCase();
- name: eventType
template: |
.message.integrations.koddi.eventType.toLowerCase();
- name: validateInput
template: |
let messageType = $.outputs.messageType;
let eventType = $.outputs.eventType;
$.assert(messageType, "message Type is not present. Aborting message.");
$.assert(messageType in {{$.EventType.([.TRACK])}}, "message type " + messageType + " is not supported");
$.assert(eventType in {{$.EVENT_TYPES.([.IMPRESSIONS, .CLICKS, .CONVERSIONS])}}, "event type " + eventType + " is not supported");
$.assertConfig(.destination.Config.apiBaseUrl, "API Base URL is not present. Aborting");
$.assertConfig(.destination.Config.clientName, "Client Name is not present. Aborting");
- name: preparePayload
template: |
const payload = $.constructFullPayload($.outputs.eventType, .message, .destination.Config);
$.context.payload = $.removeUndefinedAndNullValues(payload);
- name: buildResponse
template: |
const response = $.constructResponse($.outputs.eventType, .destination.Config, $.context.payload);
response
31 changes: 31 additions & 0 deletions src/cdk/v2/destinations/koddi/rtWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
bindings:
- name: handleRtTfSingleEventError
path: ../../../../v0/util/index

steps:
- name: validateInput
template: |
$.assert(Array.isArray(^) && ^.length > 0, "Invalid event array")

- name: transform
externalWorkflow:
path: ./procWorkflow.yaml
loopOverInput: true

- name: successfulEvents
template: |
$.outputs.transform#idx.output.({
"batchedRequest": .,
"batched": false,
"destination": ^[idx].destination,
"metadata": ^[idx].metadata[],
"statusCode": 200
})[]
- name: failedEvents
template: |
$.outputs.transform#idx.error.(
$.handleRtTfSingleEventError(^[idx], .originalError ?? ., {})
)[]
- name: finalPayload
template: |
[...$.outputs.successfulEvents, ...$.outputs.failedEvents]
116 changes: 116 additions & 0 deletions src/cdk/v2/destinations/koddi/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const { InstrumentationError } = require('@rudderstack/integrations-lib');
const { EVENT_TYPES, IMPRESSIONS_CONFIG, CLICKS_CONFIG, CONVERSIONS_CONFIG } = require('./config');
const {
constructPayload,
defaultRequestConfig,
toUnixTimestamp,
stripTrailingSlash,
} = require('../../../../v0/util');

const validateBidders = (bidders) => {
if (!Array.isArray(bidders)) {
throw new InstrumentationError('properties.bidders should be an array of objects. Aborting.');
}
if (bidders.length === 0) {
throw new InstrumentationError(
'properties.bidders should contains at least one bidder. Aborting.',
);
}
bidders.forEach((bidder) => {
if (!(bidder.bidder || bidder.alternate_bidder)) {
throw new InstrumentationError('bidder or alternate_bidder is not present. Aborting.');
}
if (!bidder.count) {
throw new InstrumentationError('count is not present. Aborting.');
}
if (!bidder.base_price) {
throw new InstrumentationError('base_price is not present. Aborting.');
}
});
};

/**
* This function constructs payloads based upon mappingConfig for all calls.
* @param {*} eventType
* @param {*} message
* @param {*} Config
* @returns
*/
const constructFullPayload = (eventType, message, Config) => {
let payload;
switch (eventType) {
case EVENT_TYPES.IMPRESSIONS:
payload = constructPayload(message, IMPRESSIONS_CONFIG);
payload.clientName = Config.clientName;
break;
case EVENT_TYPES.CLICKS:
payload = constructPayload(message, CLICKS_CONFIG);
payload.clientName = Config.clientName;
if (!Config.testVersionOverride) {
payload.testVersionOverride = null;
manish339k marked this conversation as resolved.
Show resolved Hide resolved
}
if (!Config.overrides) {
payload.overrides = null;
}
break;
case EVENT_TYPES.CONVERSIONS:
payload = constructPayload(message, CONVERSIONS_CONFIG);
payload.client_name = Config.clientName;
payload.unixtime = toUnixTimestamp(payload.unixtime);
manish339k marked this conversation as resolved.
Show resolved Hide resolved
validateBidders(payload.bidders);
break;
default:
throw new InstrumentationError(`event type ${eventType} is not supported.`);
}
return payload;
};

const getEndpoint = (eventType, Config) => {
let endpoint = stripTrailingSlash(Config.apiBaseUrl);
switch (eventType) {
case EVENT_TYPES.IMPRESSIONS:
endpoint += '?action=impression';
break;
case EVENT_TYPES.CLICKS:
endpoint += '?action=click';
break;
case EVENT_TYPES.CONVERSIONS:
endpoint += '/conversion';
break;
default:
throw new InstrumentationError(`event type ${eventType} is not supported.`);
}
return endpoint;
};

/**
* This function constructs response based upon event.
* @param {*} eventType
* @param {*} Config
* @param {*} payload
* @returns
*/
const constructResponse = (eventType, Config, payload) => {
if (!Object.values(EVENT_TYPES).includes(eventType)) {
throw new InstrumentationError(`event type ${eventType} is not supported.`);
}
const response = defaultRequestConfig();
response.endpoint = getEndpoint(eventType, Config);
response.headers = {
accept: 'application/json',
};
if (eventType === EVENT_TYPES.CONVERSIONS) {
response.body.JSON = payload;
response.method = 'POST';
response.headers = {
...response.headers,
'content-type': 'application/json',
};
} else {
response.params = payload;
response.method = 'GET';
}
return response;
};

module.exports = { getEndpoint, validateBidders, constructFullPayload, constructResponse };
Loading
Loading