-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: onboard destination ortto (#2730)
* 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
Showing
7 changed files
with
2,053 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.