diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee4007dba..08032253ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.55.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.54.4...v1.55.0) (2024-02-05) + + +### Features + +* add new stat for access token expired in fb custom audience ([#3043](https://github.com/rudderlabs/rudder-transformer/issues/3043)) ([1e6d540](https://github.com/rudderlabs/rudder-transformer/commit/1e6d540fafc61a84fbbaa63d4bc5b1edc17ec44e)) +* **intercom:** upgrade intercom version from 1.4 to 2.10 ([#2976](https://github.com/rudderlabs/rudder-transformer/issues/2976)) ([717639b](https://github.com/rudderlabs/rudder-transformer/commit/717639bcce605109b145eb4cc6836fe1589278fe)) +* onboard new destination rakuten ([#3046](https://github.com/rudderlabs/rudder-transformer/issues/3046)) ([c7c3110](https://github.com/rudderlabs/rudder-transformer/commit/c7c3110a4526e31bc296abb33f3246fa8eee049a)) +* trade desk real time conversions ([#3023](https://github.com/rudderlabs/rudder-transformer/issues/3023)) ([212d5f0](https://github.com/rudderlabs/rudder-transformer/commit/212d5f09d8addc618d4398029e62c9a18a9512cf)) + + +### Bug Fixes + +* adding map for marketo known values and javascript known values ([#3037](https://github.com/rudderlabs/rudder-transformer/issues/3037)) ([64ab555](https://github.com/rudderlabs/rudder-transformer/commit/64ab555d31b4c1c49863794444bd79b2b6a45927)) +* mixpanel timestamp in ms ([#3028](https://github.com/rudderlabs/rudder-transformer/issues/3028)) ([5ad55a2](https://github.com/rudderlabs/rudder-transformer/commit/5ad55a27c8b737fd96f65c68ba086769747c5360)) +* upgrade ua-parser-js from 1.0.35 to 1.0.37 ([9a4cdef](https://github.com/rudderlabs/rudder-transformer/commit/9a4cdef59ab1c2d9dc95eb8629a7009d8d633297)) + ### [1.54.4](https://github.com/rudderlabs/rudder-transformer/compare/v1.54.3...v1.54.4) (2024-01-31) diff --git a/package-lock.json b/package-lock.json index 6eb2c81d79..4e11da75b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rudder-transformer", - "version": "1.54.4", + "version": "1.55.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rudder-transformer", - "version": "1.54.4", + "version": "1.55.0", "license": "ISC", "dependencies": { "@amplitude/ua-parser-js": "0.7.24", diff --git a/package.json b/package.json index 880f8e3415..5b4eb8e82c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rudder-transformer", - "version": "1.54.4", + "version": "1.55.0", "description": "", "homepage": "https://github.com/rudderlabs/rudder-transformer#readme", "bugs": { diff --git a/src/cdk/v2/destinations/intercom/procWorkflow.yaml b/src/cdk/v2/destinations/intercom/procWorkflow.yaml index a681772e19..0a8842d5e7 100644 --- a/src/cdk/v2/destinations/intercom/procWorkflow.yaml +++ b/src/cdk/v2/destinations/intercom/procWorkflow.yaml @@ -41,7 +41,7 @@ steps: version; - name: rEtlPayload - condition: .message.context.mappedToDestination === true + condition: .message.context.mappedToDestination template: | $.addExternalIdToTraits(.message); const payload = $.getFieldValueFromMessage(.message, "traits"); diff --git a/src/cdk/v2/destinations/rakuten/procWorkflow.yaml b/src/cdk/v2/destinations/rakuten/procWorkflow.yaml index 9ee9b5c03a..b4dcacfa09 100644 --- a/src/cdk/v2/destinations/rakuten/procWorkflow.yaml +++ b/src/cdk/v2/destinations/rakuten/procWorkflow.yaml @@ -23,7 +23,7 @@ steps: template: | const properties = $.constructProperties(.message); const lineItems = $.constructLineItems(.message.properties) - $.context.payload = {...properties,...lineItems,xml:1, mid:.destination.Config.mid} + $.context.payload = {...properties,...lineItems,xml:1,source:'rudderstack', mid:.destination.Config.mid} $.context.payload = $.removeUndefinedAndNullValues($.context.payload); - name: buildResponse diff --git a/src/cdk/v2/destinations/the_trade_desk/utils.js b/src/cdk/v2/destinations/the_trade_desk/utils.js index 64c5f2b78a..f51d8dc3ff 100644 --- a/src/cdk/v2/destinations/the_trade_desk/utils.js +++ b/src/cdk/v2/destinations/the_trade_desk/utils.js @@ -115,8 +115,10 @@ const prepareItemsPayload = (message) => { let items; const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()]; if (eventMapInfo?.itemsArray) { + // if event is one of the supported ecommerce events and products array is present items = prepareItemsFromProducts(message); } else if (eventMapInfo) { + // if event is one of the supported ecommerce events and products array is not present items = prepareItemsFromProperties(message); } return items; @@ -304,14 +306,17 @@ const enrichTrackPayload = (message, payload) => { if (eventsMapInfo && !eventsMapInfo.itemsArray) { const itemExclusionList = generateExclusionList(ITEM_CONFIGS); rawPayload = extractCustomFields(message, rawPayload, ['properties'], itemExclusionList); - } else { - // for custom events + } else if (eventsMapInfo) { + // for ecomm events with products array supports. e.g Order Completed event rawPayload = extractCustomFields( message, rawPayload, ['properties'], ['products', 'revenue', 'value'], ); + } else { + // for custom events + rawPayload = extractCustomFields(message, rawPayload, ['properties'], ['value']); } return rawPayload; }; diff --git a/src/cdk/v2/destinations/the_trade_desk/utils.test.js b/src/cdk/v2/destinations/the_trade_desk/utils.test.js index b489309956..029c3004ae 100644 --- a/src/cdk/v2/destinations/the_trade_desk/utils.test.js +++ b/src/cdk/v2/destinations/the_trade_desk/utils.test.js @@ -635,25 +635,35 @@ describe('enrichTrackPayload', () => { order_id: 'ord123', property1: 'value1', property2: 'value2', + revenue: 10, + value: 11, + products: [ + { + product_id: 'prd123', + test: 'test', + }, + ], }, }; const payload = { order_id: 'ord123', + value: 11, }; - let expectedPayload = { + const expectedPayload = { order_id: 'ord123', property1: 'value1', property2: 'value2', + revenue: 10, + value: 11, + products: [ + { + product_id: 'prd123', + test: 'test', + }, + ], }; - let result = enrichTrackPayload(message, payload); + const result = enrichTrackPayload(message, payload); expect(result).toEqual(expectedPayload); - - expectedPayload = { - order_id: 'ord123', - property1: 'value1', - property2: 'value2', - }; - expect(enrichTrackPayload(message, {})).toEqual(expectedPayload); }); }); diff --git a/src/v0/destinations/hs/config.js b/src/v0/destinations/hs/config.js index b602a7542f..fb9790f0e5 100644 --- a/src/v0/destinations/hs/config.js +++ b/src/v0/destinations/hs/config.js @@ -64,6 +64,8 @@ const API_VERSION = { v3: 'newApi', }; +const MAX_CONTACTS_PER_REQUEST = 100; + const ConfigCategory = { COMMON: { name: 'HSCommonConfig', @@ -109,5 +111,6 @@ module.exports = { SEARCH_LIMIT_VALUE, RETL_SOURCE, RETL_CREATE_ASSOCIATION_OPERATION, + MAX_CONTACTS_PER_REQUEST, DESTINATION: 'HS', }; diff --git a/src/v0/destinations/hs/util.js b/src/v0/destinations/hs/util.js index 5c8f4a908a..e905ee63c4 100644 --- a/src/v0/destinations/hs/util.js +++ b/src/v0/destinations/hs/util.js @@ -1,3 +1,5 @@ +/* eslint-disable no-await-in-loop */ +const lodash = require('lodash'); const get = require('get-value'); const { NetworkInstrumentationError, @@ -25,6 +27,7 @@ const { SEARCH_LIMIT_VALUE, hsCommonConfigJson, DESTINATION, + MAX_CONTACTS_PER_REQUEST, } = require('./config'); const tags = require('../../util/tags'); @@ -464,42 +467,127 @@ const getEventAndPropertiesFromConfig = (message, destination, payload) => { }; /** - * DOC: https://developers.hubspot.com/docs/api/crm/search + * Validates object and identifier type is present in message + * @param {*} firstMessage + * @returns + */ +const getObjectAndIdentifierType = (firstMessage) => { + const { objectType, identifierType } = getDestinationExternalIDInfoForRetl( + firstMessage, + DESTINATION, + ); + if (!objectType || !identifierType) { + throw new InstrumentationError('rETL - external Id not found.'); + } + return { objectType, identifierType }; +}; + +/** + * Returns values for search api call * @param {*} inputs + * @returns + */ +const extractIDsForSearchAPI = (inputs) => { + const values = inputs.map((input) => { + const { message } = input; + const { destinationExternalId } = getDestinationExternalIDInfoForRetl(message, DESTINATION); + return destinationExternalId.toString().toLowerCase(); + }); + + return Array.from(new Set(values)); +}; + +/** + * Returns hubspot records + * Ref : https://developers.hubspot.com/docs/api/crm/search + * @param {*} data + * @param {*} requestOptions + * @param {*} objectType + * @param {*} identifierType * @param {*} destination + * @returns */ -const getExistingData = async (inputs, destination) => { +const performHubSpotSearch = async ( + reqdata, + reqOptions, + objectType, + identifierType, + destination, +) => { + let checkAfter = 1; + const searchResults = []; + const requestData = reqdata; const { Config } = destination; - let values = []; - let searchResponse; - let updateHubspotIds = []; - const firstMessage = inputs[0].message; - let objectType = null; - let identifierType = null; - - if (firstMessage) { - objectType = getDestinationExternalIDInfoForRetl(firstMessage, DESTINATION).objectType; - identifierType = getDestinationExternalIDInfoForRetl(firstMessage, DESTINATION).identifierType; - if (!objectType || !identifierType) { - throw new InstrumentationError('rETL - external Id not found.'); + + const endpoint = IDENTIFY_CRM_SEARCH_ALL_OBJECTS.replace(':objectType', objectType); + const endpointPath = `objects/:objectType/search`; + + const url = + Config.authorizationType === 'newPrivateAppApi' + ? endpoint + : `${endpoint}?hapikey=${Config.apiKey}`; + + const requestOptions = Config.authorizationType === 'newPrivateAppApi' ? reqOptions : {}; + + /* * + * This is needed for processing paginated response when searching hubspot. + * we can't avoid await in loop as response to the request contains the pagination details + * */ + + while (checkAfter) { + const searchResponse = await httpPOST(url, requestData, requestOptions, { + destType: 'hs', + feature: 'transformation', + endpointPath, + }); + + const processedResponse = processAxiosResponse(searchResponse); + + if (processedResponse.status !== 200) { + throw new NetworkError( + `rETL - Error during searching object record. ${JSON.stringify( + processedResponse.response?.message, + )}`, + processedResponse.status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(processedResponse.status), + }, + processedResponse, + ); + } + + const after = processedResponse.response?.paging?.next?.after || 0; + requestData.after = after; // assigning to the new value of after + checkAfter = after; // assigning to the new value if no after we assign it to 0 and no more calls will take place + + const results = processedResponse.response?.results; + if (results) { + searchResults.push( + ...results.map((result) => ({ + id: result.id, + property: result.properties[identifierType], + })), + ); } - } else { - throw new InstrumentationError('rETL - objectType or identifier type not found. '); } - inputs.map(async (input) => { - const { message } = input; - const { destinationExternalId } = getDestinationExternalIDInfoForRetl(message, DESTINATION); - values.push(destinationExternalId.toString().toLowerCase()); - }); - values = Array.from(new Set(values)); + return searchResults; +}; + +/** + * Returns requestData + * @param {*} identifierType + * @param {*} chunk + * @returns + */ +const getRequestData = (identifierType, chunk) => { const requestData = { filterGroups: [ { filters: [ { propertyName: identifierType, - values, + values: chunk, operator: 'IN', }, ], @@ -510,65 +598,45 @@ const getExistingData = async (inputs, destination) => { after: 0, }; + return requestData; +}; + +/** + * DOC: https://developers.hubspot.com/docs/api/crm/search + * @param {*} inputs + * @param {*} destination + */ +const getExistingContactsData = async (inputs, destination) => { + const { Config } = destination; + const updateHubspotIds = []; + const firstMessage = inputs[0].message; + + if (!firstMessage) { + throw new InstrumentationError('rETL - objectType or identifier type not found.'); + } + + const { objectType, identifierType } = getObjectAndIdentifierType(firstMessage); + + const values = extractIDsForSearchAPI(inputs); + const valuesChunk = lodash.chunk(values, MAX_CONTACTS_PER_REQUEST); const requestOptions = { headers: { 'Content-Type': JSON_MIME_TYPE, Authorization: `Bearer ${Config.accessToken}`, }, }; - let checkAfter = 1; // variable to keep checking if we have more results - - /* eslint-disable no-await-in-loop */ - - /* * - * This is needed for processing paginated response when searching hubspot. - * we can't avoid await in loop as response to the request contains the pagination details - * */ - - while (checkAfter) { - const endpoint = IDENTIFY_CRM_SEARCH_ALL_OBJECTS.replace(':objectType', objectType); - const endpointPath = `objects/:objectType/search`; - - const url = - Config.authorizationType === 'newPrivateAppApi' - ? endpoint - : `${endpoint}?hapikey=${Config.apiKey}`; - searchResponse = - Config.authorizationType === 'newPrivateAppApi' - ? await httpPOST(url, requestData, requestOptions, { - destType: 'hs', - feature: 'transformation', - endpointPath, - }) - : await httpPOST(url, requestData, { - destType: 'hs', - feature: 'transformation', - endpointPath, - }); - searchResponse = processAxiosResponse(searchResponse); - - if (searchResponse.status !== 200) { - throw new NetworkError( - `rETL - Error during searching object record. ${searchResponse.response?.message}`, - searchResponse.status, - { - [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(searchResponse.status), - }, - searchResponse, - ); - } - - const after = searchResponse.response?.paging?.next?.after || 0; - - requestData.after = after; // assigning to the new value of after - checkAfter = after; // assigning to the new value if no after we assign it to 0 and no more calls will take place - - const results = searchResponse.response?.results; - if (results) { - updateHubspotIds = results.map((result) => { - const propertyValue = result.properties[identifierType]; - return { id: result.id, property: propertyValue }; - }); + // eslint-disable-next-line no-restricted-syntax + for (const chunk of valuesChunk) { + const requestData = getRequestData(identifierType, chunk); + const searchResults = await performHubSpotSearch( + requestData, + requestOptions, + objectType, + identifierType, + destination, + ); + if (searchResults.length > 0) { + updateHubspotIds.push(...searchResults); } } return updateHubspotIds; @@ -601,7 +669,7 @@ const setHsSearchId = (input, id) => { const splitEventsForCreateUpdate = async (inputs, destination) => { // get all the id and properties of already existing objects needed for update. - const updateHubspotIds = await getExistingData(inputs, destination); + const updateHubspotIds = await getExistingContactsData(inputs, destination); const resultInput = inputs.map((input) => { const { message } = input; @@ -680,4 +748,7 @@ module.exports = { validatePayloadDataTypes, getUTCMidnightTimeStampValue, populateTraits, + getObjectAndIdentifierType, + extractIDsForSearchAPI, + getRequestData, }; diff --git a/src/v0/destinations/hs/util.test.js b/src/v0/destinations/hs/util.test.js index 737b206401..30e89d3aee 100644 --- a/src/v0/destinations/hs/util.test.js +++ b/src/v0/destinations/hs/util.test.js @@ -1,4 +1,9 @@ -const { validatePayloadDataTypes } = require('../../../../src/v0/destinations/hs/util'); +const { + getRequestData, + extractIDsForSearchAPI, + validatePayloadDataTypes, + getObjectAndIdentifierType, +} = require('./util'); const propertyMap = { firstName: 'string', @@ -40,3 +45,187 @@ describe('Validate payload data types utility function test cases', () => { } }); }); + +describe('getObjectAndIdentifierType utility test cases', () => { + it('should return an object with objectType and identifierType properties when given a valid input', () => { + const firstMessage = { + type: 'identify', + traits: { + to: { + id: 1, + }, + from: { + id: 940, + }, + }, + userId: '1', + context: { + externalId: [ + { + id: 1, + type: 'HS-association', + toObjectType: 'contacts', + fromObjectType: 'companies', + identifierType: 'id', + associationTypeId: 'engineer', + }, + ], + mappedToDestination: 'true', + }, + }; + const result = getObjectAndIdentifierType(firstMessage); + expect(result).toEqual({ objectType: 'association', identifierType: 'id' }); + }); + + it('should throw an error when objectType or identifierType is not present in input', () => { + const firstMessage = { + type: 'identify', + traits: { + to: { + id: 1, + }, + from: { + id: 940, + }, + }, + userId: '1', + context: { + externalId: [ + { + id: 1, + type: 'HS-', + toObjectType: 'contacts', + fromObjectType: 'companies', + associationTypeId: 'engineer', + }, + ], + mappedToDestination: 'true', + }, + }; + try { + getObjectAndIdentifierType(firstMessage); + } catch (err) { + expect(err.message).toBe('rETL - external Id not found.'); + } + }); +}); + +describe('extractUniqueValues utility test cases', () => { + it('Should return an array of unique values', () => { + const inputs = [ + { + message: { + context: { + externalId: [ + { + identifierType: 'email', + id: 'testhubspot2@email.com', + type: 'HS-lead', + }, + ], + mappedToDestination: true, + }, + }, + }, + { + message: { + context: { + externalId: [ + { + identifierType: 'email', + id: 'Testhubspot3@email.com', + type: 'HS-lead', + }, + ], + mappedToDestination: true, + }, + }, + }, + { + message: { + context: { + externalId: [ + { + identifierType: 'email', + id: 'testhubspot4@email.com', + type: 'HS-lead', + }, + ], + mappedToDestination: true, + }, + }, + }, + { + message: { + context: { + externalId: [ + { + identifierType: 'email', + id: 'testHUBSPOT5@email.com', + type: 'HS-lead', + }, + ], + mappedToDestination: true, + }, + }, + }, + { + message: { + context: { + externalId: [ + { + identifierType: 'email', + id: 'testhubspot2@email.com', + type: 'HS-lead', + }, + ], + mappedToDestination: true, + }, + }, + }, + ]; + + const result = extractIDsForSearchAPI(inputs); + + expect(result).toEqual([ + 'testhubspot2@email.com', + 'testhubspot3@email.com', + 'testhubspot4@email.com', + 'testhubspot5@email.com', + ]); + }); + + it('Should return an empty array when the input is empty', () => { + const inputs = []; + const result = extractIDsForSearchAPI(inputs); + expect(result).toEqual([]); + }); +}); + +describe('getRequestDataAndRequestOptions utility test cases', () => { + it('Should return an object with requestData and requestOptions', () => { + const identifierType = 'email'; + const chunk = 'test1@gmail.com'; + const accessToken = 'dummyAccessToken'; + + const expectedRequestData = { + filterGroups: [ + { + filters: [ + { + propertyName: identifierType, + values: chunk, + operator: 'IN', + }, + ], + }, + ], + properties: [identifierType], + limit: 100, + after: 0, + }; + + const requestData = getRequestData(identifierType, chunk, accessToken); + expect(requestData).toEqual(expectedRequestData); + }); +}); diff --git a/src/v0/destinations/intercom/config.js b/src/v0/destinations/intercom/config.js new file mode 100644 index 0000000000..ae29eebc1e --- /dev/null +++ b/src/v0/destinations/intercom/config.js @@ -0,0 +1,53 @@ +const { getMappingConfig } = require('../../util'); + +const BASE_ENDPOINT = 'https://api.intercom.io'; + +// track events | Track +const TRACK_ENDPOINT = `${BASE_ENDPOINT}/events`; +// Create, Update a user with a company | Identify +const IDENTIFY_ENDPOINT = `${BASE_ENDPOINT}/users`; +// create, update, delete a company | Group +const GROUP_ENDPOINT = `${BASE_ENDPOINT}/companies`; + +const ConfigCategory = { + TRACK: { + endpoint: TRACK_ENDPOINT, + name: 'INTERCOMTrackConfig', + }, + IDENTIFY: { + endpoint: IDENTIFY_ENDPOINT, + name: 'INTERCOMIdentifyConfig', + }, + GROUP: { + endpoint: GROUP_ENDPOINT, + name: 'INTERCOMGroupConfig', + }, +}; + +const MappingConfig = getMappingConfig(ConfigCategory, __dirname); + +const ReservedTraitsProperties = [ + 'userId', + 'email', + 'phone', + 'name', + 'createdAt', + 'firstName', + 'lastName', + 'firstname', + 'lastname', + 'company', +]; + +const ReservedCompanyProperties = ['id', 'name', 'industry']; + +// ref:- https://developers.intercom.com/intercom-api-reference/v1.4/reference/event-metadata-types +const MetadataTypes = { richLink: ['url', 'value'], monetaryAmount: ['amount', 'currency'] }; + +module.exports = { + ConfigCategory, + MappingConfig, + ReservedCompanyProperties, + ReservedTraitsProperties, + MetadataTypes, +}; diff --git a/src/v0/destinations/intercom/data/INTERCOMGroupConfig.json b/src/v0/destinations/intercom/data/INTERCOMGroupConfig.json new file mode 100644 index 0000000000..6857c4e104 --- /dev/null +++ b/src/v0/destinations/intercom/data/INTERCOMGroupConfig.json @@ -0,0 +1,53 @@ +[ + { + "destKey": "company_id", + "sourceKeys": "groupId", + "required": true + }, + { + "destKey": "name", + "sourceKeys": "name", + "sourceFromGenericMap": true, + "required": false + }, + { + "destKey": "plan", + "sourceKeys": ["traits.plan", "context.traits.plan"], + "required": false + }, + { + "destKey": "size", + "sourceKeys": ["traits.size", "context.traits.size"], + "metadata": { + "type": "toNumber" + }, + "required": false + }, + { + "destKey": "website", + "sourceKeys": "website", + "sourceFromGenericMap": true, + "required": false + }, + { + "destKey": "industry", + "sourceKeys": ["traits.industry", "context.traits.industry"], + "required": false + }, + { + "destKey": "monthly_spend", + "sourceKeys": ["traits.monthlySpend", "context.traits.monthlySpend"], + "metadata": { + "type": "toNumber" + }, + "required": false + }, + { + "destKey": "remote_created_at", + "sourceKeys": ["traits.remoteCreatedAt", "context.traits.remoteCreatedAt"], + "metadata": { + "type": "toNumber" + }, + "required": false + } +] diff --git a/src/v0/destinations/intercom/data/INTERCOMIdentifyConfig.json b/src/v0/destinations/intercom/data/INTERCOMIdentifyConfig.json new file mode 100644 index 0000000000..726a741161 --- /dev/null +++ b/src/v0/destinations/intercom/data/INTERCOMIdentifyConfig.json @@ -0,0 +1,46 @@ +[ + { + "destKey": "user_id", + "sourceKeys": [ + "userId", + "traits.userId", + "traits.id", + "context.traits.userId", + "context.traits.id" + ], + "required": false + }, + { + "destKey": "email", + "sourceKeys": ["traits.email", "context.traits.email"], + "required": false + }, + { + "destKey": "phone", + "sourceKeys": ["traits.phone", "context.traits.phone"], + "required": false + }, + { + "destKey": "name", + "sourceKeys": ["traits.name", "context.traits.name"], + "required": false + }, + { + "destKey": "signed_up_at", + "sourceKeys": ["traits.createdAt", "context.traits.createdAt"], + "required": false, + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "last_seen_user_agent", + "sourceKeys": "context.userAgent", + "required": false + }, + { + "destKey": "custom_attributes", + "sourceKeys": ["traits", "context.traits"], + "required": false + } +] diff --git a/src/v0/destinations/intercom/data/INTERCOMTrackConfig.json b/src/v0/destinations/intercom/data/INTERCOMTrackConfig.json new file mode 100644 index 0000000000..f33c9a8a98 --- /dev/null +++ b/src/v0/destinations/intercom/data/INTERCOMTrackConfig.json @@ -0,0 +1,36 @@ +[ + { + "destKey": "user_id", + "sourceKeys": [ + "userId", + "traits.userId", + "traits.id", + "context.traits.userId", + "context.traits.id" + ], + "required": false + }, + { + "destKey": "email", + "sourceKeys": ["traits.email", "context.traits.email"], + "required": false + }, + { + "destKey": "event_name", + "sourceKeys": "event", + "required": true + }, + { + "destKey": "created", + "sourceKeys": "timestamp", + "sourceFromGenericMap": true, + "required": true, + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "metadata", + "sourceKeys": "properties" + } +] diff --git a/src/v0/destinations/intercom/transform.js b/src/v0/destinations/intercom/transform.js new file mode 100644 index 0000000000..212eaba13b --- /dev/null +++ b/src/v0/destinations/intercom/transform.js @@ -0,0 +1,252 @@ +const md5 = require('md5'); +const get = require('get-value'); +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { EventType, MappedToDestinationKey } = require('../../../constants'); +const { + ConfigCategory, + MappingConfig, + ReservedTraitsProperties, + ReservedCompanyProperties, +} = require('./config'); +const { + constructPayload, + removeUndefinedAndNullValues, + defaultRequestConfig, + defaultPostRequestConfig, + getFieldValueFromMessage, + addExternalIdToTraits, + simpleProcessRouterDest, + flattenJson, +} = require('../../util'); +const { separateReservedAndRestMetadata } = require('./util'); +const { JSON_MIME_TYPE } = require('../../util/constant'); + +function getCompanyAttribute(company) { + const companiesList = []; + if (company.name || company.id) { + const customAttributes = {}; + Object.keys(company).forEach((key) => { + // the key is not in ReservedCompanyProperties + if (!ReservedCompanyProperties.includes(key)) { + const val = company[key]; + if (val !== Object(val)) { + customAttributes[key] = val; + } else { + customAttributes[key] = JSON.stringify(val); + } + } + }); + + companiesList.push({ + company_id: company.id || md5(company.name), + custom_attributes: removeUndefinedAndNullValues(customAttributes), + name: company.name, + industry: company.industry, + }); + } + return companiesList; +} + +function validateIdentify(message, payload, config) { + const finalPayload = payload; + + finalPayload.update_last_request_at = + config.updateLastRequestAt !== undefined ? config.updateLastRequestAt : true; + if (payload.user_id || payload.email) { + if (payload.name === undefined || payload.name === '') { + const firstName = getFieldValueFromMessage(message, 'firstName'); + const lastName = getFieldValueFromMessage(message, 'lastName'); + if (firstName && lastName) { + finalPayload.name = `${firstName} ${lastName}`; + } else { + finalPayload.name = firstName || lastName; + } + } + + if (get(finalPayload, 'custom_attributes.company')) { + finalPayload.companies = getCompanyAttribute(finalPayload.custom_attributes.company); + } + + if (finalPayload.custom_attributes) { + ReservedTraitsProperties.forEach((trait) => { + delete finalPayload.custom_attributes[trait]; + }); + finalPayload.custom_attributes = flattenJson(finalPayload.custom_attributes); + } + + return finalPayload; + } + throw new InstrumentationError('Either of `email` or `userId` is required for Identify call'); +} + +function validateTrack(payload) { + if (!payload.user_id && !payload.email) { + throw new InstrumentationError('Either of `email` or `userId` is required for Track call'); + } + // pass only string, number, boolean properties + if (payload.metadata) { + // reserved metadata contains JSON objects that does not requires flattening + const { reservedMetadata, restMetadata } = separateReservedAndRestMetadata(payload.metadata); + return { ...payload, metadata: { ...reservedMetadata, ...flattenJson(restMetadata) } }; + } + + return payload; +} + +const checkIfEmailOrUserIdPresent = (message, Config) => { + const { context, anonymousId } = message; + let { userId } = message; + if (Config.sendAnonymousId && !userId) { + userId = anonymousId; + } + return !!(userId || context.traits?.email); +}; + +function attachUserAndCompany(message, Config) { + const email = message.context?.traits?.email; + const { userId, anonymousId, traits, groupId } = message; + const requestBody = {}; + if (userId) { + requestBody.user_id = userId; + } + if (Config.sendAnonymousId && !userId) { + requestBody.user_id = anonymousId; + } + if (email) { + requestBody.email = email; + } + const companyObj = { + company_id: groupId, + }; + if (traits?.name) { + companyObj.name = traits.name; + } + requestBody.companies = [companyObj]; + const response = defaultRequestConfig(); + response.method = defaultPostRequestConfig.requestMethod; + response.endpoint = ConfigCategory.IDENTIFY.endpoint; + response.headers = { + 'Content-Type': JSON_MIME_TYPE, + Authorization: `Bearer ${Config.apiKey}`, + Accept: JSON_MIME_TYPE, + 'Intercom-Version': '1.4', + }; + response.body.JSON = requestBody; + return response; +} + +function buildCustomAttributes(message, payload) { + const finalPayload = payload; + const { traits } = message; + const customAttributes = {}; + const companyReservedKeys = [ + 'remoteCreatedAt', + 'monthlySpend', + 'industry', + 'website', + 'size', + 'plan', + 'name', + ]; + + if (traits) { + Object.keys(traits).forEach((key) => { + if (!companyReservedKeys.includes(key) && key !== 'userId') { + customAttributes[key] = traits[key]; + } + }); + } + + if (Object.keys(customAttributes).length > 0) { + finalPayload.custom_attributes = flattenJson(customAttributes); + } + + return finalPayload; +} + +function validateAndBuildResponse(message, payload, category, destination) { + const respList = []; + const response = defaultRequestConfig(); + response.method = defaultPostRequestConfig.requestMethod; + response.endpoint = category.endpoint; + response.headers = { + 'Content-Type': JSON_MIME_TYPE, + Authorization: `Bearer ${destination.Config.apiKey}`, + Accept: JSON_MIME_TYPE, + 'Intercom-Version': '1.4', + }; + response.userId = message.anonymousId; + const messageType = message.type.toLowerCase(); + switch (messageType) { + case EventType.IDENTIFY: + response.body.JSON = removeUndefinedAndNullValues( + validateIdentify(message, payload, destination.Config), + ); + break; + case EventType.TRACK: + response.body.JSON = removeUndefinedAndNullValues(validateTrack(payload)); + break; + case EventType.GROUP: { + response.body.JSON = removeUndefinedAndNullValues(buildCustomAttributes(message, payload)); + respList.push(response); + if (checkIfEmailOrUserIdPresent(message, destination.Config)) { + const attachUserAndCompanyResponse = attachUserAndCompany(message, destination.Config); + attachUserAndCompanyResponse.userId = message.anonymousId; + respList.push(attachUserAndCompanyResponse); + } + break; + } + default: + throw new InstrumentationError(`Message type ${messageType} not supported`); + } + + return messageType === EventType.GROUP ? respList : response; +} + +function processSingleMessage(message, destination) { + if (!message.type) { + throw new InstrumentationError('Message Type is not present. Aborting message.'); + } + const { sendAnonymousId } = destination.Config; + const messageType = message.type.toLowerCase(); + let category; + + switch (messageType) { + case EventType.IDENTIFY: + category = ConfigCategory.IDENTIFY; + break; + case EventType.TRACK: + category = ConfigCategory.TRACK; + break; + case EventType.GROUP: + category = ConfigCategory.GROUP; + break; + default: + throw new InstrumentationError(`Message type ${messageType} not supported`); + } + + // build the response and return + let payload; + if (get(message, MappedToDestinationKey)) { + addExternalIdToTraits(message); + payload = getFieldValueFromMessage(message, 'traits'); + } else { + payload = constructPayload(message, MappingConfig[category.name]); + } + if (category !== ConfigCategory.GROUP && sendAnonymousId && !payload.user_id) { + payload.user_id = message.anonymousId; + } + return validateAndBuildResponse(message, payload, category, destination); +} + +function process(event) { + const response = processSingleMessage(event.message, event.destination); + return response; +} + +const processRouterDest = async (inputs, reqMetadata) => { + const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); + return respList; +}; + +module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/intercom/util.js b/src/v0/destinations/intercom/util.js new file mode 100644 index 0000000000..24a2934f7e --- /dev/null +++ b/src/v0/destinations/intercom/util.js @@ -0,0 +1,32 @@ +const { MetadataTypes } = require('./config'); + +/** + * Separates reserved metadata from rest of the metadata based on the metadata types + * ref:- https://developers.intercom.com/intercom-api-reference/v1.4/reference/event-metadata-types + * @param {*} metadata + * @returns + */ +function separateReservedAndRestMetadata(metadata) { + const reservedMetadata = {}; + const restMetadata = {}; + if (metadata) { + Object.entries(metadata).forEach(([key, value]) => { + if (value && typeof value === 'object') { + const hasMonetaryAmountKeys = MetadataTypes.monetaryAmount.every((type) => type in value); + const hasRichLinkKeys = MetadataTypes.richLink.every((type) => type in value); + if (hasMonetaryAmountKeys || hasRichLinkKeys) { + reservedMetadata[key] = value; + } else { + restMetadata[key] = value; + } + } else { + restMetadata[key] = value; + } + }); + } + + // Return the separated metadata objects + return { reservedMetadata, restMetadata }; +} + +module.exports = { separateReservedAndRestMetadata }; diff --git a/src/v0/destinations/intercom/util.test.js b/src/v0/destinations/intercom/util.test.js new file mode 100644 index 0000000000..99dbdd1f7e --- /dev/null +++ b/src/v0/destinations/intercom/util.test.js @@ -0,0 +1,176 @@ +const { separateReservedAndRestMetadata } = require('./util'); + +describe('separateReservedAndRestMetadata utility test', () => { + it('separate reserved and rest metadata', () => { + const metadata = { + property1: 1, + property2: 'test', + property3: true, + property4: { + property1: 1, + property2: 'test', + property3: { + subProp1: { + a: 'a', + b: 'b', + }, + subProp2: ['a', 'b'], + }, + }, + property5: {}, + property6: [], + property7: null, + property8: undefined, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + price: { + amount: 3000, + currency: 'USD', + }, + article: { + url: 'https://example.org/ab1de.html', + value: 'the dude abides', + }, + }; + const expectedReservedMetadata = { + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + price: { + amount: 3000, + currency: 'USD', + }, + article: { + url: 'https://example.org/ab1de.html', + value: 'the dude abides', + }, + }; + const expectedRestMetadata = { + property1: 1, + property2: 'test', + property3: true, + property4: { + property1: 1, + property2: 'test', + property3: { + subProp1: { + a: 'a', + b: 'b', + }, + subProp2: ['a', 'b'], + }, + }, + property5: {}, + property6: [], + property7: null, + property8: undefined, + }; + const { reservedMetadata, restMetadata } = separateReservedAndRestMetadata(metadata); + + expect(expectedReservedMetadata).toEqual(reservedMetadata); + expect(expectedRestMetadata).toEqual(restMetadata); + }); + + it('reserved metadata types not present in input metadata', () => { + const metadata = { + property1: 1, + property2: 'test', + property3: true, + property4: { + property1: 1, + property2: 'test', + property3: { + subProp1: { + a: 'a', + b: 'b', + }, + subProp2: ['a', 'b'], + }, + }, + property5: {}, + property6: [], + property7: null, + property8: undefined, + }; + const expectedRestMetadata = { + property1: 1, + property2: 'test', + property3: true, + property4: { + property1: 1, + property2: 'test', + property3: { + subProp1: { + a: 'a', + b: 'b', + }, + subProp2: ['a', 'b'], + }, + }, + property5: {}, + property6: [], + property7: null, + property8: undefined, + }; + const { reservedMetadata, restMetadata } = separateReservedAndRestMetadata(metadata); + + expect({}).toEqual(reservedMetadata); + expect(expectedRestMetadata).toEqual(restMetadata); + }); + + it('metadata input contains only reserved metadata types', () => { + const metadata = { + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + price: { + amount: 3000, + currency: 'USD', + }, + article: { + url: 'https://example.org/ab1de.html', + value: 'the dude abides', + }, + }; + const expectedReservedMetadata = { + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + price: { + amount: 3000, + currency: 'USD', + }, + article: { + url: 'https://example.org/ab1de.html', + value: 'the dude abides', + }, + }; + const { reservedMetadata, restMetadata } = separateReservedAndRestMetadata(metadata); + + expect(expectedReservedMetadata).toEqual(reservedMetadata); + expect({}).toEqual(restMetadata); + }); + + it('empty metadata object', () => { + const metadata = {}; + const { reservedMetadata, restMetadata } = separateReservedAndRestMetadata(metadata); + expect({}).toEqual(reservedMetadata); + expect({}).toEqual(restMetadata); + }); + + it('null/undefined metadata', () => { + const metadata = null; + const { reservedMetadata, restMetadata } = separateReservedAndRestMetadata(metadata); + expect({}).toEqual(reservedMetadata); + expect({}).toEqual(restMetadata); + }); +}); diff --git a/test/integrations/destinations/hs/processor/data.ts b/test/integrations/destinations/hs/processor/data.ts index 5eaa109dc4..03ad9d0a3b 100644 --- a/test/integrations/destinations/hs/processor/data.ts +++ b/test/integrations/destinations/hs/processor/data.ts @@ -4769,7 +4769,7 @@ export const data = [ body: [ { error: - '{"message":"rETL - Error during searching object record. Request Rate Limit reached","destinationResponse":{"response":{"status":"error","message":"Request Rate Limit reached","correlationId":"4d39ff11-e121-4514-bcd8-132a9dd1ff50","category":"RATE-LIMIT_REACHED","links":{"api key":"https://app.hubspot.com/l/api-key/"}},"status":429}}', + '{"message":"rETL - Error during searching object record. \\"Request Rate Limit reached\\"","destinationResponse":{"response":{"status":"error","message":"Request Rate Limit reached","correlationId":"4d39ff11-e121-4514-bcd8-132a9dd1ff50","category":"RATE-LIMIT_REACHED","links":{"api key":"https://app.hubspot.com/l/api-key/"}},"status":429}}', metadata: { jobId: 2, }, diff --git a/test/integrations/destinations/intercom/processor/data.ts b/test/integrations/destinations/intercom/processor/data.ts index 7ed9879b34..2c562ed4e9 100644 --- a/test/integrations/destinations/intercom/processor/data.ts +++ b/test/integrations/destinations/intercom/processor/data.ts @@ -1024,10 +1024,16 @@ export const data = [ body: [ { message: { - userId: 'user@1', channel: 'web', context: { - mappedToDestination: true, + externalId: [ + { + id: 'user@1', + type: 'INTERCOM-customer', + identifierType: 'user_id', + }, + ], + mappedToDestination: 'true', }, traits: { email: 'test@rudderlabs.com', @@ -1072,6 +1078,7 @@ export const data = [ name: 'Test Rudderlabs', phone: '+91 9999999999', owner_id: 13, + user_id: 'user@1', }, XML: {}, FORM: {}, @@ -1112,7 +1119,7 @@ export const data = [ userId: 'user@1', channel: 'web', context: { - mappedToDestination: true, + mappedToDestination: 'true', }, traits: { event_name: 'Product Viewed', @@ -2803,11 +2810,12 @@ export const data = [ }, externalId: [ { - identifierType: 'email', - id: 'test@gmail.com', + id: '10156', + type: 'INTERCOM-customer', + identifierType: 'user_id', }, ], - mappedToDestination: true, + mappedToDestination: 'true', device: { id: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', manufacturer: 'Apple', @@ -2905,8 +2913,8 @@ export const data = [ createdAt: '2020-09-30T19:11:00.337Z', phone: '9876543210', key1: 'value1', - email: 'test@gmail.com', update_last_request_at: true, + user_id: '10156', }, JSON_ARRAY: {}, XML: {}, @@ -3735,4 +3743,407 @@ export const data = [ }, }, }, + { + name: 'intercom', + description: 'Test 0', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + channel: 'mobile', + context: { + app: { + build: '1.0', + name: 'Test_Example', + namespace: 'com.example.testapp', + version: '1.0', + }, + device: { + id: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + manufacturer: 'Apple', + model: 'iPhone', + name: 'iPod touch (7th generation)', + type: 'iOS', + }, + library: { + name: 'test-ios-library', + version: '1.0.7', + }, + locale: 'en-US', + network: { + bluetooth: false, + carrier: 'unavailable', + cellular: false, + wifi: true, + }, + os: { + name: 'iOS', + version: '14.0', + }, + screen: { + density: 2, + height: 320, + width: 568, + }, + timezone: 'Asia/Kolkata', + traits: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + name: 'Test Name', + firstName: 'Test', + lastName: 'Name', + createdAt: '2020-09-30T19:11:00.337Z', + userId: 'test_user_id_1', + email: 'test_1@test.com', + phone: '9876543210', + key1: 'value1', + address: { + city: 'Kolkata', + state: 'West Bengal', + }, + originalArray: [ + { + nested_field: 'nested value', + tags: ['tag_1', 'tag_2', 'tag_3'], + }, + { + nested_field: 'nested value', + tags: ['tag_1'], + }, + { + nested_field: 'nested value', + }, + ], + }, + userAgent: 'unknown', + }, + event: 'Test Event 2', + integrations: { + All: true, + }, + messageId: '1601493060-39010c49-e6e4-4626-a75c-0dbf1925c9e8', + originalTimestamp: '2020-09-30T19:11:00.337Z', + receivedAt: '2020-10-01T00:41:11.369+05:30', + request_ip: '2405:201:8005:9856:7911:25e7:5603:5e18', + sentAt: '2020-09-30T19:11:10.382Z', + timestamp: '2020-10-01T00:41:01.324+05:30', + type: 'identify', + }, + destination: { + Config: { + apiKey: 'intercomApiKey', + appId: '9e9cdea1-78fa-4829-a9b2-5d7f7e96d1a0', + collectContext: false, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.intercom.io/users', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer intercomApiKey', + Accept: 'application/json', + 'Intercom-Version': '1.4', + }, + params: {}, + body: { + JSON: { + user_id: 'test_user_id_1', + email: 'test_1@test.com', + phone: '9876543210', + name: 'Test Name', + signed_up_at: 1601493060, + last_seen_user_agent: 'unknown', + custom_attributes: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + key1: 'value1', + 'address.city': 'Kolkata', + 'address.state': 'West Bengal', + 'originalArray[0].nested_field': 'nested value', + 'originalArray[0].tags[0]': 'tag_1', + 'originalArray[0].tags[1]': 'tag_2', + 'originalArray[0].tags[2]': 'tag_3', + 'originalArray[1].nested_field': 'nested value', + 'originalArray[1].tags[0]': 'tag_1', + 'originalArray[2].nested_field': 'nested value', + }, + update_last_request_at: true, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'intercom', + description: 'Test 1', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + channel: 'mobile', + context: { + app: { + build: '1.0', + name: 'Test_Example', + namespace: 'com.example.testapp', + version: '1.0', + }, + device: { + id: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + manufacturer: 'Apple', + model: 'iPhone', + name: 'iPod touch (7th generation)', + type: 'iOS', + }, + library: { + name: 'test-ios-library', + version: '1.0.7', + }, + locale: 'en-US', + network: { + bluetooth: false, + carrier: 'unavailable', + cellular: false, + wifi: true, + }, + os: { + name: 'iOS', + version: '14.0', + }, + screen: { + density: 2, + height: 320, + width: 568, + }, + timezone: 'Asia/Kolkata', + traits: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + firstName: 'Test', + lastName: 'Name', + createdAt: '2020-09-30T19:11:00.337Z', + email: 'test_1@test.com', + phone: '9876543210', + key1: 'value1', + }, + userAgent: 'unknown', + }, + event: 'Test Event 2', + integrations: { + All: true, + }, + messageId: '1601493060-39010c49-e6e4-4626-a75c-0dbf1925c9e8', + originalTimestamp: '2020-09-30T19:11:00.337Z', + receivedAt: '2020-10-01T00:41:11.369+05:30', + request_ip: '2405:201:8005:9856:7911:25e7:5603:5e18', + sentAt: '2020-09-30T19:11:10.382Z', + timestamp: '2020-10-01T00:41:01.324+05:30', + type: 'identify', + }, + destination: { + Config: { + apiKey: 'intercomApiKey', + appId: '9e9cdea1-78fa-4829-a9b2-5d7f7e96d1a0', + collectContext: false, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.intercom.io/users', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer intercomApiKey', + Accept: 'application/json', + 'Intercom-Version': '1.4', + }, + params: {}, + body: { + JSON: { + email: 'test_1@test.com', + phone: '9876543210', + signed_up_at: 1601493060, + last_seen_user_agent: 'unknown', + custom_attributes: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + key1: 'value1', + }, + update_last_request_at: true, + name: 'Test Name', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'intercom', + description: 'Test 2', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + channel: 'mobile', + context: { + app: { + build: '1.0', + name: 'Test_Example', + namespace: 'com.example.testapp', + version: '1.0', + }, + device: { + id: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + manufacturer: 'Apple', + model: 'iPhone', + name: 'iPod touch (7th generation)', + type: 'iOS', + }, + library: { + name: 'test-ios-library', + version: '1.0.7', + }, + locale: 'en-US', + network: { + bluetooth: false, + carrier: 'unavailable', + cellular: false, + wifi: true, + }, + os: { + name: 'iOS', + version: '14.0', + }, + screen: { + density: 2, + height: 320, + width: 568, + }, + timezone: 'Asia/Kolkata', + traits: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + lastName: 'Name', + createdAt: '2020-09-30T19:11:00.337Z', + email: 'test_1@test.com', + phone: '9876543210', + key1: 'value1', + }, + userAgent: 'unknown', + }, + event: 'Test Event 2', + integrations: { + All: true, + }, + messageId: '1601493060-39010c49-e6e4-4626-a75c-0dbf1925c9e8', + originalTimestamp: '2020-09-30T19:11:00.337Z', + receivedAt: '2020-10-01T00:41:11.369+05:30', + request_ip: '2405:201:8005:9856:7911:25e7:5603:5e18', + sentAt: '2020-09-30T19:11:10.382Z', + timestamp: '2020-10-01T00:41:01.324+05:30', + type: 'identify', + }, + destination: { + Config: { + apiKey: 'intercomApiKey', + appId: '9e9cdea1-78fa-4829-a9b2-5d7f7e96d1a0', + collectContext: false, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.intercom.io/users', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer intercomApiKey', + Accept: 'application/json', + 'Intercom-Version': '1.4', + }, + params: {}, + body: { + JSON: { + email: 'test_1@test.com', + phone: '9876543210', + signed_up_at: 1601493060, + last_seen_user_agent: 'unknown', + custom_attributes: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + key1: 'value1', + }, + update_last_request_at: true, + name: 'Name', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + }, + statusCode: 200, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/intercom/router/data.ts b/test/integrations/destinations/intercom/router/data.ts index 7ce3c7351a..2ce8621ca1 100644 --- a/test/integrations/destinations/intercom/router/data.ts +++ b/test/integrations/destinations/intercom/router/data.ts @@ -794,4 +794,394 @@ export const data = [ }, }, }, + { + name: 'intercom', + description: 'Test 0', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + channel: 'mobile', + context: { + app: { + build: '1.0', + name: 'Test_Example', + namespace: 'com.example.testapp', + version: '1.0', + }, + device: { + id: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + manufacturer: 'Apple', + model: 'iPhone', + name: 'iPod touch (7th generation)', + type: 'iOS', + }, + library: { + name: 'test-ios-library', + version: '1.0.7', + }, + locale: 'en-US', + network: { + bluetooth: false, + carrier: 'unavailable', + cellular: false, + wifi: true, + }, + os: { + name: 'iOS', + version: '14.0', + }, + screen: { + density: 2, + height: 320, + width: 568, + }, + timezone: 'Asia/Kolkata', + traits: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + name: 'Test Name', + firstName: 'Test', + lastName: 'Name', + createdAt: '2020-09-30T19:11:00.337Z', + userId: 'test_user_id_1', + email: 'test_1@test.com', + phone: '9876543210', + key1: 'value1', + }, + userAgent: 'unknown', + }, + event: 'Test Event 2', + integrations: { + All: true, + }, + messageId: '1601493060-39010c49-e6e4-4626-a75c-0dbf1925c9e8', + originalTimestamp: '2020-09-30T19:11:00.337Z', + receivedAt: '2020-10-01T00:41:11.369+05:30', + request_ip: '2405:201:8005:9856:7911:25e7:5603:5e18', + sentAt: '2020-09-30T19:11:10.382Z', + timestamp: '2020-10-01T00:41:01.324+05:30', + type: 'identify', + }, + metadata: { + jobId: 1, + }, + destination: { + Config: { + apiKey: 'testApiKey', + apiVersion: 'v1', + sendAnonymousId: false, + updateLastRequestAt: false, + collectContext: false, + }, + }, + }, + { + message: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + channel: 'mobile', + context: { + app: { + build: '1.0', + name: 'Test_Example', + namespace: 'com.example.testapp', + version: '1.0', + }, + device: { + id: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + manufacturer: 'Apple', + model: 'iPhone', + name: 'iPod touch (7th generation)', + type: 'iOS', + }, + library: { + name: 'test-ios-library', + version: '1.0.7', + }, + locale: 'en-US', + network: { + bluetooth: false, + carrier: 'unavailable', + cellular: false, + wifi: true, + }, + os: { + name: 'iOS', + version: '14.0', + }, + screen: { + density: 2, + height: 320, + width: 568, + }, + timezone: 'Asia/Kolkata', + traits: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + firstName: 'Test', + lastName: 'Name', + createdAt: '2020-09-30T19:11:00.337Z', + email: 'test_1@test.com', + phone: '9876543210', + key1: 'value1', + }, + userAgent: 'unknown', + }, + event: 'Test Event 2', + integrations: { + All: true, + }, + messageId: '1601493060-39010c49-e6e4-4626-a75c-0dbf1925c9e8', + originalTimestamp: '2020-09-30T19:11:00.337Z', + receivedAt: '2020-10-01T00:41:11.369+05:30', + request_ip: '2405:201:8005:9856:7911:25e7:5603:5e18', + sentAt: '2020-09-30T19:11:10.382Z', + timestamp: '2020-10-01T00:41:01.324+05:30', + type: 'identify', + }, + metadata: { + jobId: 2, + }, + destination: { + Config: { + apiKey: 'testApiKey', + apiVersion: 'v1', + sendAnonymousId: false, + updateLastRequestAt: false, + collectContext: false, + }, + }, + }, + { + message: { + userId: 'user@5', + groupId: 'rudderlabs', + channel: 'web', + context: { + traits: { + email: 'test+5@rudderlabs.com', + phone: '+91 9599999999', + firstName: 'John', + lastName: 'Snow', + ownerId: '17', + }, + }, + traits: { + name: 'RudderStack', + size: 500, + website: 'www.rudderstack.com', + industry: 'CDP', + plan: 'enterprise', + }, + type: 'group', + originalTimestamp: '2023-11-10T14:42:44.724Z', + timestamp: '2023-11-22T10:12:44.757+05:30', + }, + destination: { + Config: { + apiKey: 'testApiKey', + apiVersion: 'v1', + sendAnonymousId: false, + collectContext: false, + }, + }, + metadata: { + jobId: 3, + }, + }, + ], + destType: 'intercom', + }, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.intercom.io/users', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer testApiKey', + Accept: 'application/json', + 'Intercom-Version': '1.4', + }, + params: {}, + body: { + JSON: { + email: 'test_1@test.com', + phone: '9876543210', + name: 'Test Name', + signed_up_at: 1601493060, + last_seen_user_agent: 'unknown', + update_last_request_at: false, + user_id: 'test_user_id_1', + custom_attributes: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + key1: 'value1', + }, + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + userId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + }, + metadata: [ + { + jobId: 1, + }, + ], + batched: false, + statusCode: 200, + destination: { + Config: { + apiKey: 'testApiKey', + apiVersion: 'v1', + collectContext: false, + sendAnonymousId: false, + updateLastRequestAt: false, + }, + }, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.intercom.io/users', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer testApiKey', + Accept: 'application/json', + 'Intercom-Version': '1.4', + }, + params: {}, + body: { + JSON: { + email: 'test_1@test.com', + phone: '9876543210', + signed_up_at: 1601493060, + name: 'Test Name', + last_seen_user_agent: 'unknown', + update_last_request_at: false, + custom_attributes: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + key1: 'value1', + }, + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + userId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + }, + metadata: [ + { + jobId: 2, + }, + ], + batched: false, + statusCode: 200, + destination: { + Config: { + apiKey: 'testApiKey', + apiVersion: 'v1', + collectContext: false, + sendAnonymousId: false, + updateLastRequestAt: false, + }, + }, + }, + { + batched: false, + batchedRequest: [ + { + body: { + FORM: {}, + JSON: { + company_id: 'rudderlabs', + industry: 'CDP', + name: 'RudderStack', + plan: 'enterprise', + size: 500, + website: 'www.rudderstack.com', + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.intercom.io/companies', + files: {}, + headers: { + Accept: 'application/json', + Authorization: 'Bearer testApiKey', + 'Content-Type': 'application/json', + 'Intercom-Version': '1.4', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + { + body: { + FORM: {}, + JSON: { + companies: [ + { + company_id: 'rudderlabs', + name: 'RudderStack', + }, + ], + email: 'test+5@rudderlabs.com', + user_id: 'user@5', + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.intercom.io/users', + files: {}, + headers: { + Accept: 'application/json', + Authorization: 'Bearer testApiKey', + 'Content-Type': 'application/json', + 'Intercom-Version': '1.4', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + ], + destination: { + Config: { + apiKey: 'testApiKey', + apiVersion: 'v1', + collectContext: false, + sendAnonymousId: false, + }, + }, + metadata: [ + { + jobId: 3, + }, + ], + statusCode: 200, + }, + ], + }, + }, + }, + }, ]; diff --git a/test/integrations/destinations/rakuten/dataDelivery/data.ts b/test/integrations/destinations/rakuten/dataDelivery/data.ts index 2d2b00a5e4..ff40954fdf 100644 --- a/test/integrations/destinations/rakuten/dataDelivery/data.ts +++ b/test/integrations/destinations/rakuten/dataDelivery/data.ts @@ -1,6 +1,7 @@ import { endpoint, commonOutputHeaders } from '../processor/commonConfig'; const commonParams = { xml: 1, + source: 'rudderstack', amtlist: '12500|12500', qlist: '|5', ord: 'SampleOrderId', diff --git a/test/integrations/destinations/rakuten/network.ts b/test/integrations/destinations/rakuten/network.ts index 9633ee54a1..a9770d83e2 100644 --- a/test/integrations/destinations/rakuten/network.ts +++ b/test/integrations/destinations/rakuten/network.ts @@ -6,6 +6,7 @@ export const networkCallsData = [ params: { mid: 'invalid_mid', xml: 1, + source: 'rudderstack', amtlist: '12500|12500', qlist: '|5', ord: 'SampleOrderId', @@ -30,6 +31,7 @@ export const networkCallsData = [ params: { mid: 'access_denied_for_mid', xml: 1, + source: 'rudderstack', amtlist: '12500|12500', qlist: '|5', ord: 'SampleOrderId', @@ -54,6 +56,7 @@ export const networkCallsData = [ params: { mid: 'valid_mid_with_good_records', xml: 1, + source: 'rudderstack', amtlist: '12500|12500', qlist: '|5', ord: 'SampleOrderId', @@ -78,6 +81,7 @@ export const networkCallsData = [ params: { mid: 'valid_mid_with_bad_records', xml: 1, + source: 'rudderstack', amtlist: '12500|12500', qlist: '|5', ord: 'SampleOrderId', diff --git a/test/integrations/destinations/rakuten/processor/track.ts b/test/integrations/destinations/rakuten/processor/track.ts index 78a76e4263..49b26e4658 100644 --- a/test/integrations/destinations/rakuten/processor/track.ts +++ b/test/integrations/destinations/rakuten/processor/track.ts @@ -80,6 +80,7 @@ export const trackSuccess = [ params: { mid: 'dummyMarketingId', xml: 1, + source: 'rudderstack', amtlist: '2000|2500|3000', brandlist: 'SampleBrand||', catidlist: '12345||', @@ -209,6 +210,7 @@ export const trackSuccess = [ params: { mid: 'dummyMarketingId', xml: 1, + source: 'rudderstack', amtlist: '12500|12500|3000', couponlist: '||SALE50', namelist: 'name_1|name_2|SampleProduct', @@ -316,6 +318,7 @@ export const trackSuccess = [ params: { mid: 'dummyMarketingId', xml: 1, + source: 'rudderstack', amtlist: '-12500|-12500', skulist: 'custom sku 0|custom sku 1', qlist: '1|5', @@ -429,6 +432,7 @@ export const trackSuccess = [ params: { mid: 'dummyMarketingId', xml: 1, + source: 'rudderstack', namelist: 'name_1|name_2|Discount', amtlist: '12500|12500|-50000', skulist: 'custom sku 0|custom sku 1|Discount', diff --git a/test/integrations/destinations/the_trade_desk/router/data.ts b/test/integrations/destinations/the_trade_desk/router/data.ts index 6f379195fa..691ec703b9 100644 --- a/test/integrations/destinations/the_trade_desk/router/data.ts +++ b/test/integrations/destinations/the_trade_desk/router/data.ts @@ -1681,9 +1681,16 @@ export const data = [ properties: { key1: 'value1', value: 25, + revenue: 10, product_id: 'prd123', key2: true, test: 'test123', + products: [ + { + product_id: 'prd123', + test: 'test', + }, + ], }, }, destination: sampleDestination, @@ -1779,6 +1786,13 @@ export const data = [ test: 'test123', key1: 'value1', key2: true, + revenue: 10, + products: [ + { + product_id: 'prd123', + test: 'test', + }, + ], }, ], },