Skip to content

Commit

Permalink
feat: onboard destination ortto (#2730)
Browse files Browse the repository at this point in the history
* feat: onboard destination ortto

* feat: add track call support

* feat: add router batching support

* fix: refactor code

* fix: add instance region support

* fix: fix merge by values

* fix: add func for birthday obj

* fix: fix header

* fix: fix header

* fix: remove logger

* fix: property mapping

* fix: property mapping

* fix: refactor code

* fix: add router tests

* fix: update tests

* fix: refactor code

* fix: update tags mapping

* fix: update tags mapping

* chore: refactor code and add tests

* fix: code smell

* fix: refactor birthday validation
  • Loading branch information
ujjwal-ab authored Oct 30, 2023
1 parent c8baf5b commit 9be5740
Show file tree
Hide file tree
Showing 7 changed files with 2,053 additions and 1 deletion.
38 changes: 38 additions & 0 deletions src/cdk/v2/destinations/ortto/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const IDENTIFY_ENDPOINT = {
au: 'https://api.au.ap3api.com/v1/person/merge',
eu: 'https://api.eu.ap3api.com/v1/person/merge',
other: 'https://api.ap3api.com/v1/person/merge',
};
// https://help.ortto.com/developer/latest/api-reference/person/merge.html#person-fields
const TRACK_ENDPOINT = {
au: 'https://api.au.ap3api.com/v1/activities/create',
eu: 'https://api.eu.ap3api.com/v1/activities/create',
other: 'https://api.ap3api.com/v1/activities/create',
};
// https://help.ortto.com/a-271-create-a-custom-activity-event-create

const maxBatchSize = 1;

const fieldTypeMap = {
text: 'str',
email: 'str',
longText: 'txt',
number: 'int',
decimalNumber: 'int',
currency: 'int',
date: 'dtz',
timeAndDate: 'tme',
boolean: 'bol',
phone: 'phn',
singleSelect: 'str',
multiSelect: 'sst',
link: 'str',
object: 'obj',
};

module.exports = {
IDENTIFY_ENDPOINT,
TRACK_ENDPOINT,
fieldTypeMap,
maxBatchSize,
};
126 changes: 126 additions & 0 deletions src/cdk/v2/destinations/ortto/procWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
bindings:
- name: EventType
path: ../../../../constants
- path: ../../bindings/jsontemplate
exportAll: true
- path: ./config
- name: removeUndefinedAndNullValues
path: ../../../../v0/util
- name: defaultRequestConfig
path: ../../../../v0/util
- path: ./utils

steps:
- name: validateInput
template: |
let messageType = .message.type;
$.assert(.message.type, "message Type is not present. Aborting message.");
$.assertConfig(.destination.Config.privateApiKey, "Private Api Key is not present");
$.assertConfig(.destination.Config.instanceRegion, "Instance Region is not present");
$.assert(messageType in {{$.EventType.([.IDENTIFY, .TRACK])}}, "message type " + messageType + " is not supported");
$.assert(.message.().({{{{$.getGenericPaths("email")}}}}) || .message.().({{{{$.getGenericPaths("userId")}}}}), "Either of email or userId is required. Aborting message.");
- name: messageType
template: |
.message.type.toLowerCase()
- name: validateInputForTrack
description: Additional validation for Track events
condition: $.outputs.messageType === {{$.EventType.TRACK}}
template: |
$.assert(.message.event, "event is not present. Aborting.")
- name: commonFields
description: |
Builds common fields in destination payload.
template: |
let commonFields = .message.().({
"fields": {
"str::first": {{{{$.getGenericPaths("firstName")}}}},
"str::last": {{{{$.getGenericPaths("lastName")}}}},
"str::email": {{{{$.getGenericPaths("email")}}}},
"geo::city": {"name":{{{{$.getGenericPaths("city")}}}}},
"geo::country": {"name":{{{{$.getGenericPaths("country")}}}}},
"geo::region": {"name":{{{{$.getGenericPaths("region")}}}}},
"str::postal": {{{{$.getGenericPaths("zipcode")}}}},
"dtz::b": $.getBirthdayObj({{{{$.getGenericPaths("birthday")}}}}),
"str::ei": {{{{$.getGenericPaths("userId")}}}},
"str::language": .context.traits.language || .context.locale,
"phn::phone": {"n": {{{{$.getGenericPaths("phone")}}}}},
"bol::gdpr": .context.traits.gdpr ?? true,
"bol::p": .context.traits.emailConsent || false,
"bol::sp": .context.traits.smsConsent || false,
},
"location": {"source_ip": .context.ip}
});
.destination.Config.orttoPersonAttributes@attribute.(
const trimmedOrttoAttribute = attribute.orttoAttribute.trim().toLowerCase().replace(new RegExp('\\s+', 'g'),'-');
commonFields.fields[$.fieldTypeMap[attribute.type]+":cm:"+trimmedOrttoAttribute] = $.originalInput.message.context.traits[attribute.rudderTraits]
)[]
commonFields.fields = $.removeUndefinedAndNullValues(commonFields.fields)
$.removeUndefinedAndNullValues(commonFields)
- name: prepareIdentifyPayload
condition: $.outputs.messageType === {{$.EventType.IDENTIFY}}
template: |
const peopleObj = {
"fields": $.outputs.commonFields.fields,
"tags": .message.context.traits.tags || .message.traits.tags,
"unset_tags": .message.context.traits.unset_tags || .message.traits.unset_tags
}
const identifyPayoad = {
"people":[peopleObj],
"merge_by": ["str::ei", "str::email"]
}
$.removeUndefinedAndNullValues(identifyPayoad)
- name: prepareTrackPayload
condition: $.outputs.messageType === {{$.EventType.TRACK}}
steps:
- name: getTrimmedEvent
template: |
let customEvent = "";
const event = .message.event;
.destination.Config.orttoEventsMapping@order.(
customEvent = event === .rsEventName ? .orttoEventName : null;
)
$.assert(customEvent, "Event names is not mapped");
"act:cm:"+customEvent.trim().toLowerCase().replace(new RegExp('\\s+', 'g'),'-');
- name: getAttributes
template: |
let attributes = {};
[email protected]@prop.(
attributes[$.fieldTypeMap[prop.type]+":cm:"+prop.orttoProperty.trim().toLowerCase().replace(new RegExp('\\s+', 'g'),'-')] = $.originalInput.message.properties[prop.rudderProperty]
)
$.removeUndefinedAndNullValues(attributes)
- name: preparePayload
template: |
const activityObj = {
"fields": $.outputs.commonFields.fields,
"activity_id": $.outputs.prepareTrackPayload.getTrimmedEvent,
"attributes": $.outputs.prepareTrackPayload.getAttributes,
"location": {"source_ip": .message.context.ip}
};
{
"activities":[activityObj],
"merge_by": ["str::ei", "str::email"]
}
- name: payload
template: |
$.outputs.messageType === {{$.EventType.IDENTIFY}} ? $.outputs.prepareIdentifyPayload : $.outputs.prepareTrackPayload
- name: buildResponseForProcessTransformation
description: build response
template: |
const response = $.defaultRequestConfig();
const instanceRegion = $.originalInput.destination.Config.instanceRegion;
response.body.JSON = $.outputs.payload;
response.endpoint = response.body.JSON.people? $.IDENTIFY_ENDPOINT[instanceRegion] : $.TRACK_ENDPOINT[instanceRegion];
response.headers = {
"X-Api-Key": .destination.Config.privateApiKey,
"Content-Type": "application/json"
};
response;
70 changes: 70 additions & 0 deletions src/cdk/v2/destinations/ortto/rtWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
bindings:
- path: ./utils
- path: ./config

steps:
- name: validateInput
template: |
$.assert(Array.isArray(^) && ^.length > 0, "Invalid event array")
- name: transform
externalWorkflow:
path: ./procWorkflow.yaml
bindings:
- name: batchMode
value: true
loopOverInput: true
- name: successfulEvents
template: |
$.outputs.transform#idx.output.({
"message": .[],
"destination": ^ [idx].destination,
"metadata": ^ [idx].metadata
})[]
- name: failedEvents
template: |
$.outputs.transform#idx.error.({
"metadata": ^[idx].metadata[],
"destination": ^[idx].destination,
"batched": false,
"statusCode": .status,
"error": .message,
"statTags": .originalError.statTags
})[]
- name: batchSuccessfulEvents
description: Batches the successfulEvents using V3 API
condition: $.outputs.successfulEvents.length
template: |
let batches = $.batchEvents($.outputs.successfulEvents);
batches@batch.({
"batchedRequest": {
"body": {
"JSON": batch.message,
"JSON_ARRAY": {},
"XML": {},
"FORM": {}
},
"version": "1",
"type": "REST",
"method": "POST",
"endpoint": batch.message.people ? $.IDENTIFY_ENDPOINT[$.originalInput[0].destination.Config.instanceRegion] : $.TRACK_ENDPOINT[$.originalInput[0].destination.Config.instanceRegion],
"headers": {
"X-Api-Key": .destination.Config.privateApiKey,
"Content-Type": "application/json",
},
"params": {},
"files": {}
},
"metadata": batch.metadata,
"batched": true,
"statusCode": 200,
"destination": batch.destination
})[];
else:
name: returnEmptyOuput
template: '[]'

- name: finalPayload
template: |
[...$.outputs.batchSuccessfulEvents, ...$.outputs.failedEvents]
85 changes: 85 additions & 0 deletions src/cdk/v2/destinations/ortto/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const lodash = require('lodash');
const { CommonUtils } = require('../../../../util/common');
const { maxBatchSize } = require('./config');

const getBirthdayObj = (birthday) => {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/; // YYYY-MM-DD format

if (!dateRegex.test(birthday)) {
return null; // Invalid birthday format
}
const date = new Date(birthday);

const year = date.getFullYear();
const month = date.getMonth() + 1; // Month is 0-based, so add 1
const day = date.getDate();

return { year, month, day };
};

const groupEventsByEndpoint = (events) => {
const eventMap = {
person: [],
activities: [],
};
const batchErrorRespList = [];
events.forEach((result) => {
if (result.message) {
const { destination, metadata } = result;
const message = CommonUtils.toArray(result.message);
message.forEach((msg) => {
const endpoint = Object.keys(eventMap).find((key) => msg.endpoint?.includes(key));
if (endpoint) {
eventMap[endpoint].push({ message: msg.body.JSON, destination, metadata });
}
});
} else if (result.error) {
batchErrorRespList.push(result);
}
});
return {
personEvents: eventMap.person,
activitiesEvents: eventMap.activities,
batchErrorRespList,
};
};
const combinePersonAndActivitiesArraysofEvents = (events, identifier) => {
const batchedPersonEvents = [];
if (Array.isArray(events)) {
events.forEach((chunk) => {
const response = { destination: chunk[0].destination };

chunk.forEach((event, index) => {
if (index === 0) {
response.message = event.message;
response.destination = event.destination;
response.metadata = [event.metadata];
} else {
response.message[identifier].push(...event.message[identifier]);
response.metadata.push(event.metadata);
}
});
batchedPersonEvents.push(response);
});
}
return batchedPersonEvents;
};

const batchEvents = (successfulEvents) => {
const { personEvents, activitiesEvents } = groupEventsByEndpoint(successfulEvents);
const personEventsChunks = lodash.chunk(personEvents, maxBatchSize);
const activityEventsChunks = lodash.chunk(activitiesEvents, maxBatchSize);
const batchedPersonEvents = combinePersonAndActivitiesArraysofEvents(
personEventsChunks,
'people',
);
const batchedActivityEvents = combinePersonAndActivitiesArraysofEvents(
activityEventsChunks,
'activities',
);
return [...batchedPersonEvents, ...batchedActivityEvents];
};
module.exports = {
getBirthdayObj,
batchEvents,
};
3 changes: 2 additions & 1 deletion src/features.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"BRAZE": true,
"OPTIMIZELY_FULLSTACK": true,
"TWITTER_ADS": true,
"CLEVERTAP": true
"CLEVERTAP": true,
"ORTTO": true
}
}
Loading

0 comments on commit 9be5740

Please sign in to comment.