-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: twitter web conversions (#2220)
* feat: twiiter web conversions * feat: twiiter ads transformation * fix: add oauth1 dev dependency * fix: add oauth1 dev dependency * fix: add oauth1 dev dependency * fix: eslint fixes * fix: addressed comments * fix: addressed comments * fix: populateEventId func return eventId * fix: populateEventId func return eventId * fix: sonar code smells * fix: sonar code smells * fix: replace some with find function * fix: replace some with find function * chore: remove unnecessary console.log Signed-off-by: Sai Sankeerth <[email protected]> --------- Signed-off-by: Sai Sankeerth <[email protected]> Co-authored-by: Sai Sankeerth <[email protected]>
- Loading branch information
1 parent
8572817
commit 3791dd4
Showing
9 changed files
with
1,031 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
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,18 @@ | ||
const { getMappingConfig } = require('../../util'); | ||
|
||
const BASE_URL = 'https://ads-api.twitter.com/12/measurement/conversions'; | ||
|
||
const ConfigCategories = { | ||
TRACK: { | ||
type: 'track', | ||
name: 'TwitterAdsTrackConfig', | ||
}, | ||
}; | ||
|
||
const mappingConfig = getMappingConfig(ConfigCategories, __dirname); | ||
|
||
module.exports = { | ||
mappingConfig, | ||
ConfigCategories, | ||
BASE_URL | ||
}; |
45 changes: 45 additions & 0 deletions
45
src/v0/destinations/twitter_ads/data/TwitterAdsTrackConfig.json
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,45 @@ | ||
[ | ||
{ | ||
"destKey": "event_id", | ||
"sourceKeys": "properties.eventId", | ||
"required": false | ||
}, | ||
{ | ||
"destKey": "conversion_time", | ||
"sourceKeys": "properties.conversionTime", | ||
"required": false | ||
}, | ||
{ | ||
"destKey": "number_items", | ||
"sourceKeys": ["properties.quantity", "properties.numberItems"], | ||
"metadata": { | ||
"type": "toNumber" | ||
}, | ||
"required": false | ||
}, | ||
{ | ||
"destKey": "price_currency", | ||
"sourceKeys": ["properties.currency"], | ||
"required": false | ||
}, | ||
{ | ||
"destKey": "value", | ||
"sourceKeys": ["properties.value", "properties.price", "properties.revenue"], | ||
"required": false | ||
}, | ||
{ | ||
"destKey": "conversion_id", | ||
"sourceKeys": "properties.conversionId", | ||
"required": false | ||
}, | ||
{ | ||
"destKey": "description", | ||
"sourceKeys": "properties.description", | ||
"required": false | ||
}, | ||
{ | ||
"destKey": "contents", | ||
"sourceKeys": "properties.contents", | ||
"required": false | ||
} | ||
] |
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,180 @@ | ||
const sha256 = require('sha256'); | ||
|
||
const { | ||
constructPayload, | ||
defaultRequestConfig, | ||
defaultPostRequestConfig, | ||
removeUndefinedAndNullValues, | ||
isDefinedAndNotNull, | ||
simpleProcessRouterDest, | ||
} = require('../../util'); | ||
const { EventType } = require('../../../constants'); | ||
const { | ||
ConfigCategories, | ||
mappingConfig, | ||
BASE_URL | ||
} = require('./config'); | ||
|
||
const { InstrumentationError, OAuthSecretError, ConfigurationError } = require('../../util/errorTypes'); | ||
const { JSON_MIME_TYPE } = require('../../util/constant'); | ||
const { getAuthHeaderForRequest } = require("./util"); | ||
|
||
const getOAuthFields = ({ secret }) => { | ||
if (!secret) { | ||
throw new OAuthSecretError('[TWITTER ADS]:: OAuth - access keys not found'); | ||
} | ||
const oAuthObject = { | ||
consumerKey: secret.consumerKey, | ||
consumerSecret: secret.consumerSecret, | ||
accessToken: secret.accessToken, | ||
accessTokenSecret: secret.accessTokenSecret | ||
}; | ||
return oAuthObject; | ||
}; | ||
|
||
// build final response | ||
function buildResponse(message, requestJson, metadata, endpointUrl) { | ||
const response = defaultRequestConfig(); | ||
response.endpoint = endpointUrl; | ||
response.method = defaultPostRequestConfig.requestMethod; | ||
response.body.JSON.conversions = [removeUndefinedAndNullValues(requestJson)]; | ||
// required to be in accordance with oauth package | ||
const request = { | ||
url: response.endpoint, | ||
method: response.method, | ||
body: response.body.JSON | ||
}; | ||
|
||
const oAuthObject = getOAuthFields(metadata); | ||
const authHeader = getAuthHeaderForRequest(request, oAuthObject).Authorization; | ||
response.headers = { | ||
Authorization: authHeader, | ||
'Content-Type': JSON_MIME_TYPE, | ||
}; | ||
return response; | ||
} | ||
|
||
function prepareUrl(message, destination) { | ||
const pixelId = message.properties.pixelId || destination.Config.pixelId; | ||
return `${BASE_URL}/${pixelId}`; | ||
} | ||
|
||
function populateEventId(event, requestJson, destination) { | ||
|
||
const eventNameToIdMappings = destination.Config.twitterAdsEventNames; | ||
let eventId = ""; | ||
|
||
if (eventNameToIdMappings) { | ||
const eventObj = eventNameToIdMappings.find(obj => obj.rudderEventName?.trim().toLowerCase() === event?.toString().toLowerCase()); | ||
eventId = eventObj?.twitterEventId; | ||
} | ||
|
||
if(!eventId) { | ||
throw new ConfigurationError(`[TWITTER ADS]: Event - '${event}' do not have a corresponding eventId in configuration. Aborting`); | ||
} | ||
|
||
return eventId; | ||
} | ||
|
||
function populateContents(requestJson) { | ||
const reqJson = { ...requestJson }; | ||
if (reqJson.contents) { | ||
const transformedContents = requestJson.contents.map(obj => ({ | ||
...(obj.id && { content_id: obj.id }), | ||
...(obj.groupId && { content_group_id: obj.groupId }), | ||
...(obj.name && { content_name: obj.name }), | ||
...(obj.price && { content_price: parseFloat(obj.price) }), | ||
...(obj.type && { content_type: obj.type }), | ||
...(obj.quantity && { num_items: parseInt(obj.quantity, 10) }) | ||
})).filter(tfObj => Object.keys(tfObj).length > 0); | ||
if (transformedContents.length > 0) { | ||
reqJson.contents = transformedContents; | ||
} | ||
} | ||
return reqJson; | ||
} | ||
|
||
// process track call | ||
function processTrack(message, metadata, destination) { | ||
|
||
let requestJson = constructPayload(message, mappingConfig[ConfigCategories.TRACK.name]); | ||
|
||
requestJson.event_id = requestJson.event_id || populateEventId(message.event, requestJson, destination); | ||
|
||
requestJson.conversion_time = isDefinedAndNotNull(requestJson.conversion_time) | ||
? requestJson.conversion_time : message.timestamp; | ||
|
||
const identifiers = []; | ||
|
||
if (message.properties.email) { | ||
let email = message.properties.email.trim(); | ||
if (email) { | ||
email = email.toLowerCase(); | ||
identifiers.push({hashed_email: sha256(email)}) | ||
} | ||
} | ||
|
||
if (message.properties.phone) { | ||
const phone = message.properties.phone.trim(); | ||
if (phone) { | ||
identifiers.push({hashed_phone_number: sha256(phone)}) | ||
} | ||
} | ||
|
||
if (message.properties.twclid) { | ||
identifiers.push({twclid: sha256(message.properties.twclid)}); | ||
} | ||
|
||
requestJson = populateContents(requestJson); | ||
|
||
requestJson.identifiers = identifiers; | ||
|
||
const endpointUrl = prepareUrl(message, destination); | ||
|
||
return buildResponse( | ||
message, | ||
requestJson, | ||
metadata, | ||
endpointUrl | ||
); | ||
} | ||
|
||
function validateRequest(message) { | ||
|
||
const { properties } = message; | ||
|
||
if (!properties) { | ||
throw new InstrumentationError( | ||
'[TWITTER ADS]: properties must be present in event. Aborting message', | ||
); | ||
} | ||
|
||
if (!properties.email && !properties.phone && !properties.twclid) { | ||
throw new InstrumentationError( | ||
'[TWITTER ADS]: one of twclid, phone or email must be present in properties.', | ||
); | ||
} | ||
} | ||
|
||
function process(event) { | ||
|
||
const { message, metadata, destination } = event; | ||
|
||
validateRequest(message); | ||
|
||
const messageType = message.type?.toLowerCase(); | ||
|
||
if (messageType === EventType.TRACK) { | ||
return processTrack(message, metadata, destination); | ||
} | ||
|
||
throw new InstrumentationError(`Message type ${messageType} not supported`); | ||
|
||
} | ||
|
||
const processRouterDest = async (inputs, reqMetadata) => { | ||
const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); | ||
return respList; | ||
}; | ||
|
||
module.exports = { process, processRouterDest }; |
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,24 @@ | ||
const crypto = require('crypto'); | ||
const oauth1a = require('oauth-1.0a'); | ||
|
||
function getAuthHeaderForRequest(request, oAuthObject) { | ||
const oauth = oauth1a({ | ||
consumer: { key: oAuthObject.consumerKey, secret: oAuthObject.consumerSecret }, | ||
signature_method: 'HMAC-SHA1', | ||
hash_function(base_string, k) { | ||
return crypto | ||
.createHmac('sha1', k) | ||
.update(base_string) | ||
.digest('base64') | ||
}, | ||
}) | ||
|
||
const authorization = oauth.authorize(request, { | ||
key: oAuthObject.accessToken, | ||
secret: oAuthObject.accessTokenSecret, | ||
}); | ||
|
||
return oauth.toHeader(authorization); | ||
} | ||
|
||
module.exports = { getAuthHeaderForRequest }; |
Oops, something went wrong.