-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
367 additions
and
177 deletions.
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
File renamed without changes.
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 path = require('path'); | ||
const fs = require('fs'); | ||
const Message = require('../../v0/sources/message'); | ||
const { excludedFieldList } = require('./config'); | ||
const { extractCustomFields, generateUUID } = require('../../v0/util'); | ||
const { convertToISODate } = require('./utils'); | ||
|
||
// ref : https://help.adjust.com/en/article/global-callbacks#general-recommended-placeholders | ||
// import mapping json using JSON.parse to preserve object key order | ||
const mapping = JSON.parse(fs.readFileSync(path.resolve(__dirname, './mapping.json'), 'utf-8')); | ||
|
||
const processPayload = (payload) => { | ||
const message = new Message(`Adjust`); | ||
|
||
// event type is always track | ||
const eventType = 'track'; | ||
message.setEventType(eventType); | ||
message.setPropertiesV2(payload, mapping); | ||
let customProperties = {}; | ||
|
||
// to remove writeKey from custom properties we can add it to excludedFieldList | ||
excludedFieldList.push('writeKey'); | ||
|
||
customProperties = extractCustomFields(payload, customProperties, 'root', excludedFieldList); | ||
|
||
message.properties = { ...message.properties, ...customProperties }; | ||
|
||
if (payload.created_at) { | ||
const ts = convertToISODate(payload.created_at); | ||
message.setProperty('originalTimestamp', ts); | ||
message.setProperty('timestamp', ts); | ||
} | ||
// adjust does not has the concept of user but we need to set some random anonymousId in order to make the server accept the message | ||
message.anonymousId = generateUUID(); | ||
return message; | ||
}; | ||
|
||
module.exports = { processPayload }; |
File renamed without changes.
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 |
---|---|---|
@@ -1,18 +1,46 @@ | ||
const { processEvent: processV0Event } = require('../../v0/sources/adjust/transform'); | ||
const { TransformationError } = require('@rudderstack/integrations-lib'); | ||
const { CommonUtils } = require('../../util/common'); | ||
const logger = require('../../logger'); | ||
const { flattenParams } = require('./utils'); | ||
const { processPayload } = require('./core'); | ||
|
||
const convertV2ToV0 = (sourceEvent) => { | ||
const v0Event = JSON.parse(sourceEvent.request.body); | ||
if (sourceEvent.request.query_parameters) { | ||
v0Event.query_parameters = sourceEvent.request.query_parameters; | ||
/** | ||
* Extracts and flattens query parameters from the webhook request | ||
* @param {Object} inputRequest - The incoming webhook request object | ||
* @returns {Object} Flattened query parameters | ||
* @throws {TransformationError} If request or query_parameters are missing | ||
*/ | ||
const getPayloadFromRequest = (inputRequest) => { | ||
const { request } = inputRequest; | ||
if (!request) { | ||
throw new TransformationError('request field is missing from webhook V2 payload'); | ||
} | ||
return v0Event; | ||
|
||
const { query_parameters: qParams } = request; | ||
logger.debug(`[Adjust] Input event: query_params: ${JSON.stringify(qParams)}`); | ||
if (!qParams || Object.keys(qParams).length === 0) { | ||
throw new TransformationError('Query_parameters is missing'); | ||
} | ||
|
||
return flattenParams(qParams); | ||
}; | ||
|
||
/** | ||
* Processes incoming webhook requests from Adjust | ||
* @param {Object|Array} requests - Single request object or array of webhook requests | ||
* @returns {Array} Array of transformed payloads ready to be sent to rudder-server | ||
* @description | ||
* This function: | ||
* - converts incoming payload to array | ||
* - extracts params and constructs payload | ||
* - sends it to processPayload for transformation | ||
*/ | ||
const process = (requests) => { | ||
const requestsArray = CommonUtils.toArray(requests); | ||
const v0Events = requestsArray.map(convertV2ToV0); | ||
return v0Events.map(processV0Event); | ||
return requestsArray.map((inputRequest) => { | ||
const formattedPayload = getPayloadFromRequest(inputRequest); | ||
return processPayload(formattedPayload); | ||
}); | ||
}; | ||
|
||
module.exports = { process }; |
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,56 @@ | ||
const { TransformationError, isDefinedAndNotNull } = require('@rudderstack/integrations-lib'); | ||
|
||
/** | ||
* Converts a raw timestamp to ISO 8601 date string format | ||
* @param {number|string} rawTimestamp - The timestamp to convert (expects Unix timestamp in seconds) | ||
* @returns {string} The timestamp converted to ISO 8601 format | ||
* @throws {TransformationError} If the timestamp is invalid or cannot be parsed | ||
*/ | ||
const convertToISODate = (rawTimestamp) => { | ||
if (typeof rawTimestamp !== 'number' && typeof rawTimestamp !== 'string') { | ||
throw new TransformationError( | ||
`Invalid timestamp type: expected number or string, received ${typeof rawTimestamp}`, | ||
); | ||
} | ||
|
||
const createdAt = Number(rawTimestamp); | ||
|
||
if (Number.isNaN(createdAt)) { | ||
throw new TransformationError(`Failed to parse timestamp: "${rawTimestamp}"`); | ||
} | ||
|
||
const date = new Date(createdAt * 1000); | ||
|
||
if (Number.isNaN(date.getTime())) { | ||
throw new TransformationError(`Failed to create valid date for timestamp "${rawTimestamp}"`); | ||
} | ||
|
||
return date.toISOString(); | ||
}; | ||
|
||
/** | ||
* Flattens an object containing array parameters into a simple key-value object with first element of array as value | ||
* @param {Object.<string, Array>} qParams - Object containing parameters where values are arrays | ||
* @returns {Object.<string, *>} Flattened object where array values are replaced with their first element | ||
* @description | ||
* This function flattens an object containing array parameters into a simple key-value object | ||
* with first element of array as value if it is an array, otherwise the value is returned as is | ||
* In case of empty array, the key is removed from the output | ||
*/ | ||
const flattenParams = (qParams) => { | ||
const formattedOutput = {}; | ||
if (qParams) { | ||
Object.entries(qParams).forEach(([key, value]) => { | ||
const finalValue = Array.isArray(value) ? value[0] : value; | ||
if (isDefinedAndNotNull(finalValue)) { | ||
formattedOutput[key] = finalValue; | ||
} | ||
}); | ||
} | ||
return formattedOutput; | ||
}; | ||
|
||
module.exports = { | ||
convertToISODate, | ||
flattenParams, | ||
}; |
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,207 @@ | ||
const { convertToISODate } = require('./utils'); | ||
const { TransformationError } = require('@rudderstack/integrations-lib'); | ||
|
||
describe('convertToISODate', () => { | ||
const testCases = [ | ||
{ | ||
name: 'valid numeric timestamp', | ||
input: 1633072800, | ||
expected: '2021-10-01T07:20:00.000Z', | ||
shouldThrow: false, | ||
}, | ||
{ | ||
name: 'non-numeric string', | ||
input: 'invalid', | ||
shouldThrow: true, | ||
errorType: TransformationError, | ||
}, | ||
{ | ||
name: 'valid numeric string timestamp', | ||
input: '1633072800', | ||
expected: '2021-10-01T07:20:00.000Z', | ||
shouldThrow: false, | ||
}, | ||
{ | ||
name: 'object input', | ||
input: {}, | ||
shouldThrow: true, | ||
errorType: TransformationError, | ||
}, | ||
{ | ||
name: 'array input', | ||
input: [], | ||
shouldThrow: true, | ||
errorType: TransformationError, | ||
}, | ||
{ | ||
name: 'null input', | ||
input: null, | ||
shouldThrow: true, | ||
errorType: TransformationError, | ||
}, | ||
{ | ||
name: 'undefined input', | ||
input: undefined, | ||
shouldThrow: true, | ||
errorType: TransformationError, | ||
}, | ||
{ | ||
name: 'huge timestamp that becomes invalid', | ||
input: 999999999999999, | ||
shouldThrow: true, | ||
errorType: TransformationError, | ||
}, | ||
]; | ||
|
||
testCases.forEach(({ name, input, expected, shouldThrow, errorType }) => { | ||
it(`should handle ${name}`, () => { | ||
if (shouldThrow) { | ||
expect(() => convertToISODate(input)).toThrow(errorType); | ||
} else { | ||
const result = convertToISODate(input); | ||
expect(result).toBe(expected); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
describe('flattenParams', () => { | ||
const { flattenParams } = require('./utils'); | ||
|
||
const testCases = [ | ||
{ | ||
name: 'flatten array values to first elements', | ||
input: { | ||
key1: ['value1'], | ||
key2: ['value2'], | ||
key3: [123], | ||
}, | ||
expected: { | ||
key1: 'value1', | ||
key2: 'value2', | ||
key3: 123, | ||
}, | ||
}, | ||
{ | ||
name: 'handle null input', | ||
input: null, | ||
expected: {}, | ||
}, | ||
{ | ||
name: 'handle undefined input', | ||
input: undefined, | ||
expected: {}, | ||
}, | ||
{ | ||
name: 'handle empty object', | ||
input: {}, | ||
expected: {}, | ||
}, | ||
{ | ||
name: 'ignore null/undefined array elements', | ||
input: { | ||
key1: [undefined], | ||
key2: [null], | ||
key3: [undefined, 'value'], | ||
key4: [null, 'value'], | ||
}, | ||
expected: {}, | ||
}, | ||
{ | ||
name: 'handle mixed type values in arrays', | ||
input: { | ||
number: [42], | ||
string: ['test'], | ||
boolean: [true], | ||
object: [{ nested: 'value' }], | ||
date: [new Date('2024-01-01')], | ||
}, | ||
expected: { | ||
number: 42, | ||
string: 'test', | ||
boolean: true, | ||
object: { nested: 'value' }, | ||
date: new Date('2024-01-01'), | ||
}, | ||
}, | ||
{ | ||
name: 'handle empty arrays', | ||
input: { | ||
key1: [], | ||
key2: ['value'], | ||
key3: [], | ||
}, | ||
expected: { | ||
key2: 'value', | ||
}, | ||
}, | ||
{ | ||
name: 'keep non-array values unchanged', | ||
input: { | ||
string: 'direct string', | ||
number: 42, | ||
boolean: true, | ||
object: { test: 'value' }, | ||
array: ['first', 'second'], | ||
}, | ||
expected: { | ||
string: 'direct string', | ||
number: 42, | ||
boolean: true, | ||
object: { test: 'value' }, | ||
array: 'first', | ||
}, | ||
}, | ||
{ | ||
name: 'handle mixed array and non-array values', | ||
input: { | ||
arrayValue: ['first'], | ||
emptyArray: [], | ||
directValue: 'string', | ||
nullValue: null, | ||
undefinedValue: undefined, | ||
}, | ||
expected: { | ||
arrayValue: 'first', | ||
directValue: 'string', | ||
}, | ||
}, | ||
{ | ||
name: 'handle nested arrays', | ||
input: { | ||
nested: [ | ||
[1, 2], | ||
[3, 4], | ||
], | ||
mixed: [['a', 'b'], 'c'], | ||
}, | ||
expected: { | ||
nested: [1, 2], | ||
mixed: ['a', 'b'], | ||
}, | ||
}, | ||
{ | ||
name: 'handle special values in arrays', | ||
input: { | ||
nan: [NaN], | ||
infinity: [Infinity], | ||
negInfinity: [-Infinity], | ||
zero: [0], | ||
negZero: [-0], | ||
}, | ||
expected: { | ||
nan: NaN, | ||
infinity: Infinity, | ||
negInfinity: -Infinity, | ||
zero: 0, | ||
negZero: -0, | ||
}, | ||
}, | ||
]; | ||
|
||
testCases.forEach(({ name, input, expected }) => { | ||
it(`should ${name}`, () => { | ||
expect(flattenParams(input)).toEqual(expected); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.