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 new destination ninetailed #3106

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

const ConfigCategories = {
GENERAL: {
type: 'general',
name: 'generalPayloadMapping',
},
CONTEXT: {
type: 'context',
name: 'contextMapping',
},
TRACK: {
type: 'track',
name: 'trackMapping',
},
IDENTIFY: {
type: 'identify',
name: 'identifyMapping',
},
PAGE: {
type: 'page',
name: 'pageMapping',
},
};

// MAX_BATCH_SIZE : // Maximum number of events to send in a single batch
const mappingConfig = getMappingConfig(ConfigCategories, __dirname);
const batchEndpoint =
'https://experience.ninetailed.co/v2/organizations/{{organisationId}}/environments/{{environment}}/events';

module.exports = { ConfigCategories, mappingConfig, batchEndpoint, MAX_BATCH_SIZE: 200 };
43 changes: 43 additions & 0 deletions src/cdk/v2/destinations/ninetailed/data/contextMapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[
{
"sourceKeys": "app.name",
"required": true,
"destKey": "app.name"
},
{
"sourceKeys": "app.version",
"required": true,
"destKey": "app.version"
},
{
"sourceKeys": "campaign",
"destKey": "campaign"
},
{
"sourceKeys": "library.name",
"required": true,
"destKey": "library.name"
},
{
"sourceKeys": "library.version",
"required": true,
"destKey": "library.version"
},
{
"sourceKeys": "locale",
"destKey": "locale"
},
{
"sourceKeys": "page",
"destKey": "page"
},
{
"sourceKeys": "userAgent",
"destKey": "userAgent"
},
{
"sourceKeys": "location",
"required": true,
"destKey": "location"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
{
"sourceKeys": "anonymousId",
"required": true,
"destKey": "anonymousId"
},
{
"sourceKeys": "messageId",
"required": true,
"destKey": "messageId"
},
{
"sourceKeys": "channel",
"required": true,
"destKey": "channel"
},
{
"sourceKeys": "type",
"destKey": "type"
},
{
"sourceKeys": "originalTimestamp",
"destKey": "originalTimestamp"
}
]
14 changes: 14 additions & 0 deletions src/cdk/v2/destinations/ninetailed/data/identifyMapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"sourceKeys": "traits",
"sourceFromGenericMap": true,
"required": true,
"destKey": "traits"
},
{
"sourceKeys": "userIdOnly",
"sourceFromGenericMap": true,
"required": true,
"destKey": "userId"
}
]
7 changes: 7 additions & 0 deletions src/cdk/v2/destinations/ninetailed/data/pageMapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"sourceKeys": "properties",
"required": true,
"destKey": "properties"
}
]
12 changes: 12 additions & 0 deletions src/cdk/v2/destinations/ninetailed/data/trackMapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"sourceKeys": "properties",
"required": true,
"destKey": "properties"
},
{
"sourceKeys": "event",
"required": true,
"destKey": "event"
}
]
33 changes: 33 additions & 0 deletions src/cdk/v2/destinations/ninetailed/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: defaultRequestConfig
path: ../../../../v0/util
- name: removeUndefinedAndNullValues
path: ../../../../v0/util
- path: ./utils

steps:
- name: messageType
template: |
.message.type.toLowerCase();
- name: validateInput
template: |
let messageType = $.outputs.messageType;
$.assert(messageType, "message Type is not present. Aborting");
$.assert(messageType in {{$.EventType.([.TRACK,.IDENTIFY,.PAGE])}}, "message type " + messageType + " is not supported");
$.assertConfig(.destination.Config.organisationId, "Organisation ID is not present. Aborting");
$.assertConfig(.destination.Config.environment, "Environment is not present. Aborting");
- name: preparePayload
template: |
const payload = $.constructFullPayload(.message);
$.context.payload = $.removeUndefinedAndNullValues(payload);

- name: buildResponse
template: |
const response = $.defaultRequestConfig();
response.body.JSON.events = [$.context.payload];
response.endpoint = $.getEndpoint(.destination.Config);
response.method = "POST";
response
35 changes: 35 additions & 0 deletions src/cdk/v2/destinations/ninetailed/rtWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
bindings:
- path: ./config
- name: handleRtTfSingleEventError
path: ../../../../v0/util/index
- path: ./utils
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.({
"output": .body.JSON.events[0],
"destination": ^[idx].destination,
"metadata": ^[idx].metadata
})[]
- name: failedEvents
template: |
$.outputs.transform#idx.error.(
$.handleRtTfSingleEventError(^[idx], .originalError ?? ., {})
)[]
- name: batchSuccessfulEvents
description: Batches the successfulEvents
template: |
$.batchResponseBuilder($.outputs.successfulEvents);

- name: finalPayload
template: |
[...$.outputs.failedEvents, ...$.outputs.batchSuccessfulEvents]
109 changes: 109 additions & 0 deletions src/cdk/v2/destinations/ninetailed/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const { BatchUtils } = require('@rudderstack/workflow-engine');
const config = require('./config');
const { constructPayload } = require('../../../../v0/util');

/**
* This fucntion constructs payloads based upon mappingConfig for all calls
* We build context as it has some specific payloads with default values so just breaking them down
* @param {*} message
* @returns
*/
const constructFullPayload = (message) => {
const context = constructPayload(
message?.context || {},
config.mappingConfig[config.ConfigCategories.CONTEXT.name],
);
const payload = constructPayload(
message,
config.mappingConfig[config.ConfigCategories.GENERAL.name],
);
let typeSpecifcPayload;
switch (message.type) {
case 'track':
typeSpecifcPayload = constructPayload(
message,
config.mappingConfig[config.ConfigCategories.TRACK.name],
);
break;
case 'identify':
typeSpecifcPayload = constructPayload(
message,
config.mappingConfig[config.ConfigCategories.IDENTIFY.name],
);
break;
case 'page':
typeSpecifcPayload = constructPayload(
message,
config.mappingConfig[config.ConfigCategories.PAGE.name],
);
break;
default:
break;
}
payload.context = context;
return { ...payload, ...typeSpecifcPayload }; // merge base and type-specific payloads;
};

const getEndpoint = (Config) => {
const { organisationId, environment } = Config;
return config.batchEndpoint
.replace('{{organisationId}}', organisationId)
.replace('{{environment}}', environment);
};

const mergeMetadata = (batch) => {
const metadata = [];
batch.forEach((event) => {
metadata.push(event.metadata);
});
return metadata;
};

const getMergedEvents = (batch) => {
const events = [];
batch.forEach((event) => {
events.push(event.output);
});
return events;
};

const batchBuilder = (batch) => ({
batchedRequest: {
body: {
JSON: { events: getMergedEvents(batch) },
JSON_ARRAY: {},
XML: {},
FORM: {},
},
version: '1',
type: 'REST',
method: 'POST',
endpoint: getEndpoint(batch[0].destination.Config),
headers: {
'Content-Type': 'application/json',
},
params: {},
files: {},
},
metadata: mergeMetadata(batch),
batched: true,
statusCode: 200,
destination: batch[0].destination,
});

/**
* This fucntions make chunk of successful events based on MAX_BATCH_SIZE
* and then build the response for each chunk to be returned as object of an array
* @param {*} events
* @returns
*/
const batchResponseBuilder = (events) => {
const batches = BatchUtils.chunkArrayBySizeAndLength(events, { maxItems: config.MAX_BATCH_SIZE });
const response = [];
batches.items.forEach((batch) => {
response.push(batchBuilder(batch));
});
return response;
};

module.exports = { constructFullPayload, getEndpoint, batchResponseBuilder };
3 changes: 2 additions & 1 deletion src/features.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"TIKTOK_AUDIENCE": true,
"REDDIT": true,
"THE_TRADE_DESK": true,
"INTERCOM": true
"INTERCOM": true,
"NINETAILED": true
},
"regulations": [
"BRAZE",
Expand Down