Skip to content

Commit

Permalink
feat: onboard new destination bloomreach
Browse files Browse the repository at this point in the history
  • Loading branch information
Gauravudia committed Mar 15, 2024
1 parent bdc0f41 commit cf1c3ba
Show file tree
Hide file tree
Showing 14 changed files with 1,142 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/cdk/v2/destinations/bloomreach/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getMappingConfig } from '../../../../v0/util';

export const CUSTOMER_COMMAND = 'customers';
export const CUSTOMER_EVENT_COMMAND = 'customers/events';
export const MAX_BATCH_SIZE = 50;
export const getBatchEndpoint = (apiBaseUrl: string, projectToken: string): string =>
`${apiBaseUrl}/track/v2/projects/${projectToken}/batch`;

const CONFIG_CATEGORIES = {
CUSTOMER_PROPERTIES_CONFIG: { name: 'BloomreachCustomerPropertiesConfig' },
};
const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname);
export const EXCLUSION_FIELDS = [
'email',
'firstName',
'firstname',
'first_name',
'lastName',
'lastname',
'last_name',
'name',
'phone',
'city',
'birthday',
'country',
];
export const CUSTOMER_PROPERTIES_CONFIG =
MAPPING_CONFIG[CONFIG_CATEGORIES.CUSTOMER_PROPERTIES_CONFIG.name];
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[
{
"destKey": "first_name",
"sourceKeys": "firstName",
"sourceFromGenericMap": true
},
{
"destKey": "last_name",
"sourceKeys": "lastName",
"sourceFromGenericMap": true
},
{
"destKey": "email",
"sourceKeys": "emailOnly",
"sourceFromGenericMap": true
},
{
"destKey": "phone",
"sourceKeys": "phone",
"sourceFromGenericMap": true
},
{
"destKey": "city",
"sourceKeys": "city",
"sourceFromGenericMap": true
},
{
"destKey": "country",
"sourceKeys": ["traits.address.country", "context.traits.address.country"]
},
{
"destKey": "birthday",
"sourceKeys": "birthday",
"sourceFromGenericMap": true
}
]
119 changes: 119 additions & 0 deletions src/cdk/v2/destinations/bloomreach/procWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
bindings:
- name: EventType
path: ../../../../constants
- path: ../../bindings/jsontemplate
- name: defaultRequestConfig
path: ../../../../v0/util
- name: toUnixTimestamp
path: ../../../../v0/util
- name: base64Convertor
path: ../../../../v0/util
- name: removeUndefinedAndNullValues
path: ../../../../v0/util
- name: generateExclusionList
path: ../../../../v0/util
- name: extractCustomFields
path: ../../../../v0/util
- name: constructPayload
path: ../../../../v0/util
- path: ./utils
- path: ./config

steps:
- name: messageType
template: |
$.context.messageType = .message.type.toLowerCase();
- name: validateInput
template: |
let messageType = $.context.messageType;
$.assert(messageType, "message Type is not present. Aborting");
$.assert(messageType in {{$.EventType.([.IDENTIFY,.TRACK,.PAGE,.SCREEN])}}, "message type " + messageType + " is not supported");
$.assertConfig(.destination.Config.apiBaseUrl, "API Base URL is not present. Aborting");
$.assertConfig(.destination.Config.apiKey, "API Key is not present . Aborting");
$.assertConfig(.destination.Config.apiSecret, "API Secret is not present. Aborting");
$.assertConfig(.destination.Config.projectToken, "Project Token is not present. Aborting");
$.assertConfig(.destination.Config.hardID, "Hard ID is not present. Aborting");
$.assertConfig(.destination.Config.softID, "Soft ID is not present. Aborting");
$.assert(.message.timestamp ?? .message.originalTimestamp, "Timestamp is not present. Aborting");
const userId = .message.().(
{{{{$.getGenericPaths("userIdOnly")}}}};
);
$.assert(userId ?? .message.anonymousId, "Either one of userId or anonymousId is required. Aborting");
- name: prepareIdentifyPayload
condition: $.context.messageType === {{$.EventType.IDENTIFY}}
template: |
const customerIDs = $.prepareCustomerIDs(.message, .destination);
const customerProperties = $.constructPayload(.message, $.CUSTOMER_PROPERTIES_CONFIG);
const extraCustomerProperties = $.extractCustomFields(.message, {}, ['traits', 'context.traits'], $.EXCLUSION_FIELDS);
const properties = {
...customerProperties,
...extraCustomerProperties
}
const data = .message.().({
"customer_ids": customerIDs,
"update_timestamp": $.toUnixTimestamp({{{{$.getGenericPaths("timestamp")}}}}),
properties
});
$.context.payload = $.removeUndefinedAndNullValues({name: $.CUSTOMER_COMMAND, data})
- name: prepareEventName
steps:
- name: pageEventName
condition: $.context.messageType === {{$.EventType.PAGE}}
template: |
const category = .message.category ?? .message.properties.category;
const name = .message.name || .message.properties.name;
const eventNameArray = ["Viewed"];
category ? eventNameArray.push(category);
name ? eventNameArray.push(name);
eventNameArray.push("Page");
$.context.event = eventNameArray.join(" ");
- name: screenEventName
condition: $.context.messageType === {{$.EventType.SCREEN}}
template: |
const category = .message.category ?? .message.properties.category;
const name = .message.name || .message.properties.name;
const eventNameArray = ["Viewed"];
category ? eventNameArray.push(category);
name ? eventNameArray.push(name);
eventNameArray.push("Screen");
$.context.event = eventNameArray.join(" ");
- name: trackEventName
condition: $.context.messageType === {{$.EventType.TRACK}}
template: |
$.assert(.message.event, "Event name is required. Aborting");
$.context.event = .message.event
- name: prepareTrackPageScreenPayload
condition: $.context.messageType !== {{$.EventType.IDENTIFY}}
template: |
const customerIDs = $.prepareCustomerIDs(.message, .destination);
const data = .message.().({
"customer_ids": customerIDs,
"timestamp": $.toUnixTimestamp({{{{$.getGenericPaths("timestamp")}}}}),
"properties": .properties,
"event_type": $.context.event,
});
$.context.payload = $.removeUndefinedAndNullValues({name: $.CUSTOMER_EVENT_COMMAND, data})
- name: buildResponse
description: In batchMode we return payload directly
condition: $.batchMode
template: |
$.context.payload
else:
name: buildResponseForProcessTransformation
template: |
const response = $.defaultRequestConfig();
response.body.JSON = $.context.payload;
response.endpoint = $.getBatchEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken);
response.method = "POST";
response.headers = {
"Content-Type": "application/json",
"Authorization": "Basic " + $.base64Convertor(.destination.Config.apiKey + ":" + .destination.Config.apiSecret)
}
response;
76 changes: 76 additions & 0 deletions src/cdk/v2/destinations/bloomreach/rtWorkflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
bindings:
- name: handleRtTfSingleEventError
path: ../../../../v0/util/index
- path: ./utils
exportAll: true
- name: base64Convertor
path: ../../../../v0/util
- name: toUnixTimestamp
path: ../../../../v0/util
- name: BatchUtils
path: '@rudderstack/workflow-engine'
- 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.({
"batchedRequest": .,
"batched": false,
"destination": ^[idx].destination,
"metadata": ^[idx].metadata,
"statusCode": 200
})[]
- name: failedEvents
template: |
$.outputs.transform#idx.error.(
$.handleRtTfSingleEventError(^[idx], .originalError ?? ., {})
)[]
- name: batchSuccessfulEvents
description: Batches the successfulEvents
template: |
let batches = $.BatchUtils.chunkArrayBySizeAndLength(
$.outputs.successfulEvents, {maxItems: $.MAX_BATCH_SIZE}).items;
batches@batch.({
"batchedRequest": {
"body": {
"JSON": {"commands": ~r batch.batchedRequest[]},
"JSON_ARRAY": {},
"XML": {},
"FORM": {}
},
"version": "1",
"type": "REST",
"method": "POST",
"endpoint": batch[0].destination.Config.().($.getBatchEndpoint(.apiBaseUrl, .projectToken)),
"headers": batch[0].destination.Config.().({
"Content-Type": "application/json",
"Authorization": "Basic " + $.base64Convertor(.apiKey + ":" + .apiSecret)
}),
"params": {},
"files": {}
},
"metadata": ~r batch.metadata[],
"batched": true,
"statusCode": 200,
"destination": batch[0].destination
})[];
- name: finalPayload
template: |
[...$.outputs.batchSuccessfulEvents, ...$.outputs.failedEvents]
30 changes: 30 additions & 0 deletions src/cdk/v2/destinations/bloomreach/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { isObject, isEmptyObject, getIntegrationsObj } from '../../../../v0/util';

const getCustomerIDsFromIntegrationObject = (message) => {
const integrationObj = getIntegrationsObj(message, 'bloomreach' as any) || {};
const { hardID, softID } = integrationObj;
const customerIDs = {};

if (isObject(hardID) && !isEmptyObject(hardID)) {
Object.keys(hardID).forEach((id) => {
customerIDs[id] = hardID[id];
});
}

if (isObject(softID) && !isEmptyObject(softID)) {
Object.keys(softID).forEach((id) => {
customerIDs[id] = softID[id];
});
}

return customerIDs;
};

export const prepareCustomerIDs = (message, destination) => {
const customerIDs = {
[destination.Config.hardID]: message.userId,
[destination.Config.softID]: message.anonymousId,
...getCustomerIDsFromIntegrationObject(message),
};
return customerIDs;
};
1 change: 1 addition & 0 deletions src/constants/destinationCanonicalNames.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const DestCanonicalNames = {
],
INTERCOM: ['INTERCOM', 'intercom', 'Intercom'],
koala: ['Koala', 'koala', 'KOALA'],
bloomreach: ['Bloomreach', 'bloomreach', 'BLOOMREACH'],
};

module.exports = { DestHandlerMap, DestCanonicalNames };
90 changes: 90 additions & 0 deletions test/integrations/destinations/bloomreach/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Destination } from '../../../../src/types';

const destType = 'bloomreach';
const destTypeInUpperCase = 'BLOOMREACH';
const displayName = 'bloomreach';
const channel = 'web';
const destination: Destination = {
Config: {
apiBaseUrl: 'https://demoapp-api.bloomreach.com',
apiKey: 'test-api-key',
apiSecret: 'test-api-secret',
projectToken: 'test-project-token',
hardID: 'registered',
softID: 'cookie',
},
DestinationDefinition: {
DisplayName: displayName,
ID: '123',
Name: destTypeInUpperCase,
Config: { cdkV2Enabled: true },
},
Enabled: true,
ID: '123',
Name: destTypeInUpperCase,
Transformations: [],
WorkspaceID: 'test-workspace-id',
};

const traits = {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe',
phone: '1234567890',
address: {
city: 'New York',
country: 'USA',
pinCode: '123456',
},
};

const properties = {
product_id: '622c6f5d5cf86a4c77358033',
sku: '8472-998-0112',
category: 'Games',
name: 'Cones of Dunshire',
brand: 'Wyatt Games',
variant: 'expansion pack',
price: 49.99,
quantity: 5,
coupon: 'PREORDER15',
currency: 'USD',
position: 1,
url: 'https://www.website.com/product/path',
image_url: 'https://www.website.com/product/path.webp',
key1: 'value1',
};
const endpoint = 'https://demoapp-api.bloomreach.com/track/v2/projects/test-project-token/batch';

const processorInstrumentationErrorStatTags = {
destType: destTypeInUpperCase,
errorCategory: 'dataValidation',
errorType: 'instrumentation',
feature: 'processor',
implementation: 'cdkV2',
module: 'destination',
destinationId: 'default-destinationId',
workspaceId: 'default-workspaceId',
};

const RouterInstrumentationErrorStatTags = {
...processorInstrumentationErrorStatTags,
feature: 'router',
};

const headers = {
'Content-Type': 'application/json',
Authorization: 'Basic dGVzdC1hcGkta2V5OnRlc3QtYXBpLXNlY3JldA==',
};

export {
destType,
channel,
destination,
processorInstrumentationErrorStatTags,
RouterInstrumentationErrorStatTags,
traits,
headers,
properties,
endpoint,
};
5 changes: 5 additions & 0 deletions test/integrations/destinations/bloomreach/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as config from '../../../../src/cdk/v2/destinations/bloomreach/config';

export const defaultMockFns = () => {
jest.replaceProperty(config, 'MAX_BATCH_SIZE', 3 as any);
};
Loading

0 comments on commit cf1c3ba

Please sign in to comment.