From d776d2abf1582c10d1ff3aee232083cbc743c3af Mon Sep 17 00:00:00 2001 From: ItsSudip Date: Thu, 7 Nov 2024 14:04:35 +0530 Subject: [PATCH 1/5] feat: onboard linkedin audience destination --- .../destinations/linkedin_audience/config.ts | 10 ++ .../linkedin_audience/procWorkflow.yaml | 97 +++++++++++++++++++ .../linkedin_audience/rtWorkflow.yaml | 40 ++++++++ .../destinations/linkedin_audience/utils.ts | 87 +++++++++++++++++ src/features.json | 3 +- 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 src/cdk/v2/destinations/linkedin_audience/config.ts create mode 100644 src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml create mode 100644 src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml create mode 100644 src/cdk/v2/destinations/linkedin_audience/utils.ts diff --git a/src/cdk/v2/destinations/linkedin_audience/config.ts b/src/cdk/v2/destinations/linkedin_audience/config.ts new file mode 100644 index 00000000000..86ea94425a5 --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_audience/config.ts @@ -0,0 +1,10 @@ +export const SUPPORTED_EVENT_TYPE = 'record'; +export const ACTION_TYPES = ['insert', 'delete']; +export const BASE_ENDPOINT = 'https://api.linkedin.com/rest'; +export const USER_ENDPOINT = '/dmpSegments/audienceId/users'; +export const COMPANY_ENDPOINT = '/dmpSegments/audienceId/companies'; +export const FIELD_MAP = { + sha256Email: 'SHA256_EMAIL', + sha512Email: 'SHA512_EMAIL', + googleAid: 'GOOGLE_AID', +}; diff --git a/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml b/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml new file mode 100644 index 00000000000..e292dba8e90 --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml @@ -0,0 +1,97 @@ +bindings: + - path: ./config + exportAll: true + - path: ./utils + exportAll: true + - name: defaultRequestConfig + path: ../../../../v0/util + +steps: + - name: validateInput + description: Validate input, if all the required fields are available or not. + template: | + const config = .connection.config.destination; + const secret = .metadata.secret; + let messageType = .message.type; + $.assertConfig(config.audienceId, "Audience Id is not present. Aborting"); + $.assertConfig(secret.accessToken, "Access Token is not present. Aborting"); + $.assertConfig(config.audienceType, "audienceType is not present. Aborting"); + $.assert(messageType, "Message Type is not present. Aborting message."); + $.assert(messageType.toLowerCase() === $.SUPPORTED_EVENT_TYPE, "Event type " + .message.type.toLowerCase() + " is not supported. Aborting message."); + $.assert(.message.fields, "`fields` is not present. Aborting message."); + $.assert(.message.identifiers, "`identifiers` is not present inside properties. Aborting message."); + $.assert($.containsAll([.message.action], $.ACTION_TYPES), "Unsupported action type. Aborting message.") + + - name: getConfigs + description: This step fetches the configs from different places and combines them. + template: | + const config = .connection.config.destination; + const secret = .metadata.secret; + const audienceType = config.audienceType; + const audienceId = config.audienceId; + const accessToken = secret.accessToken; + { + audienceType, + audienceId, + accessToken, + isHashRequired: config.isHashRequired, + } + + - name: hashIdentifiers + description: Hash identifiers + condition: $.outputs.getConfigs.isHashRequired === true && $.outputs.getConfigs.audienceType === 'user' + template: | + const hashedIdentifiers = $.hashIdentifiers(.message.identifiers); + hashedIdentifiers; + + - name: prepareUserIds + description: Prepare user ids for user audience type + condition: $.outputs.getConfigs.audienceType === 'user' + template: | + const userIds = $.outputs.getConfigs.isHashRequired ? $.prepareUserIds($.outputs.hashIdentifiers) : $.prepareUserIds(.message.identifiers); + userIds; + + - name: prepareUserTypeBasePayload + description: Prepare base payload for user audiences + condition: $.outputs.getConfigs.audienceType === 'user' + template: | + const payload = { + 'elements': [ + { + 'action': $.generateActionType(.message.action), + 'userIds': $.outputs.prepareUserIds, + ....message.fields + } + ] + } + payload; + + - name: prepareCompanyTypeBasePayload + description: Prepare base payload for company audiences + condition: $.outputs.getConfigs.audienceType === 'company' + template: | + const payload = { + 'elements': [ + { + 'action': $.generateActionType(.message.action), + ....message.identifiers, + ....message.fields + } + ] + } + payload; + + - name: buildResponseForProcessTransformation + description: build response depending upon batch size + template: | + const response = $.defaultRequestConfig(); + response.body.JSON = {...$.outputs.prepareUserTypeBasePayload, ...$.outputs.prepareCompanyTypeBasePayload}; + response.endpoint = $.generateEndpoint($.outputs.getConfigs.audienceType, $.outputs.getConfigs.audienceId); + response.headers = { + "Authorization": "Bearer " + $.outputs.getConfigs.accessToken, + "Content-Type": "application/json", + "X-RestLi-Method": "BATCH_CREATE", + "X-Restli-Protocol-Version": "2.0.0", + "LinkedIn-Version": "202409" + }; + response; diff --git a/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml b/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml new file mode 100644 index 00000000000..2938b10c73d --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml @@ -0,0 +1,40 @@ +bindings: + - path: ./utils + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: transform + externalWorkflow: + path: ./procWorkflow.yaml + bindings: + - name: batchMode + value: true + loopOverInput: true + + - name: successfulEvents + template: | + $.outputs.transform#idx.output.({ + "message": .[], + "destination": ^ [idx].destination, + "metadata": ^ [idx].metadata + })[] + + - name: failedEvents + template: | + $.outputs.transform#idx.error.( + $.handleRtTfSingleEventError(^[idx], .originalError ?? ., {}) + )[] + + - name: batchSuccessfulEvents + description: Batches the successfulEvents + template: | + $.batchResponseBuilder($.outputs.successfulEvents); + + - name: finalPayload + template: | + [...$.outputs.failedEvents, ...$.outputs.batchSuccessfulEvents] diff --git a/src/cdk/v2/destinations/linkedin_audience/utils.ts b/src/cdk/v2/destinations/linkedin_audience/utils.ts new file mode 100644 index 00000000000..acbb604d564 --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_audience/utils.ts @@ -0,0 +1,87 @@ +const lodash = require('lodash'); +import { hashToSha256 } from '@rudderstack/integrations-lib'; +import { createHash } from 'crypto'; +import { BASE_ENDPOINT, COMPANY_ENDPOINT, FIELD_MAP, USER_ENDPOINT } from './config'; + +export function hashIdentifiers(identifiers: string[]): Record { + const hashedIdentifiers = {}; + Object.keys(identifiers).forEach((key) => { + if (key === 'sha256Email') { + hashedIdentifiers[key] = hashToSha256(identifiers[key]); + } else if (key === 'sha512Email') { + hashedIdentifiers[key] = createHash('sha512').update(identifiers[key]).digest('hex'); + } else { + hashedIdentifiers[key] = identifiers[key]; + } + }); + return hashedIdentifiers; +} + +export function prepareUserIds( + identifiers: Record, +): { idType: string; idValue: string }[] { + const userIds: { idType: string; idValue: string }[] = []; + Object.keys(identifiers).forEach((key) => { + userIds.push({ idType: FIELD_MAP[key], idValue: identifiers[key] }); + }); + return userIds; +} + +export function generateEndpoint(audienceType: string, audienceId: string) { + if (audienceType === 'user') { + return BASE_ENDPOINT + USER_ENDPOINT.replace('audienceId', audienceId); + } + return BASE_ENDPOINT + COMPANY_ENDPOINT.replace('audienceId', audienceId); +} + +export function batchResponseBuilder(successfulEvents) { + const chunkOnActionType = lodash.groupBy( + successfulEvents, + (event) => event.message[0].body.JSON.elements[0].action, + ); + const result: any = []; + Object.keys(chunkOnActionType).forEach((actionType) => { + const firstEvent = chunkOnActionType[actionType][0]; + const { method, endpoint, headers, type, version } = firstEvent.message[0]; + const batchEvent = { + batchedRequest: { + body: { + JSON: { elements: firstEvent.message[0].body.JSON.elements }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version, + type, + method, + endpoint, + headers, + params: {}, + files: {}, + }, + metadata: [firstEvent.metadata], + batched: true, + statusCode: 200, + destination: firstEvent.destination, + }; + firstEvent.metadata = [firstEvent.metadata]; + chunkOnActionType[actionType].forEach((element, index) => { + if (index !== 0) { + batchEvent.batchedRequest.body.JSON.elements.push(element.message[0].body.JSON.elements[0]); + batchEvent.metadata.push(element.metadata); + } + }); + result.push(batchEvent); + }); + return result; +} + +export const generateActionType = (actionType: string): string => { + if (actionType === 'insert') { + return 'ADD'; + } + if (actionType === 'delete') { + return 'REMOVE'; + } + return actionType; +}; diff --git a/src/features.json b/src/features.json index 63862eefed4..02cf33a6eae 100644 --- a/src/features.json +++ b/src/features.json @@ -83,7 +83,8 @@ "SMARTLY": true, "HTTP": true, "AMAZON_AUDIENCE": true, - "INTERCOM_V2": true + "INTERCOM_V2": true, + "LINKEDIN_AUDIENCE": true }, "regulations": [ "BRAZE", From 47bd9bd0764c2afc1d574c875313606049074bcb Mon Sep 17 00:00:00 2001 From: ItsSudip Date: Thu, 7 Nov 2024 14:15:38 +0530 Subject: [PATCH 2/5] chore: resolve conflicts --- src/controllers/misc.ts | 2 +- src/features.json | 106 ---------- src/features.ts | 117 +++++++++++ src/services/__tests__/misc.test.ts | 40 ++++ src/services/misc.ts | 8 +- .../transform.js | 32 ++- .../recordTransform.js | 71 ++++++- .../transform.js | 20 +- .../google_adwords_remarketing_lists/util.js | 47 +++-- .../util.test.js | 20 +- test/apitests/service.api.test.ts | 6 +- .../processor/data.ts | 190 ++++++++++++++++++ .../router/data.ts | 165 +++++++++++++++ .../processor/data.ts | 37 ++-- .../router/audience.ts | 2 +- .../router/data.ts | 12 +- .../router/record.ts | 2 +- 17 files changed, 698 insertions(+), 179 deletions(-) delete mode 100644 src/features.json create mode 100644 src/features.ts diff --git a/src/controllers/misc.ts b/src/controllers/misc.ts index e2efdab5db7..7f8ec52825d 100644 --- a/src/controllers/misc.ts +++ b/src/controllers/misc.ts @@ -21,7 +21,7 @@ export class MiscController { } public static features(ctx: Context) { - ctx.body = MiscService.getFetaures(); + ctx.body = MiscService.getFeatures(); ctx.status = 200; return ctx; } diff --git a/src/features.json b/src/features.json deleted file mode 100644 index 02cf33a6eae..00000000000 --- a/src/features.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "routerTransform": { - "ACTIVE_CAMPAIGN": true, - "ALGOLIA": true, - "CANDU": true, - "DELIGHTED": true, - "DRIP": true, - "FB_CUSTOM_AUDIENCE": true, - "GA": true, - "GAINSIGHT": true, - "GAINSIGHT_PX": true, - "GOOGLESHEETS": true, - "GOOGLE_ADWORDS_ENHANCED_CONVERSIONS": true, - "GOOGLE_ADWORDS_REMARKETING_LISTS": true, - "GOOGLE_ADWORDS_OFFLINE_CONVERSIONS": true, - "HS": true, - "ITERABLE": true, - "KLAVIYO": true, - "KUSTOMER": true, - "MAILCHIMP": true, - "MAILMODO": true, - "MARKETO": true, - "OMETRIA": true, - "PARDOT": true, - "PINTEREST_TAG": true, - "PROFITWELL": true, - "SALESFORCE": true, - "SALESFORCE_OAUTH": true, - "SALESFORCE_OAUTH_SANDBOX": true, - "SFMC": true, - "SNAPCHAT_CONVERSION": true, - "TIKTOK_ADS": true, - "TRENGO": true, - "YAHOO_DSP": true, - "CANNY": true, - "LAMBDA": true, - "WOOTRIC": true, - "GOOGLE_CLOUD_FUNCTION": true, - "BQSTREAM": true, - "CLICKUP": true, - "FRESHMARKETER": true, - "FRESHSALES": true, - "MONDAY": true, - "CUSTIFY": true, - "USER": true, - "REFINER": true, - "FACEBOOK_OFFLINE_CONVERSIONS": true, - "MAILJET": true, - "SNAPCHAT_CUSTOM_AUDIENCE": true, - "MARKETO_STATIC_LIST": true, - "CAMPAIGN_MANAGER": true, - "SENDGRID": true, - "SENDINBLUE": true, - "ZENDESK": true, - "MP": true, - "TIKTOK_ADS_OFFLINE_EVENTS": true, - "CRITEO_AUDIENCE": true, - "CUSTOMERIO": true, - "BRAZE": true, - "OPTIMIZELY_FULLSTACK": true, - "TWITTER_ADS": true, - "CLEVERTAP": true, - "ORTTO": true, - "GLADLY": true, - "ONE_SIGNAL": true, - "TIKTOK_AUDIENCE": true, - "REDDIT": true, - "THE_TRADE_DESK": true, - "INTERCOM": true, - "NINETAILED": true, - "KOALA": true, - "LINKEDIN_ADS": true, - "BLOOMREACH": true, - "MOVABLE_INK": true, - "EMARSYS": true, - "KODDI": true, - "WUNDERKIND": true, - "CLICKSEND": true, - "ZOHO": true, - "CORDIAL": true, - "X_AUDIENCE": true, - "BLOOMREACH_CATALOG": true, - "SMARTLY": true, - "HTTP": true, - "AMAZON_AUDIENCE": true, - "INTERCOM_V2": true, - "LINKEDIN_AUDIENCE": true - }, - "regulations": [ - "BRAZE", - "AM", - "INTERCOM", - "CLEVERTAP", - "AF", - "MP", - "GA", - "ITERABLE", - "ENGAGE", - "CUSTIFY", - "SENDGRID", - "SPRIG", - "EMARSYS" - ], - "supportSourceTransformV1": true, - "supportTransformerProxyV1": true -} diff --git a/src/features.ts b/src/features.ts new file mode 100644 index 00000000000..4ff419a7fe2 --- /dev/null +++ b/src/features.ts @@ -0,0 +1,117 @@ +interface FeaturesConfig { + routerTransform: Record; + regulations: string[]; + supportSourceTransformV1: boolean; + supportTransformerProxyV1: boolean; + upgradedToSourceTransformV2?: boolean; +} + +const defaultFeaturesConfig: FeaturesConfig = { + routerTransform: { + ACTIVE_CAMPAIGN: true, + ALGOLIA: true, + CANDU: true, + DELIGHTED: true, + DRIP: true, + FB_CUSTOM_AUDIENCE: true, + GA: true, + GAINSIGHT: true, + GAINSIGHT_PX: true, + GOOGLESHEETS: true, + GOOGLE_ADWORDS_ENHANCED_CONVERSIONS: true, + GOOGLE_ADWORDS_REMARKETING_LISTS: true, + GOOGLE_ADWORDS_OFFLINE_CONVERSIONS: true, + HS: true, + ITERABLE: true, + KLAVIYO: true, + KUSTOMER: true, + MAILCHIMP: true, + MAILMODO: true, + MARKETO: true, + OMETRIA: true, + PARDOT: true, + PINTEREST_TAG: true, + PROFITWELL: true, + SALESFORCE: true, + SALESFORCE_OAUTH: true, + SALESFORCE_OAUTH_SANDBOX: true, + SFMC: true, + SNAPCHAT_CONVERSION: true, + TIKTOK_ADS: true, + TRENGO: true, + YAHOO_DSP: true, + CANNY: true, + LAMBDA: true, + WOOTRIC: true, + GOOGLE_CLOUD_FUNCTION: true, + BQSTREAM: true, + CLICKUP: true, + FRESHMARKETER: true, + FRESHSALES: true, + MONDAY: true, + CUSTIFY: true, + USER: true, + REFINER: true, + FACEBOOK_OFFLINE_CONVERSIONS: true, + MAILJET: true, + SNAPCHAT_CUSTOM_AUDIENCE: true, + MARKETO_STATIC_LIST: true, + CAMPAIGN_MANAGER: true, + SENDGRID: true, + SENDINBLUE: true, + ZENDESK: true, + MP: true, + TIKTOK_ADS_OFFLINE_EVENTS: true, + CRITEO_AUDIENCE: true, + CUSTOMERIO: true, + BRAZE: true, + OPTIMIZELY_FULLSTACK: true, + TWITTER_ADS: true, + CLEVERTAP: true, + ORTTO: true, + GLADLY: true, + ONE_SIGNAL: true, + TIKTOK_AUDIENCE: true, + REDDIT: true, + THE_TRADE_DESK: true, + INTERCOM: true, + NINETAILED: true, + KOALA: true, + LINKEDIN_ADS: true, + BLOOMREACH: true, + MOVABLE_INK: true, + EMARSYS: true, + KODDI: true, + WUNDERKIND: true, + CLICKSEND: true, + ZOHO: true, + CORDIAL: true, + X_AUDIENCE: true, + BLOOMREACH_CATALOG: true, + SMARTLY: true, + HTTP: true, + AMAZON_AUDIENCE: true, + INTERCOM_V2: true, + LINKEDIN_AUDIENCE: true, + }, + regulations: [ + 'BRAZE', + 'AM', + 'INTERCOM', + 'CLEVERTAP', + 'AF', + 'MP', + 'GA', + 'ITERABLE', + 'ENGAGE', + 'CUSTIFY', + 'SENDGRID', + 'SPRIG', + 'EMARSYS', + ], + supportSourceTransformV1: true, + supportTransformerProxyV1: true, + upgradedToSourceTransformV2: process.env.UPGRADED_TO_SOURCE_TRANSFORM_V2 === 'true' || false, // redundant but required to show that the default is false +}; + +export default defaultFeaturesConfig; diff --git a/src/services/__tests__/misc.test.ts b/src/services/__tests__/misc.test.ts index 5dcd948b34e..f4befd4a042 100644 --- a/src/services/__tests__/misc.test.ts +++ b/src/services/__tests__/misc.test.ts @@ -1,4 +1,5 @@ import { DestHandlerMap } from '../../constants/destinationCanonicalNames'; +import defaultFeaturesConfig from '../../features'; import { MiscService } from '../misc'; describe('Misc tests', () => { @@ -24,3 +25,42 @@ describe('Misc tests', () => { ); }); }); + +describe('Misc | getFeatures', () => { + const originalEnv = process.env; + + beforeEach(() => { + // Reset environment variables and module cache before each test + process.env = { ...originalEnv }; + jest.resetModules(); + }); + + afterAll(() => { + // Restore the original environment variables after all tests + process.env = originalEnv; + }); + + function getMiscService() { + // Re-import config and featuresService after environment variables are set + const { MiscService: miscService } = require('../misc'); + return miscService; + } + + it('should return the default configuration as a JSON string', () => { + const miscService = getMiscService(); + const expectedConfig = JSON.stringify(defaultFeaturesConfig); + const result = miscService.getFeatures(); + expect(result).toBe(expectedConfig); + }); + + it('should return configuration with upgradedToSourceTransformV2 overridden by environment variable', () => { + process.env.UPGRADED_TO_SOURCE_TRANSFORM_V2 = 'true'; + const expectedConfig = { + ...defaultFeaturesConfig, + upgradedToSourceTransformV2: true, + }; + const miscService = getMiscService(); + const result = miscService.getFeatures(); + expect(result).toBe(JSON.stringify(expectedConfig)); + }); +}); diff --git a/src/services/misc.ts b/src/services/misc.ts index 09051edeec3..334b54ba17b 100644 --- a/src/services/misc.ts +++ b/src/services/misc.ts @@ -1,10 +1,9 @@ /* eslint-disable global-require, import/no-dynamic-require */ -import fs from 'fs'; import { Context } from 'koa'; -import path from 'path'; import { DestHandlerMap } from '../constants/destinationCanonicalNames'; import { getCPUProfile, getHeapProfile } from '../middleware'; import { Metadata } from '../types'; +import defaultFeaturesConfig from '../features'; export class MiscService { public static getDestHandler(dest: string, version: string) { @@ -62,9 +61,8 @@ export class MiscService { return process.env.npm_package_version || 'Version Info not found'; } - public static getFetaures() { - const obj = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../features.json'), 'utf8')); - return JSON.stringify(obj); + public static getFeatures() { + return JSON.stringify(defaultFeaturesConfig); } public static async getCPUProfile(seconds: number) { diff --git a/src/v0/destinations/google_adwords_enhanced_conversions/transform.js b/src/v0/destinations/google_adwords_enhanced_conversions/transform.js index 007f16d7f8e..13a294ea959 100644 --- a/src/v0/destinations/google_adwords_enhanced_conversions/transform.js +++ b/src/v0/destinations/google_adwords_enhanced_conversions/transform.js @@ -2,7 +2,11 @@ const get = require('get-value'); const { cloneDeep, isNumber } = require('lodash'); -const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); +const { + InstrumentationError, + ConfigurationError, + isDefinedAndNotNull, +} = require('@rudderstack/integrations-lib'); const isString = require('lodash/isString'); const { constructPayload, @@ -11,6 +15,7 @@ const { removeHyphens, simpleProcessRouterDest, getAccessToken, + isDefined, } = require('../../util'); const { trackMapping, BASE_ENDPOINT } = require('./config'); @@ -38,6 +43,16 @@ const responseBuilder = async (metadata, message, { Config }, payload) => { const { event } = message; const { subAccount } = Config; let { customerId, loginCustomerId } = Config; + const { configData } = Config; + + if (isDefinedAndNotNull(configData)) { + const configDetails = JSON.parse(configData); + customerId = configDetails.customerId; + if (isDefined(configDetails.loginCustomerId)) { + loginCustomerId = configDetails.loginCustomerId; + } + } + if (isNumber(customerId)) { customerId = customerId.toString(); } @@ -63,6 +78,11 @@ const responseBuilder = async (metadata, message, { Config }, payload) => { response.headers['login-customer-id'] = filteredLoginCustomerId; } else throw new ConfigurationError(`LoginCustomerId is required as subAccount is true.`); + if (loginCustomerId) { + const filteredLoginCustomerId = removeHyphens(loginCustomerId); + response.headers['login-customer-id'] = filteredLoginCustomerId; + } + return response; }; @@ -71,8 +91,14 @@ const processTrackEvent = async (metadata, message, destination) => { const { Config } = destination; const { event } = message; const { listOfConversions } = Config; - if (listOfConversions.some((i) => i.conversions === event)) { - flag = 1; + if (listOfConversions && listOfConversions.length > 0) { + if (typeof listOfConversions[0] === 'string') { + if (listOfConversions.includes(event)) { + flag = 1; + } + } else if (listOfConversions.some((i) => i.conversions === event)) { + flag = 1; + } } if (event === undefined || event === '' || flag === 0) { throw new ConfigurationError( diff --git a/src/v0/destinations/google_adwords_remarketing_lists/recordTransform.js b/src/v0/destinations/google_adwords_remarketing_lists/recordTransform.js index b05ddb07a28..f8a2b0e5862 100644 --- a/src/v0/destinations/google_adwords_remarketing_lists/recordTransform.js +++ b/src/v0/destinations/google_adwords_remarketing_lists/recordTransform.js @@ -7,9 +7,11 @@ const { constructPayload, returnArrayOfSubarrays, getSuccessRespEvents, + isEventSentByVDMV1Flow, + isEventSentByVDMV2Flow, } = require('../../util'); const { populateConsentFromConfig } = require('../../util/googleUtils'); -const { populateIdentifiers, responseBuilder } = require('./util'); +const { populateIdentifiers, responseBuilder, getOperationAudienceId } = require('./util'); const { getErrorResponse, createFinalResponse } = require('../../util/recordUtils'); const { offlineDataJobsMapping, consentConfigMap } = require('./config'); @@ -19,6 +21,9 @@ const processRecordEventArray = ( destination, accessToken, developerToken, + audienceId, + typeOfList, + isHashRequired, operationType, ) => { let outputPayloads = {}; @@ -31,7 +36,12 @@ const processRecordEventArray = ( metadata.push(record.metadata); }); - const userIdentifiersList = populateIdentifiers(fieldsArray, destination); + const userIdentifiersList = populateIdentifiers( + fieldsArray, + destination, + typeOfList, + isHashRequired, + ); const outputPayload = constructPayload(message, offlineDataJobsMapping); outputPayload.operations = []; @@ -68,7 +78,7 @@ const processRecordEventArray = ( Object.values(outputPayloads).forEach((data) => { const consentObj = populateConsentFromConfig(destination.Config, consentConfigMap); toSendEvents.push( - responseBuilder(accessToken, developerToken, data, destination, message, consentObj), + responseBuilder(accessToken, developerToken, data, destination, audienceId, consentObj), ); }); @@ -77,12 +87,13 @@ const processRecordEventArray = ( return successResponse; }; -async function processRecordInputs(groupedRecordInputs) { - const { destination, message, metadata } = groupedRecordInputs[0]; +function preparepayload(events, config) { + const { destination, message, metadata } = events[0]; const accessToken = getAccessToken(metadata, 'access_token'); const developerToken = getValueFromMessage(metadata, 'secret.developer_token'); + const { audienceId, typeOfList, isHashRequired } = config; - const groupedRecordsByAction = lodash.groupBy(groupedRecordInputs, (record) => + const groupedRecordsByAction = lodash.groupBy(events, (record) => record.message.action?.toLowerCase(), ); @@ -97,6 +108,9 @@ async function processRecordInputs(groupedRecordInputs) { destination, accessToken, developerToken, + audienceId, + typeOfList, + isHashRequired, 'remove', ); } @@ -108,6 +122,9 @@ async function processRecordInputs(groupedRecordInputs) { destination, accessToken, developerToken, + audienceId, + typeOfList, + isHashRequired, 'add', ); } @@ -119,6 +136,9 @@ async function processRecordInputs(groupedRecordInputs) { destination, accessToken, developerToken, + audienceId, + typeOfList, + isHashRequired, 'add', ); } @@ -139,6 +159,45 @@ async function processRecordInputs(groupedRecordInputs) { return finalResponse; } +function processRecordInputsV0(groupedRecordInputs) { + const { destination, message } = groupedRecordInputs[0]; + const { audienceId, typeOfList, isHashRequired } = destination.Config; + + return preparepayload(groupedRecordInputs, { + audienceId: getOperationAudienceId(audienceId, message), + typeOfList, + isHashRequired, + }); +} + +function processRecordInputsV1(groupedRecordInputs) { + const { connection } = groupedRecordInputs[0]; + const { audienceId, typeOfList, isHashRequired } = connection.config.destination; + + const events = groupedRecordInputs.map((record) => ({ + ...record, + message: { + ...record.message, + fields: record.message.identifiers, + }, + })); + + return preparepayload(events, { + audienceId, + typeOfList, + isHashRequired, + }); +} + +function processRecordInputs(groupedRecordInputs) { + const event = groupedRecordInputs[0]; + // First check for rETL flow and second check for ES flow + if (isEventSentByVDMV1Flow(event) || !isEventSentByVDMV2Flow(event)) { + return processRecordInputsV0(groupedRecordInputs); + } + return processRecordInputsV1(groupedRecordInputs); +} + module.exports = { processRecordInputs, }; diff --git a/src/v0/destinations/google_adwords_remarketing_lists/transform.js b/src/v0/destinations/google_adwords_remarketing_lists/transform.js index 3deb9be7759..299ab948461 100644 --- a/src/v0/destinations/google_adwords_remarketing_lists/transform.js +++ b/src/v0/destinations/google_adwords_remarketing_lists/transform.js @@ -12,7 +12,7 @@ const { const { populateConsentFromConfig } = require('../../util/googleUtils'); const { offlineDataJobsMapping, consentConfigMap } = require('./config'); const { processRecordInputs } = require('./recordTransform'); -const { populateIdentifiers, responseBuilder } = require('./util'); +const { populateIdentifiers, responseBuilder, getOperationAudienceId } = require('./util'); function extraKeysPresent(dictionary, keyList) { // eslint-disable-next-line no-restricted-syntax @@ -37,12 +37,18 @@ function extraKeysPresent(dictionary, keyList) { const createPayload = (message, destination) => { const { listData } = message.properties; const properties = ['add', 'remove']; + const { typeOfList, isHashRequired } = destination.Config; let outputPayloads = {}; const typeOfOperation = Object.keys(listData); typeOfOperation.forEach((key) => { if (properties.includes(key)) { - const userIdentifiersList = populateIdentifiers(listData[key], destination); + const userIdentifiersList = populateIdentifiers( + listData[key], + destination, + typeOfList, + isHashRequired, + ); if (userIdentifiersList.length === 0) { logger.info( `Google_adwords_remarketing_list]:: No attributes are present in the '${key}' property.`, @@ -113,8 +119,16 @@ const processEvent = async (metadata, message, destination) => { Object.values(createdPayload).forEach((data) => { const consentObj = populateConsentFromConfig(destination.Config, consentConfigMap); + const { audienceId } = destination.Config; response.push( - responseBuilder(accessToken, developerToken, data, destination, message, consentObj), + responseBuilder( + accessToken, + developerToken, + data, + destination, + getOperationAudienceId(audienceId, message), + consentObj, + ), ); }); return response; diff --git a/src/v0/destinations/google_adwords_remarketing_lists/util.js b/src/v0/destinations/google_adwords_remarketing_lists/util.js index 3e04dd8f6f6..f4c33a9a6fb 100644 --- a/src/v0/destinations/google_adwords_remarketing_lists/util.js +++ b/src/v0/destinations/google_adwords_remarketing_lists/util.js @@ -29,26 +29,24 @@ const hashEncrypt = (object) => { }); }; -const responseBuilder = (accessToken, developerToken, body, { Config }, message, consentBlock) => { +const responseBuilder = ( + accessToken, + developerToken, + body, + { Config }, + audienceId, + consentBlock, +) => { const payload = body; const response = defaultRequestConfig(); const filteredCustomerId = removeHyphens(Config.customerId); response.endpoint = `${BASE_ENDPOINT}/${filteredCustomerId}/offlineUserDataJobs`; response.body.JSON = removeUndefinedAndNullValues(payload); - let operationAudienceId = Config.audienceId || Config.listId; - const mappedToDestination = get(message, MappedToDestinationKey); - if (!operationAudienceId && mappedToDestination) { - const { objectType } = getDestinationExternalIDInfoForRetl( - message, - 'GOOGLE_ADWORDS_REMARKETING_LISTS', - ); - operationAudienceId = objectType; - } - if (!isDefinedAndNotNullAndNotEmpty(operationAudienceId)) { + if (!isDefinedAndNotNullAndNotEmpty(audienceId)) { throw new ConfigurationError('List ID is a mandatory field'); } response.params = { - listId: operationAudienceId, + listId: audienceId, customerId: filteredCustomerId, consent: consentBlock, }; @@ -69,14 +67,15 @@ const responseBuilder = (accessToken, developerToken, body, { Config }, message, * This function helps creates an array with proper mapping for userIdentiFier. * Logics: Here we are creating an array with all the attributes provided in the add/remove array * inside listData. - * @param {rudder event message properties listData add} attributeArray - * @param {rudder event destination} Config + * @param {Array} attributeArray rudder event message properties listData add + * @param {object} Config rudder event destination + * @param {string} typeOfList + * @param {boolean} isHashRequired * @returns */ -const populateIdentifiers = (attributeArray, { Config }) => { +const populateIdentifiers = (attributeArray, { Config }, typeOfList, isHashRequired) => { const userIdentifier = []; - const { typeOfList } = Config; - const { isHashRequired, userSchema } = Config; + const { userSchema } = Config; let attribute; if (TYPEOFLIST[typeOfList]) { attribute = TYPEOFLIST[typeOfList]; @@ -116,7 +115,21 @@ const populateIdentifiers = (attributeArray, { Config }) => { return userIdentifier; }; +const getOperationAudienceId = (audienceId, message) => { + let operationAudienceId = audienceId; + const mappedToDestination = get(message, MappedToDestinationKey); + if (!operationAudienceId && mappedToDestination) { + const { objectType } = getDestinationExternalIDInfoForRetl( + message, + 'GOOGLE_ADWORDS_REMARKETING_LISTS', + ); + operationAudienceId = objectType; + } + return operationAudienceId; +}; + module.exports = { populateIdentifiers, responseBuilder, + getOperationAudienceId, }; diff --git a/src/v0/destinations/google_adwords_remarketing_lists/util.test.js b/src/v0/destinations/google_adwords_remarketing_lists/util.test.js index a5897776c09..0b74b07b8e7 100644 --- a/src/v0/destinations/google_adwords_remarketing_lists/util.test.js +++ b/src/v0/destinations/google_adwords_remarketing_lists/util.test.js @@ -1,4 +1,4 @@ -const { populateIdentifiers, responseBuilder } = require('./util'); +const { populateIdentifiers, responseBuilder, getOperationAudienceId } = require('./util'); const { API_VERSION } = require('./config'); const accessToken = 'abcd1234'; const developerToken = 'ijkl9101'; @@ -29,7 +29,7 @@ const body = { const baseDestination = { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -150,7 +150,7 @@ describe('GARL utils test', () => { developerToken, body, baseDestination, - message, + getOperationAudienceId(baseDestination.Config.audienceId, message), consentBlock, ); expect(response).toEqual(expectedResponse); @@ -166,7 +166,7 @@ describe('GARL utils test', () => { developerToken, body, destination2, - message, + getOperationAudienceId(baseDestination.Config.audienceId, message), consentBlock, ); expect(response).toEqual(); @@ -178,13 +178,13 @@ describe('GARL utils test', () => { it('Should throw error if operationAudienceId is not defined', () => { try { const destination1 = Object.create(baseDestination); - destination1.Config.listId = ''; + destination1.Config.audienceId = ''; const response = responseBuilder( accessToken, developerToken, body, destination1, - message, + getOperationAudienceId(baseDestination.Config.audienceId, message), consentBlock, ); expect(response).toEqual(); @@ -196,7 +196,13 @@ describe('GARL utils test', () => { describe('populateIdentifiers function tests', () => { it('Should hash and return identifiers for a given list of attributes', () => { - const identifier = populateIdentifiers(attributeArray, baseDestination); + const { typeOfList, isHashRequired } = baseDestination.Config; + const identifier = populateIdentifiers( + attributeArray, + baseDestination, + typeOfList, + isHashRequired, + ); expect(identifier).toEqual(hashedArray); }); }); diff --git a/test/apitests/service.api.test.ts b/test/apitests/service.api.test.ts index 2a0db6978e0..9c1d96e7fe5 100644 --- a/test/apitests/service.api.test.ts +++ b/test/apitests/service.api.test.ts @@ -8,6 +8,7 @@ import request from 'supertest'; import networkHandlerFactory from '../../src/adapters/networkHandlerFactory'; import { FetchHandler } from '../../src/helpers/fetchHandlers'; import { applicationRoutes } from '../../src/routes'; +import defaultFeaturesConfig from '../../src/features'; let server: any; const OLD_ENV = process.env; @@ -43,12 +44,9 @@ const getDataFromPath = (pathInput) => { describe('features tests', () => { test('successful features response', async () => { - const expectedData = JSON.parse( - fs.readFileSync(path.resolve(__dirname, '../../src/features.json'), 'utf8'), - ); const response = await request(server).get('/features'); expect(response.status).toEqual(200); - expect(JSON.parse(response.text)).toEqual(expectedData); + expect(JSON.parse(response.text)).toEqual(defaultFeaturesConfig); }); test('features regulations should be array', async () => { diff --git a/test/integrations/destinations/google_adwords_enhanced_conversions/processor/data.ts b/test/integrations/destinations/google_adwords_enhanced_conversions/processor/data.ts index 40d8370fcb7..87fad8b9a54 100644 --- a/test/integrations/destinations/google_adwords_enhanced_conversions/processor/data.ts +++ b/test/integrations/destinations/google_adwords_enhanced_conversions/processor/data.ts @@ -1720,4 +1720,194 @@ export const data = [ }, }, }, + { + name: 'google_adwords_enhanced_conversions', + description: 'Success test with configDetails', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + }, + destination: { + Config: { + rudderAccountId: '25u5whFH7gVTnCiAjn4ykoCLGoC', + listOfConversions: ['Page View', 'Product Added'], + authStatus: 'active', + configData: '{"customerId": "1234567890", "loginCustomerId": ""}', + }, + }, + message: { + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + traits: { + phone: '912382193', + firstName: 'John', + lastName: 'Gomes', + city: 'London', + state: 'UK', + countryCode: 'us', + streetAddress: '71 Cherry Court SOUTHAMPTON SO53 5PD UK', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + locale: 'en-US', + ip: '0.0.0.0', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + }, + event: 'Page View', + type: 'track', + messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', + originalTimestamp: '2019-10-14T11:15:18.299Z', + anonymousId: '00000000000000000000000000', + userId: '12345', + properties: { + gclid: 'gclid1234', + conversionDateTime: '2022-01-01 12:32:45-08:00', + adjustedValue: '10', + currency: 'INR', + adjustmentDateTime: '2022-01-01 12:32:45-08:00', + partialFailure: true, + campaignId: '1', + templateId: '0', + order_id: 10000, + total: 1000, + products: [ + { + product_id: '507f1f77bcf86cd799439011', + sku: '45790-32', + name: 'Monopoly: 3rd Edition', + price: '19', + position: '1', + category: 'cars', + url: 'https://www.example.com/product/path', + image_url: 'https://www.example.com/product/path.jpg', + quantity: '2', + }, + { + product_id: '507f1f77bcf86cd7994390112', + sku: '45790-322', + name: 'Monopoly: 3rd Edition2', + price: '192', + quantity: 22, + position: '12', + category: 'Cars2', + url: 'https://www.example.com/product/path2', + image_url: 'https://www.example.com/product/path.jpg2', + }, + ], + }, + integrations: { + All: true, + }, + name: 'ApplicationLoaded', + sentAt: '2019-10-14T11:15:53.296Z', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://googleads.googleapis.com/${API_VERSION}/customers/1234567890:uploadConversionAdjustments`, + headers: { + Authorization: 'Bearer abcd1234', + 'Content-Type': 'application/json', + 'developer-token': 'ijkl91011', + }, + params: { + event: 'Page View', + customerId: '1234567890', + }, + body: { + JSON: { + conversionAdjustments: [ + { + gclidDateTimePair: { + gclid: 'gclid1234', + conversionDateTime: '2022-01-01 12:32:45-08:00', + }, + restatementValue: { + adjustedValue: 10, + currencyCode: 'INR', + }, + orderId: '10000', + adjustmentDateTime: '2022-01-01 12:32:45-08:00', + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + userIdentifiers: [ + { + hashedPhoneNumber: + '04387707e6cbed8c4538c81cc570ed9252d579469f36c273839b26d784e4bdbe', + }, + { + addressInfo: { + hashedFirstName: + 'a8cfcd74832004951b4408cdb0a5dbcd8c7e52d43f7fe244bf720582e05241da', + hashedLastName: + '1c574b17eefa532b6d61c963550a82d2d3dfca4a7fb69e183374cfafd5328ee4', + state: 'UK', + city: 'London', + countryCode: 'us', + hashedStreetAddress: + '9a4d2e50828448f137f119a3ebdbbbab8d6731234a67595fdbfeb2a2315dd550', + }, + }, + ], + adjustmentType: 'ENHANCEMENT', + }, + ], + partialFailure: true, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + }, + statusCode: 200, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/google_adwords_enhanced_conversions/router/data.ts b/test/integrations/destinations/google_adwords_enhanced_conversions/router/data.ts index 89ce06818b4..33cb4a832fc 100644 --- a/test/integrations/destinations/google_adwords_enhanced_conversions/router/data.ts +++ b/test/integrations/destinations/google_adwords_enhanced_conversions/router/data.ts @@ -413,6 +413,91 @@ const events = [ sentAt: '2019-10-14T11:15:53.296Z', }, }, + { + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + jobId: 6, + userId: 'u1', + }, + destination: { + Config: { + rudderAccountId: '25u5whFH7gVTnCiAjn4ykoCLGoC', + configData: '{"customerId":"1234567890", "loginCustomerId":"65656565"}', + listOfConversions: [{ conversions: 'Page View' }, { conversions: 'Product Added' }], + authStatus: 'active', + }, + }, + message: { + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + traits: { + phone: '912382193', + firstName: 'John', + lastName: 'Gomes', + city: 'London', + state: 'UK', + streetAddress: '71 Cherry Court SOUTHAMPTON SO53 5PD UK', + }, + library: { name: 'RudderLabs JavaScript SDK', version: '1.0.0' }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + locale: 'en-US', + }, + event: 'Page View', + type: 'track', + messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', + originalTimestamp: '2019-10-14T11:15:18.299Z', + userId: '12345', + properties: { + gclid: 'gclid1234', + conversionDateTime: '2022-01-01 12:32:45-08:00', + adjustedValue: '10', + currency: 'INR', + adjustmentDateTime: '2022-01-01 12:32:45-08:00', + partialFailure: true, + campaignId: '1', + templateId: '0', + order_id: 10000, + total: 1000, + products: [ + { + product_id: '507f1f77bcf86cd799439011', + sku: '45790-32', + name: 'Monopoly: 3rd Edition', + price: '19', + position: '1', + category: 'cars', + url: 'https://www.example.com/product/path', + image_url: 'https://www.example.com/product/path.jpg', + quantity: '2', + }, + { + product_id: '507f1f77bcf86cd7994390112', + sku: '45790-322', + name: 'Monopoly: 3rd Edition2', + price: '192', + quantity: 22, + position: '12', + category: 'Cars2', + url: 'https://www.example.com/product/path2', + image_url: 'https://www.example.com/product/path.jpg2', + }, + ], + }, + integrations: { All: true }, + name: 'ApplicationLoaded', + }, + }, ]; const invalidRtTfCases = [ @@ -807,6 +892,86 @@ export const data = [ }, }, }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://googleads.googleapis.com/${API_VERSION}/customers/1234567890:uploadConversionAdjustments`, + headers: { + Authorization: 'Bearer abcd1234', + 'Content-Type': 'application/json', + 'developer-token': 'ijkl91011', + 'login-customer-id': '65656565', + }, + params: { event: 'Page View', customerId: '1234567890' }, + body: { + JSON: { + partialFailure: true, + conversionAdjustments: [ + { + gclidDateTimePair: { + gclid: 'gclid1234', + conversionDateTime: '2022-01-01 12:32:45-08:00', + }, + restatementValue: { adjustedValue: 10, currencyCode: 'INR' }, + orderId: '10000', + adjustmentDateTime: '2022-01-01 12:32:45-08:00', + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + userIdentifiers: [ + { + hashedPhoneNumber: + '04387707e6cbed8c4538c81cc570ed9252d579469f36c273839b26d784e4bdbe', + }, + { + addressInfo: { + hashedFirstName: + 'a8cfcd74832004951b4408cdb0a5dbcd8c7e52d43f7fe244bf720582e05241da', + hashedLastName: + '1c574b17eefa532b6d61c963550a82d2d3dfca4a7fb69e183374cfafd5328ee4', + state: 'UK', + city: 'London', + hashedStreetAddress: + '9a4d2e50828448f137f119a3ebdbbbab8d6731234a67595fdbfeb2a2315dd550', + }, + }, + ], + adjustmentType: 'ENHANCEMENT', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [ + { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + jobId: 6, + userId: 'u1', + }, + ], + batched: false, + statusCode: 200, + destination: { + Config: { + configData: '{"customerId":"1234567890", "loginCustomerId":"65656565"}', + rudderAccountId: '25u5whFH7gVTnCiAjn4ykoCLGoC', + listOfConversions: [ + { conversions: 'Page View' }, + { conversions: 'Product Added' }, + ], + authStatus: 'active', + }, + }, + }, ], }, }, diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts b/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts index e20ed895455..4398bc14e16 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts @@ -20,7 +20,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -151,7 +151,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '769-372-9833', loginCustomerId: '870-483-0944', subAccount: true, @@ -269,7 +269,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -387,7 +387,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -495,7 +495,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -585,7 +585,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -673,7 +673,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -749,7 +749,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: true, @@ -845,7 +845,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -2740,7 +2740,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -2993,7 +2993,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -6724,7 +6724,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -6880,7 +6880,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -6977,7 +6977,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -10708,7 +10708,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -10961,7 +10961,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -11207,7 +11207,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -11328,7 +11328,7 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', + audienceId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -11412,7 +11412,6 @@ export const data = [ destination: { Config: { rudderAccountId: 'rudder-acc-id', - listId: 'list111', customerId: '7693729833', loginCustomerId: '', subAccount: false, diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/router/audience.ts b/test/integrations/destinations/google_adwords_remarketing_lists/router/audience.ts index e0b534bb15d..2158a57a5d2 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/router/audience.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/router/audience.ts @@ -4,7 +4,7 @@ import { generateGoogleOAuthMetadata } from '../../../testUtils'; const destination: Destination = { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts b/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts index f5789bf7ef7..a5e28996b10 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts @@ -92,7 +92,7 @@ export const data = [ destination: { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -187,7 +187,7 @@ export const data = [ destination: { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -332,7 +332,7 @@ export const data = [ destination: { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -451,7 +451,7 @@ export const data = [ destination: { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -578,7 +578,7 @@ export const data = [ destination: { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, @@ -675,7 +675,7 @@ export const data = [ destination: { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/router/record.ts b/test/integrations/destinations/google_adwords_remarketing_lists/router/record.ts index bedf1128664..de76aae17c8 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/router/record.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/router/record.ts @@ -4,7 +4,7 @@ import { generateGoogleOAuthMetadata } from '../../../testUtils'; const destination: Destination = { Config: { rudderAccountId: '258Yea7usSKNpbkIaesL9oJ9iYw', - listId: '7090784486', + audienceId: '7090784486', customerId: '7693729833', loginCustomerId: '', subAccount: false, From b718d52e22c6bcd20c6f5a7e6091ab5e795e0bd6 Mon Sep 17 00:00:00 2001 From: ItsSudip Date: Mon, 11 Nov 2024 11:21:18 +0530 Subject: [PATCH 3/5] chore: add test cases --- .../linkedin_audience/rtWorkflow.yaml | 2 +- .../linkedin_audience/processor/business.ts | 520 ++++++++++++++++++ .../linkedin_audience/processor/data.ts | 3 + .../linkedin_audience/processor/validation.ts | 396 +++++++++++++ .../linkedin_audience/router/data.ts | 384 +++++++++++++ test/integrations/testUtils.ts | 24 + 6 files changed, 1328 insertions(+), 1 deletion(-) create mode 100644 test/integrations/destinations/linkedin_audience/processor/business.ts create mode 100644 test/integrations/destinations/linkedin_audience/processor/data.ts create mode 100644 test/integrations/destinations/linkedin_audience/processor/validation.ts create mode 100644 test/integrations/destinations/linkedin_audience/router/data.ts diff --git a/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml b/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml index 2938b10c73d..fe16ab786a5 100644 --- a/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml +++ b/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml @@ -37,4 +37,4 @@ steps: - name: finalPayload template: | - [...$.outputs.failedEvents, ...$.outputs.batchSuccessfulEvents] + [...$.outputs.batchSuccessfulEvents, ...$.outputs.failedEvents] diff --git a/test/integrations/destinations/linkedin_audience/processor/business.ts b/test/integrations/destinations/linkedin_audience/processor/business.ts new file mode 100644 index 00000000000..fe12857a629 --- /dev/null +++ b/test/integrations/destinations/linkedin_audience/processor/business.ts @@ -0,0 +1,520 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata, generateRecordPayload } from '../../../testUtils'; + +export const businessTestData: ProcessorTestData[] = [ + { + id: 'linkedin_audience-business-test-1', + name: 'linkedin_audience', + description: 'Record call : non string values provided as email', + scenario: 'Business', + successCriteria: 'should fail with 400 status code and error message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 12345, + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'The "string" argument must be of type string. Received type number (12345): Workflow: procWorkflow, Step: hashIdentifiers, ChildStep: undefined, OriginalError: The "string" argument must be of type string. Received type number (12345)', + metadata: generateMetadata(1), + statTags: { + destType: 'LINKEDIN_AUDIENCE', + destinationId: 'default-destinationId', + errorCategory: 'transformation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + statusCode: 500, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-business-test-2', + name: 'linkedin_audience', + description: 'Record call : Valid event without any field mappings', + scenario: 'Business', + successCriteria: 'should pass with 200 status code and transformed message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: {}, + identifiers: { + sha256Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-business-test-2', + name: 'linkedin_audience', + description: 'Record call : customer provided hashed value and isHashRequired is false', + scenario: 'Business', + successCriteria: 'should pass with 200 status code and transformed message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: {}, + identifiers: { + sha256Email: '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + sha512Email: + '631372c5eafe80f3fe1b5d067f6a1870f1f04a0f0c0d9298eeaa20b9e54224da9588e3164d2ec6e2a5545a5299ed7df563e4a60315e6782dfa7db4de6b1c5326', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: false, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + { + idType: 'SHA512_EMAIL', + idValue: + '631372c5eafe80f3fe1b5d067f6a1870f1f04a0f0c0d9298eeaa20b9e54224da9588e3164d2ec6e2a5545a5299ed7df563e4a60315e6782dfa7db4de6b1c5326', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-business-test-2', + name: 'linkedin_audience', + description: 'Record call : event with company audience details', + scenario: 'Business', + successCriteria: 'should pass with 200 status code and transformed message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + city: 'Dhaka', + state: 'Dhaka', + industries: 'Information Technology', + postalCode: '123456', + }, + identifiers: { + companyName: 'Rudderstack', + organizationUrn: 'urn:li:organization:456', + companyWebsiteDomain: 'rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'company', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'city', + to: 'city', + }, + { + from: 'state', + to: 'state', + }, + { + from: 'domain', + to: 'industries', + }, + { + from: 'psCode', + to: 'postalCode', + }, + ], + identifierMappings: [ + { + from: 'name', + to: 'companyName', + }, + { + from: 'urn', + to: 'organizationUrn', + }, + { + from: 'Website Domain', + to: 'companyWebsiteDomain', + }, + ], + isHashRequired: false, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + city: 'Dhaka', + companyName: 'Rudderstack', + companyWebsiteDomain: 'rudderstack.com', + industries: 'Information Technology', + organizationUrn: 'urn:li:organization:456', + postalCode: '123456', + state: 'Dhaka', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/companies', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_audience/processor/data.ts b/test/integrations/destinations/linkedin_audience/processor/data.ts new file mode 100644 index 00000000000..233a9dcf861 --- /dev/null +++ b/test/integrations/destinations/linkedin_audience/processor/data.ts @@ -0,0 +1,3 @@ +import { businessTestData } from './business'; +import { validationTestData } from './validation'; +export const data = [...validationTestData, ...businessTestData]; diff --git a/test/integrations/destinations/linkedin_audience/processor/validation.ts b/test/integrations/destinations/linkedin_audience/processor/validation.ts new file mode 100644 index 00000000000..3ad37b2f4de --- /dev/null +++ b/test/integrations/destinations/linkedin_audience/processor/validation.ts @@ -0,0 +1,396 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata, generateRecordPayload } from '../../../testUtils'; + +export const validationTestData: ProcessorTestData[] = [ + { + id: 'linkedin_audience-validation-test-1', + name: 'linkedin_audience', + description: 'Record call : event is valid with all required elements', + scenario: 'Validation', + successCriteria: 'should pass with 200 status code and transformed message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 'random@rudderstack.com', + sha512Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + company: 'Rudderlabs', + country: 'Dhaka', + firstName: 'Test', + lastName: 'User', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + { + idType: 'SHA512_EMAIL', + idValue: + '631372c5eafe80f3fe1b5d067f6a1870f1f04a0f0c0d9298eeaa20b9e54224da9588e3164d2ec6e2a5545a5299ed7df563e4a60315e6782dfa7db4de6b1c5326', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-validation-test-2', + name: 'linkedin_audience', + description: 'Record call : event is not valid with all required elements', + scenario: 'Validation', + successCriteria: 'should fail with 400 status code and error message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 'random@rudderstack.com', + sha512Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Audience Id is not present. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Audience Id is not present. Aborting', + metadata: generateMetadata(1), + statTags: { + destType: 'LINKEDIN_AUDIENCE', + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'configuration', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-validation-test-3', + name: 'linkedin_audience', + description: 'Record call : isHashRequired is not provided', + scenario: 'Validation', + successCriteria: + 'should succeed with 200 status code and transformed message with provided values of identifiers', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 'random@rudderstack.com', + sha512Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 1234, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + company: 'Rudderlabs', + country: 'Dhaka', + firstName: 'Test', + lastName: 'User', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'random@rudderstack.com', + }, + { + idType: 'SHA512_EMAIL', + idValue: 'random@rudderstack.com', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/1234/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_audience/router/data.ts b/test/integrations/destinations/linkedin_audience/router/data.ts new file mode 100644 index 00000000000..c76d3e84c6f --- /dev/null +++ b/test/integrations/destinations/linkedin_audience/router/data.ts @@ -0,0 +1,384 @@ +import { generateMetadata, generateRecordPayload } from '../../../testUtils'; + +export const data = [ + { + name: 'linkedin_audience', + description: 'Test 0', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 'random@rudderstack.com', + sha512Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + { + message: generateRecordPayload({ + fields: {}, + identifiers: { + sha256Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(2), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 12345, + }, + action: 'insert', + }), + metadata: generateMetadata(3), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + destType: 'linkedin_audience', + }, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: true, + batchedRequest: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + company: 'Rudderlabs', + country: 'Dhaka', + firstName: 'Test', + lastName: 'User', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + { + idType: 'SHA512_EMAIL', + idValue: + '631372c5eafe80f3fe1b5d067f6a1870f1f04a0f0c0d9298eeaa20b9e54224da9588e3164d2ec6e2a5545a5299ed7df563e4a60315e6782dfa7db4de6b1c5326', + }, + ], + }, + { + action: 'ADD', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: { + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + DisplayName: 'Linkedin Audience', + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + }, + Enabled: true, + ID: '123', + Name: 'Linkedin Audience', + Transformations: [], + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + }, + metadata: [ + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + jobId: 1, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + }, + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + jobId: 2, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + }, + ], + statusCode: 200, + }, + { + batched: false, + destination: { + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + DisplayName: 'Linkedin Audience', + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + }, + Enabled: true, + ID: '123', + Name: 'Linkedin Audience', + Transformations: [], + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + }, + error: 'The "string" argument must be of type string. Received type number (12345)', + metadata: [ + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + jobId: 3, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + }, + ], + statTags: { + destType: 'LINKEDIN_AUDIENCE', + destinationId: 'default-destinationId', + errorCategory: 'transformation', + feature: 'router', + implementation: 'cdkV2', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + statusCode: 500, + }, + ], + }, + }, + }, + }, +]; diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 4eda20a9011..7e6e6b9acbb 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -237,6 +237,30 @@ export const generateTrackPayload: any = (parametersOverride: any) => { return removeUndefinedAndNullValues(payload); }; +export const generateRecordPayload: any = (parametersOverride: any) => { + const payload = { + type: 'record', + action: parametersOverride.action || 'insert', + fields: parametersOverride.fields || {}, + channel: 'sources', + context: { + sources: { + job_id: 'randomJobId', + version: 'local', + job_run_id: 'jobRunId', + task_run_id: 'taskRunId', + }, + }, + recordId: '3', + rudderId: 'randomRudderId', + messageId: 'randomMessageId', + receivedAt: '2024-11-08T10:30:41.618+05:30', + request_ip: '[::1]', + identifiers: parametersOverride.identifiers || {}, + }; + return removeUndefinedAndNullValues(payload); +}; + export const generateSimplifiedTrackPayload: any = (parametersOverride: any) => { return removeUndefinedAndNullValues({ type: 'track', From 810fb0c84c14fd7e57cdefdd8345d9034bb4e928 Mon Sep 17 00:00:00 2001 From: ItsSudip Date: Wed, 13 Nov 2024 13:16:52 +0530 Subject: [PATCH 4/5] chore: fix lint errors --- src/cdk/v2/destinations/linkedin_audience/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdk/v2/destinations/linkedin_audience/utils.ts b/src/cdk/v2/destinations/linkedin_audience/utils.ts index acbb604d564..12f5a0572b8 100644 --- a/src/cdk/v2/destinations/linkedin_audience/utils.ts +++ b/src/cdk/v2/destinations/linkedin_audience/utils.ts @@ -1,4 +1,4 @@ -const lodash = require('lodash'); +import lodash from 'lodash'; import { hashToSha256 } from '@rudderstack/integrations-lib'; import { createHash } from 'crypto'; import { BASE_ENDPOINT, COMPANY_ENDPOINT, FIELD_MAP, USER_ENDPOINT } from './config'; From b40512997f90247ee625355f4957697727aa470f Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 18 Nov 2024 11:28:25 +0530 Subject: [PATCH 5/5] refactor: linkedin audiences proc workflow --- .../linkedin_audience/procWorkflow.yaml | 58 ++++++++----------- .../linkedin_audience/processor/business.ts | 2 +- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml b/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml index e292dba8e90..f3f4ce07726 100644 --- a/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml +++ b/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml @@ -17,7 +17,7 @@ steps: $.assertConfig(secret.accessToken, "Access Token is not present. Aborting"); $.assertConfig(config.audienceType, "audienceType is not present. Aborting"); $.assert(messageType, "Message Type is not present. Aborting message."); - $.assert(messageType.toLowerCase() === $.SUPPORTED_EVENT_TYPE, "Event type " + .message.type.toLowerCase() + " is not supported. Aborting message."); + $.assert(messageType.toLowerCase() === $.SUPPORTED_EVENT_TYPE, `Event type ${.message.type.toLowerCase()} is not supported. Aborting message.`); $.assert(.message.fields, "`fields` is not present. Aborting message."); $.assert(.message.identifiers, "`identifiers` is not present inside properties. Aborting message."); $.assert($.containsAll([.message.action], $.ACTION_TYPES), "Unsupported action type. Aborting message.") @@ -26,45 +26,37 @@ steps: description: This step fetches the configs from different places and combines them. template: | const config = .connection.config.destination; - const secret = .metadata.secret; - const audienceType = config.audienceType; - const audienceId = config.audienceId; - const accessToken = secret.accessToken; { - audienceType, - audienceId, - accessToken, + audienceType: config.audienceType, + audienceId: config.audienceId, + accessToken: .metadata.secret.accessToken, isHashRequired: config.isHashRequired, } - - name: hashIdentifiers - description: Hash identifiers - condition: $.outputs.getConfigs.isHashRequired === true && $.outputs.getConfigs.audienceType === 'user' - template: | - const hashedIdentifiers = $.hashIdentifiers(.message.identifiers); - hashedIdentifiers; - - - name: prepareUserIds - description: Prepare user ids for user audience type - condition: $.outputs.getConfigs.audienceType === 'user' - template: | - const userIds = $.outputs.getConfigs.isHashRequired ? $.prepareUserIds($.outputs.hashIdentifiers) : $.prepareUserIds(.message.identifiers); - userIds; - - name: prepareUserTypeBasePayload - description: Prepare base payload for user audiences condition: $.outputs.getConfigs.audienceType === 'user' - template: | - const payload = { - 'elements': [ - { - 'action': $.generateActionType(.message.action), - 'userIds': $.outputs.prepareUserIds, - ....message.fields + steps: + - name: prepareUserIds + description: Prepare user ids for user audience type + template: | + const identifiers = $.outputs.getConfigs.isHashRequired === true ? + $.hashIdentifiers(.message.identifiers) : + .message.identifiers; + $.prepareUserIds(identifiers) + + - name: preparePayload + description: Prepare base payload for user audiences + template: | + const payload = { + 'elements': [ + { + 'action': $.generateActionType(.message.action), + 'userIds': $.outputs.prepareUserTypeBasePayload.prepareUserIds, + ....message.fields + } + ] } - ] - } - payload; + payload; - name: prepareCompanyTypeBasePayload description: Prepare base payload for company audiences diff --git a/test/integrations/destinations/linkedin_audience/processor/business.ts b/test/integrations/destinations/linkedin_audience/processor/business.ts index fe12857a629..28cb6a9a97b 100644 --- a/test/integrations/destinations/linkedin_audience/processor/business.ts +++ b/test/integrations/destinations/linkedin_audience/processor/business.ts @@ -93,7 +93,7 @@ export const businessTestData: ProcessorTestData[] = [ body: [ { error: - 'The "string" argument must be of type string. Received type number (12345): Workflow: procWorkflow, Step: hashIdentifiers, ChildStep: undefined, OriginalError: The "string" argument must be of type string. Received type number (12345)', + 'The "string" argument must be of type string. Received type number (12345): Workflow: procWorkflow, Step: prepareUserTypeBasePayload, ChildStep: prepareUserIds, OriginalError: The "string" argument must be of type string. Received type number (12345)', metadata: generateMetadata(1), statTags: { destType: 'LINKEDIN_AUDIENCE',