From b823a538ca4d4f38faa4762ae986375e0eb8ae05 Mon Sep 17 00:00:00 2001 From: AASHISH MALIK Date: Thu, 17 Nov 2022 15:47:59 +0530 Subject: [PATCH] feat(destination): onboard campaign manager (#1580) * feat(destination): onboard campaign manager * feat(destination): onboard campaign manager * feat: added profileId in dest config campaign manager * feat: addressed comments --- __tests__/campaign_manager.test.js | 48 +++ __tests__/data/campaign_manager.json | 308 ++++++++++++++++++ .../data/campaign_manager_proxy_input.json | 100 ++++++ .../data/campaign_manager_proxy_output.json | 49 +++ .../data/campaign_manager_router_input.json | 296 +++++++++++++++++ .../data/campaign_manager_router_output.json | 154 +++++++++ constants/destinationCanonicalNames.js | 8 + features.json | 1 + v0/destinations/campaign_manager/config.js | 18 + .../data/CampaignManagerTrackConfig.json | 97 ++++++ .../campaign_manager/networkHandler.js | 111 +++++++ v0/destinations/campaign_manager/transform.js | 173 ++++++++++ 12 files changed, 1363 insertions(+) create mode 100644 __tests__/campaign_manager.test.js create mode 100644 __tests__/data/campaign_manager.json create mode 100644 __tests__/data/campaign_manager_proxy_input.json create mode 100644 __tests__/data/campaign_manager_proxy_output.json create mode 100644 __tests__/data/campaign_manager_router_input.json create mode 100644 __tests__/data/campaign_manager_router_output.json create mode 100644 v0/destinations/campaign_manager/config.js create mode 100644 v0/destinations/campaign_manager/data/CampaignManagerTrackConfig.json create mode 100644 v0/destinations/campaign_manager/networkHandler.js create mode 100644 v0/destinations/campaign_manager/transform.js diff --git a/__tests__/campaign_manager.test.js b/__tests__/campaign_manager.test.js new file mode 100644 index 0000000000..0a5bf76f6c --- /dev/null +++ b/__tests__/campaign_manager.test.js @@ -0,0 +1,48 @@ +const integration = "campaign_manager"; +const name = "campaign_manager"; + +const fs = require("fs"); +const path = require("path"); +const version = "v0"; +const transformer = require(`../${version}/destinations/${integration}/transform`); + +// Processor Test Data +const testDataFile = fs.readFileSync( + path.resolve(__dirname, `./data/${integration}.json`) +); +const testData = JSON.parse(testDataFile); + +// Router Test files +const inputRouterDataFile = fs.readFileSync( + path.resolve(__dirname, `./data/${integration}_router_input.json`) +); +const outputRouterDataFile = fs.readFileSync( + path.resolve(__dirname, `./data/${integration}_router_output.json`) +); +const inputRouterData = JSON.parse(inputRouterDataFile); +const expectedRouterData = JSON.parse(outputRouterDataFile); + +describe(`${name} Tests`, () => { + describe("Processor", () => { + testData.forEach((dataPoint, index) => { + it(`${index}. ${integration} - ${dataPoint.description}`, async () => { + try { + let output = await transformer.process(dataPoint.input); + delete output.body.JSON.idempotency; + expect(output).toEqual(dataPoint.output); + } catch (error) { + expect(error.message).toEqual(dataPoint.output.error); + } + }); + }); + }); + + describe("Router Tests", () => { + it("Payload", async () => { + const routerOutput = await transformer.processRouterDest(inputRouterData); + expect(routerOutput).toEqual(expectedRouterData); + }); + }); +}); + + diff --git a/__tests__/data/campaign_manager.json b/__tests__/data/campaign_manager.json new file mode 100644 index 0000000000..9e3059c322 --- /dev/null +++ b/__tests__/data/campaign_manager.json @@ -0,0 +1,308 @@ +[ + { + "description": "Track - batch insert Call", + "input": { + "message": { + "channel": "web", + "context": { + "app": { + "build": "1.0.0", + "name": "RudderLabs JavaScript SDK", + "namespace": "com.rudderlabs.javascript", + "version": "1.0.0" + }, + "device": { + "id": "0572f78fa49c648e", + "name": "generic_x86_arm", + "type": "Android", + "model": "AOSP on IA Emulator", + "manufacturer": "Google", + "adTrackingEnabled": true, + "advertisingId": "44c97318-9040-4361-8bc7-4eb30f665ca8" + }, + "traits": { + "email": "alex@example.com", + "phone": "+1-202-555-0146", + "firstName": "John", + "lastName": "Gomes", + "city": "London", + "state": "England", + "countryCode": "GB", + "postalCode": "EC3M", + "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": "Promotion Clicked", + "type": "track", + "originalTimestamp": "2022-11-17T00:22:02.903+05:30", + "properties": { + "profileId": "34245", + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": "756", + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "quantity": "455678", + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true, + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "requestType": "batchupdate" + }, + "type": "track", + "event": "event test", + "anonymousId": "randomId", + "integrations": { + "All": true + }, + "name": "ApplicationLoaded", + "sentAt": "2019-10-14T11:15:53.296Z" + + }, + "metadata": { + "secret": { + "access_token": "abcd1234", + "refresh_token": "efgh5678", + "developer_token": "ijkl91011" + } + }, + "destination": { + "Config": { + "profileId": "5343234", + "treatmentForUnderage": false, + "limitAdTracking": false, + "childDirectedTreatment": false, + "nonPersonalizedAd": false, + "rudderAccountId": "2EOknn1JNH7WK1MfNku4fGYKkRK" + } + } + }, + "output": + { + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": "https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/34245/conversions/batchupdate", + "headers": { + "Authorization": "Bearer abcd1234", + "Content-Type": "application/json" + }, + "params": {}, + "body": { + "JSON": { + "kind": "dfareporting#conversionsBatchUpdateRequest", + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "conversions": [ + { + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "timestampMicros": "1668624722000000", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "quantity": "455678", + "value": 756, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true, + "treatmentForUnderage": false, + "nonPersonalizedAd": false + } + ] + }, + "JSON_ARRAY": {}, + "XML": {}, + "FORM": {} + }, + "files": {} + } + }, + { + "description": "Track - batch update Call", + "input": { + "message": { + "channel": "web", + "context": { + "app": { + "build": "1.0.0", + "name": "RudderLabs JavaScript SDK", + "namespace": "com.rudderlabs.javascript", + "version": "1.0.0" + }, + "device": { + "id": "0572f78fa49c648e", + "name": "generic_x86_arm", + "type": "Android", + "model": "AOSP on IA Emulator", + "manufacturer": "Google", + "adTrackingEnabled": true, + "advertisingId": "44c97318-9040-4361-8bc7-4eb30f665ca8" + }, + "traits": { + "email": "alex@example.com", + "phone": "+1-202-555-0146", + "firstName": "John", + "lastName": "Gomes", + "city": "London", + "state": "England", + "countryCode": "GB", + "postalCode": "EC3M", + "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": "Promotion Clicked", + "type": "track", + "originalTimestamp": "2022-11-17T00:22:02.903+05:30", + "properties": { + "profileId": "34245", + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": "756", + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "quantity": "455678", + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true, + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "requestType": "batchupdate" + }, + "type": "track", + "event": "event test", + "anonymousId": "randomId", + "integrations": { + "All": true + }, + "name": "ApplicationLoaded", + "sentAt": "2019-10-14T11:15:53.296Z" + + }, + "metadata": { + "secret": { + "access_token": "abcd1234", + "refresh_token": "efgh5678", + "developer_token": "ijkl91011" + } + }, + "destination": { + "Config": { + "profileId": "5343234", + "treatmentForUnderage": false, + "limitAdTracking": false, + "childDirectedTreatment": false, + "nonPersonalizedAd": false, + "rudderAccountId": "2EOknn1JNH7WK1MfNku4fGYKkRK" + } + } + }, + "output": + { + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": "https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/34245/conversions/batchupdate", + "headers": { + "Authorization": "Bearer abcd1234", + "Content-Type": "application/json" + }, + "params": {}, + "body": { + "JSON": { + "kind": "dfareporting#conversionsBatchUpdateRequest", + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "conversions": [ + { + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "timestampMicros": "1668624722000000", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "quantity": "455678", + "value": 756, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true, + "treatmentForUnderage": false, + "nonPersonalizedAd": false + } + ] + }, + "JSON_ARRAY": {}, + "XML": {}, + "FORM": {} + }, + "files": {} + } + } +] diff --git a/__tests__/data/campaign_manager_proxy_input.json b/__tests__/data/campaign_manager_proxy_input.json new file mode 100644 index 0000000000..35501d6ae8 --- /dev/null +++ b/__tests__/data/campaign_manager_proxy_input.json @@ -0,0 +1,100 @@ +[ + { + "request": { + "body": { + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": "https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437689/conversions/batchupdate", + "headers": { + "Authorization": "Bearer abcd1234", + "Content-Type": "application/json" + }, + "params": {}, + "body": { + "JSON": { + "kind": "dfareporting#conversionsBatchInsertRequest", + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "conversions": [ + { + "timestampMicros": "1668624722000000", + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": 7, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true + } + ] + }, + "JSON_ARRAY": {}, + "XML": {}, + "FORM": {} + }, + "files": {} + } + } + }, + { + "request": { + "body": { + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": "https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437689/conversions/batchupdate", + "headers": { + "Authorization": "Bearer fgvbjghv", + "Content-Type": "application/json" + }, + "params": {}, + "body": { + "JSON": { + "kind": "dfareporting#conversionsBatchUpdateRequest", + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "conversions": [ + { + "timestampMicros": "1668624722000000", + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": 7, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true + } + ] + }, + "JSON_ARRAY": {}, + "XML": {}, + "FORM": {} + }, + "files": {} + } + } + } +] diff --git a/__tests__/data/campaign_manager_proxy_output.json b/__tests__/data/campaign_manager_proxy_output.json new file mode 100644 index 0000000000..1880463f65 --- /dev/null +++ b/__tests__/data/campaign_manager_proxy_output.json @@ -0,0 +1,49 @@ +[ + { + "output": { + "destinationResponse": { + "error": { + "code": 403, + "message": "The caller does not have permission", + "errors": [{ + "message": "Invalid Credentials", + "domain": "global", + "reason": "authError", + "location": "Authorization", + "locationType": "header" + }], + "status": "PERMISSION_DENIED" + } + }, + "message": "Campaign Manager: Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project. during CAMPAIGN_MANAGER response transformation", + "statTags": { + "destType": "CAMPAIGN_MANAGER", + "errorAt": "proxy", + "scope": "exception", + "stage": "responseTransform" + }, + "status": 403 + } + }, + { + "output": { + "status": 200, + "message": "[CAMPAIGN_MANAGER Response Handler] - Request Processed Successfully", + "destinationResponse": { + "response": [ + { + "adjustmentType": "ENHANCEMENT", + "conversionAction": "customers/7693729833/conversionActions/874224905", + "adjustmentDateTime": "2021-01-01 12:32:45-08:00", + "gclidDateTimePair": { + "gclid": "1234", + "conversionDateTime": "2021-01-01 12:32:45-08:00" + }, + "orderId": "12345" + } + ], + "status": 200 + } + } + } +] diff --git a/__tests__/data/campaign_manager_router_input.json b/__tests__/data/campaign_manager_router_input.json new file mode 100644 index 0000000000..742aaafeab --- /dev/null +++ b/__tests__/data/campaign_manager_router_input.json @@ -0,0 +1,296 @@ +[ + { + "metadata": { + "secret": { + "access_token": "abcd1234", + "refresh_token": "efgh5678", + "developer_token": "ijkl91011" + } + }, + "destination": { + "Config": { + "treatmentForUnderage": false, + "limitAdTracking": false, + "childDirectedTreatment": false, + "nonPersonalizedAd": false, + "rudderAccountId": "2EOknn1JNH7WK1MfNku4fGYKkRK" + } + }, + "message": { + "channel": "web", + "context": { + "app": { + "build": "1.0.0", + "name": "RudderLabs JavaScript SDK", + "namespace": "com.rudderlabs.javascript", + "version": "1.0.0" + }, + "device": { + "id": "0572f78fa49c648e", + "name": "generic_x86_arm", + "type": "Android", + "model": "AOSP on IA Emulator", + "manufacturer": "Google", + "adTrackingEnabled": true, + "advertisingId": "44c97318-9040-4361-8bc7-4eb30f665ca8" + }, + "traits": { + "email": "alex@example.com", + "phone": "+1-202-555-0146", + "firstName": "John", + "lastName": "Gomes", + "city": "London", + "state": "England", + "countryCode": "GB", + "postalCode": "EC3M", + "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": "Promotion Clicked", + "type": "track", + "originalTimestamp": "2022-11-17T00:22:02.903+05:30", + "properties": { + "profileId": 437689, + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": 7, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true, + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "requestType": "batchinsert" + }, + "type": "track", + "event": "event test", + "anonymousId": "randomId", + "integrations": { + "All": true + }, + "name": "ApplicationLoaded", + "sentAt": "2022-11-17T00:22:02.903+05:30" + } + }, + { + "metadata": { + "secret": { + "access_token": "abcd1234", + "refresh_token": "efgh5678", + "developer_token": "ijkl91011" + } + }, + "destination": { + "Config": { + "treatmentForUnderage": false, + "limitAdTracking": false, + "childDirectedTreatment": false, + "nonPersonalizedAd": false, + "rudderAccountId": "2EOknn1JNH7WK1MfNku4fGYKkRK" + } + }, + "message": { + "channel": "web", + "context": { + "app": { + "build": "1.0.0", + "name": "RudderLabs JavaScript SDK", + "namespace": "com.rudderlabs.javascript", + "version": "1.0.0" + }, + "device": { + "id": "0572f78fa49c648e", + "name": "generic_x86_arm", + "type": "Android", + "model": "AOSP on IA Emulator", + "manufacturer": "Google", + "adTrackingEnabled": true, + "advertisingId": "44c97318-9040-4361-8bc7-4eb30f665ca8" + }, + "traits": { + "email": "alex@example.com", + "phone": "+1-202-555-0146", + "firstName": "John", + "lastName": "Gomes", + "city": "London", + "state": "England", + "countryCode": "GB", + "postalCode": "EC3M", + "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": "Promotion Clicked", + "type": "track", + "originalTimestamp": "2022-11-17T00:22:02.903+05:30", + "properties": { + "profileId": 437689, + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": 7, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true, + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "requestType": "batchupdate" + }, + "type": "track", + "event": "event test", + "anonymousId": "randomId", + "integrations": { + "All": true + }, + "name": "ApplicationLoaded", + "sentAt": "2022-11-17T00:22:02.903+05:30" + } + }, + { + "metadata": { + "secret": { + "access_token": "abcd1234", + "refresh_token": "efgh5678", + "developer_token": "ijkl91011" + } + }, + "destination": { + "Config": { + "treatmentForUnderage": false, + "limitAdTracking": false, + "childDirectedTreatment": false, + "nonPersonalizedAd": false, + "rudderAccountId": "2EOknn1JNH7WK1MfNku4fGYKkRK" + } + }, + "message": { + "channel": "web", + "context": { + "app": { + "build": "1.0.0", + "name": "RudderLabs JavaScript SDK", + "namespace": "com.rudderlabs.javascript", + "version": "1.0.0" + }, + "device": { + "id": "0572f78fa49c648e", + "name": "generic_x86_arm", + "type": "Android", + "model": "AOSP on IA Emulator", + "manufacturer": "Google", + "adTrackingEnabled": true, + "advertisingId": "44c97318-9040-4361-8bc7-4eb30f665ca8" + }, + "traits": { + "email": "alex@example.com", + "phone": "+1-202-555-0146", + "firstName": "John", + "lastName": "Gomes", + "city": "London", + "state": "England", + "countryCode": "GB", + "postalCode": "EC3M", + "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": "Promotion Clicked", + "type": "track", + "originalTimestamp": "2022-11-17T00:22:02.903+05:30", + "properties": { + "profileId": 437689, + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": 7, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true, + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "requestType": "randomValue" + }, + "type": "track", + "event": "event test", + "anonymousId": "randomId", + "integrations": { + "All": true + }, + "name": "ApplicationLoaded", + "sentAt": "2022-11-17T00:22:02.903+05:30" + } + } +] \ No newline at end of file diff --git a/__tests__/data/campaign_manager_router_output.json b/__tests__/data/campaign_manager_router_output.json new file mode 100644 index 0000000000..c608cea465 --- /dev/null +++ b/__tests__/data/campaign_manager_router_output.json @@ -0,0 +1,154 @@ +[ + { + "batchedRequest": { + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": "https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437689/conversions/batchinsert", + "headers": { + "Authorization": "Bearer abcd1234", + "Content-Type": "application/json" + }, + "params": {}, + "body": { + "JSON": { + "kind": "dfareporting#conversionsBatchInsertRequest", + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "conversions": [ + { + "nonPersonalizedAd": false, + "treatmentForUnderage": false, + "timestampMicros": "1668624722000000", + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": 7, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true + } + ] + }, + "JSON_ARRAY": {}, + "XML": {}, + "FORM": {} + }, + "files": {} + }, + "metadata": [ + { + "secret": { + "access_token": "abcd1234", + "developer_token": "ijkl91011", + "refresh_token": "efgh5678" + } + } + ], + "batched": false, + "statusCode": 200, + "destination": { + "Config": { + "childDirectedTreatment": false, + "limitAdTracking": false, + "nonPersonalizedAd": false, + "rudderAccountId": "2EOknn1JNH7WK1MfNku4fGYKkRK", + "treatmentForUnderage": false + } + } + }, + { + "batchedRequest": { + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": "https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437689/conversions/batchupdate", + "headers": { + "Authorization": "Bearer abcd1234", + "Content-Type": "application/json" + }, + "params": {}, + "body": { + "JSON": { + "kind": "dfareporting#conversionsBatchUpdateRequest", + "encryptionInfo": { + "kind": "dfareporting#encryptionInfo", + "encryptionSource": "AD_SERVING", + "encryptionEntityId": "3564523", + "encryptionEntityType": "DCM_ACCOUNT" + }, + "conversions": [ + { + "nonPersonalizedAd": false, + "treatmentForUnderage": false, + "timestampMicros": "1668624722000000", + "floodlightConfigurationId": "213123123", + "ordinal": "string", + "floodlightActivityId": "456543345245", + "mobileDeviceId": "string", + "value": 7, + "encryptedUserIdCandidates": [ + "dfghjbnm" + ], + "gclid": "string", + "matchId": "string", + "dclid": "string", + "impressionId": "string", + "limitAdTracking": true, + "childDirectedTreatment": true + } + ] + }, + "JSON_ARRAY": {}, + "XML": {}, + "FORM": {} + }, + "files": {} + }, + "metadata": [ + { + "secret": { + "access_token": "abcd1234", + "developer_token": "ijkl91011", + "refresh_token": "efgh5678" + } + } + ], + "batched": false, + "statusCode": 200, + "destination": { + "Config": { + "childDirectedTreatment": false, + "limitAdTracking": false, + "nonPersonalizedAd": false, + "rudderAccountId": "2EOknn1JNH7WK1MfNku4fGYKkRK", + "treatmentForUnderage": false + } + } + }, + { + "batched": false, + "error": "[CAMPAIGN MANAGER (DCM)]: properties.requestType must be one of batchinsert or batchupdate.", + "metadata": [ + { + "secret": { + "access_token": "abcd1234", + "developer_token": "ijkl91011", + "refresh_token": "efgh5678" + } + } + ], + "statusCode": 400 + } +] diff --git a/constants/destinationCanonicalNames.js b/constants/destinationCanonicalNames.js index 2619ddd11f..6d9ec7cada 100644 --- a/constants/destinationCanonicalNames.js +++ b/constants/destinationCanonicalNames.js @@ -86,6 +86,14 @@ const DestCanonicalNames = { "SNAPCHAT_CUSTOM_AUDIENCE", "SNAPCHATCUSTOMAUDIENCE" ], + CAMPAIGN_MANAGER: [ + "campaign manager", + "campain Manager", + "CAMPAIGN MANAGER", + "campaignManager", + "campaign_manager", + "CAMPAIGN_MANAGER" + ], gainsight_px: [ "GAINSIGHT_PX", "GAINSIGHTPX", diff --git a/features.json b/features.json index be3838bdf9..818b3a2dcf 100644 --- a/features.json +++ b/features.json @@ -44,6 +44,7 @@ "FACEBOOK_OFFLINE_CONVERSIONS": true, "MAILJET": true, "SNAPCHAT_CUSTOM_AUDIENCE": true, + "CAMPAIGN_MANAGER": true, "SENDGRID": true } } diff --git a/v0/destinations/campaign_manager/config.js b/v0/destinations/campaign_manager/config.js new file mode 100644 index 0000000000..9a783f4042 --- /dev/null +++ b/v0/destinations/campaign_manager/config.js @@ -0,0 +1,18 @@ +const { getMappingConfig } = require("../../util"); + +const BASE_URL = + "https://dfareporting.googleapis.com/dfareporting/v4/userprofiles"; + +const ConfigCategories = { + TRACK: { + type: "track", + name: "CampaignManagerTrackConfig" + } +}; + +const mappingConfig = getMappingConfig(ConfigCategories, __dirname); +module.exports = { + mappingConfig, + ConfigCategories, + BASE_URL +}; diff --git a/v0/destinations/campaign_manager/data/CampaignManagerTrackConfig.json b/v0/destinations/campaign_manager/data/CampaignManagerTrackConfig.json new file mode 100644 index 0000000000..28269ef82e --- /dev/null +++ b/v0/destinations/campaign_manager/data/CampaignManagerTrackConfig.json @@ -0,0 +1,97 @@ +[ + { + "destKey": "floodlightConfigurationId", + "sourceKeys": "properties.floodlightConfigurationId", + "required": true + }, + { + "destKey": "ordinal", + "sourceKeys": "properties.ordinal", + "required": true + }, + { + "destKey": "timestampMicros", + "sourceKeys": "originalTimestamp", + "required": true, + "metadata": { + "type": "microSecondTimestamp" + } + }, + { + "destKey": "floodlightActivityId", + "sourceKeys": "properties.floodlightActivityId", + "required": true + }, + { + "destKey": "customVariables", + "sourceKeys": "properties.customVariables", + "required": false + }, + { + "destKey": "mobileDeviceId", + "sourceKeys": "properties.mobileDeviceId", + "required": false + }, + { + "destKey": "quantity", + "sourceKeys": "properties.quantity", + "required": false + }, + { + "destKey": "value", + "sourceKeys": [ + "properties.value", + "properties.total", + "properties.revenue" + ], + "metadata": { + "type": "toNumber" + }, + "required": false + }, + { + "destKey": "encryptedUserIdCandidates", + "sourceKeys": "properties.encryptedUserIdCandidates", + "required": false + }, + { + "destKey": "gclid", + "sourceKeys": "properties.gclid", + "required": false + }, + { + "destKey": "matchId", + "sourceKeys": "properties.matchId", + "required": false + }, + { + "destKey": "dclid", + "sourceKeys": "properties.dclid", + "required": false + }, + { + "destKey": "impressionId", + "sourceKeys": "properties.impressionId", + "required": false + }, + { + "destKey": "limitAdTracking", + "sourceKeys": "properties.limitAdTracking", + "required": false + }, + { + "destKey": "treatmentForUnderage", + "sourceKeys": "properties.treatmentForUnderage", + "required": false + }, + { + "destKey": "childDirectedTreatment", + "sourceKeys": "properties.childDirectedTreatment", + "required": false + }, + { + "destKey": "nonPersonalizedAd", + "sourceKeys": "properties.nonPersonalizedAd", + "required": false + } +] \ No newline at end of file diff --git a/v0/destinations/campaign_manager/networkHandler.js b/v0/destinations/campaign_manager/networkHandler.js new file mode 100644 index 0000000000..36b094a817 --- /dev/null +++ b/v0/destinations/campaign_manager/networkHandler.js @@ -0,0 +1,111 @@ +const { + prepareProxyRequest, + proxyRequest +} = require("../../../adapters/network"); +const { isHttpStatusSuccess } = require("../../util/index"); +const ErrorBuilder = require("../../util/error"); +const { + DISABLE_DEST, + REFRESH_TOKEN +} = require("../../../adapters/networkhandler/authConstants"); + +const { + processAxiosResponse +} = require("../../../adapters/utils/networkUtils"); +const { ApiError } = require("../../util/errors"); +const { TRANSFORMER_METRIC } = require("../../util/constant"); + +/** + * This function helps to detarmine type of error occured. According to the response + * we set authErrorCategory to take decision if we need to refresh the access_token + * or need to disable the destination. + * @param {*} code + * @returns + */ +const getAuthErrCategory = code => { + switch (code) { + case 401: + return REFRESH_TOKEN; + case 403: // Access Denied + return DISABLE_DEST; + default: + return ""; + } +}; + +function checkIfFailuresAreRetryable(response) { + try { + if ( + Array.isArray(response.status) && + Array.isArray(response.status[0].errors) + ) { + return ( + response.status[0].errors[0].code !== "PERMISSION_DENIED" && + response.status[0].errors[0].code !== "INVALID_ARGUMENT" + ); + } + return true; + } catch (e) { + return true; + } +} + +const responseHandler = destinationResponse => { + const message = `[CAMPAIGN_MANAGER Response Handler] - Request Processed Successfully`; + const { response, status } = destinationResponse; + if (isHttpStatusSuccess(status)) { + // check for Failures + if (response.hasFailures === true) { + if (checkIfFailuresAreRetryable(response)) { + throw new ApiError( + `Campaign Manager: Retrying during CAMPAIGN_MANAGER response transformation`, + 500, + { + scope: TRANSFORMER_METRIC.MEASUREMENT_TYPE.API.SCOPE, + meta: TRANSFORMER_METRIC.MEASUREMENT_TYPE.API.META.RETRYABLE + }, + destinationResponse, + undefined, + "CAMPAIGN_MANAGER" + ); + } else { + // abort message + throw new ApiError( + `Campaign Manager: Aborting during CAMPAIGN_MANAGER response transformation`, + 400, + { + scope: TRANSFORMER_METRIC.MEASUREMENT_TYPE.API.SCOPE, + meta: TRANSFORMER_METRIC.MEASUREMENT_TYPE.API.META.ABORTABLE + }, + destinationResponse, + undefined, + "CAMPAIGN_MANAGER" + ); + } + } + + return { + status, + message, + destinationResponse + }; + } + + throw new ErrorBuilder() + .setStatus(status) + .setDestinationResponse(response) + .setMessage( + `Campaign Manager: ${response.error.message} during CAMPAIGN_MANAGER response transformation 3` + ) + .setAuthErrorCategory(getAuthErrCategory(status)) + .build(); +}; + +const networkHandler = function() { + this.prepareProxy = prepareProxyRequest; + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.responseHandler = responseHandler; +}; + +module.exports = { networkHandler }; diff --git a/v0/destinations/campaign_manager/transform.js b/v0/destinations/campaign_manager/transform.js new file mode 100644 index 0000000000..c20ea2755f --- /dev/null +++ b/v0/destinations/campaign_manager/transform.js @@ -0,0 +1,173 @@ +const { EventType } = require("../../../constants"); + +const { + constructPayload, + defaultRequestConfig, + getSuccessRespEvents, + getErrorRespEvents, + CustomError, + defaultPostRequestConfig, + removeUndefinedAndNullValues +} = require("../../util"); + +const { ConfigCategories, mappingConfig, BASE_URL } = require("./config"); +const ErrorBuilder = require("../../util/error"); + +const getAccessToken = ({ secret }) => { + if (!secret) { + throw new ErrorBuilder() + .setMessage("[CAMPAIGN MANAGER (DCM)]:: OAuth - access token not found") + .setStatus(500) + .build(); + } + return secret.access_token; +}; + +// build final response +function buildResponse( + requestJson, + metadata, + endpointUrl, + requestType, + encryptionInfo +) { + const response = defaultRequestConfig(); + response.endpoint = endpointUrl; + response.headers = { + Authorization: `Bearer ${getAccessToken(metadata)}`, + "Content-Type": "application/json" + }; + response.method = defaultPostRequestConfig.requestMethod; + response.body.JSON.kind = + requestType === "batchinsert" + ? "dfareporting#conversionsBatchInsertRequest" + : "dfareporting#conversionsBatchUpdateRequest"; + response.body.JSON.encryptionInfo = encryptionInfo || {}; + response.body.JSON.conversions = [removeUndefinedAndNullValues(requestJson)]; + return response; +} + +function prepareUrl(message, destination) { + const profileId = message.properties.profileId + ? Number(message.properties.profileId) + : Number(destination.Config.profileId); + return `${BASE_URL}/${profileId}/conversions/${message.properties.requestType}`; +} + +// process track call +function processTrack(message, metadata, destination) { + const requestJson = constructPayload( + message, + mappingConfig[ConfigCategories.TRACK.name] + ); + requestJson.limitAdTracking = + requestJson.limitAdTracking || destination.Config.limitAdTracking; + requestJson.treatmentForUnderage = + requestJson.treatmentForUnderage || destination.Config.treatmentForUnderage; + requestJson.childDirectedTreatment = + requestJson.childDirectedTreatment || + destination.Config.childDirectedTreatment; + requestJson.nonPersonalizedAd = + requestJson.nonPersonalizedAd || destination.Config.nonPersonalizedAd; + requestJson.timestampMicros = requestJson.timestampMicros.toString(); + const endpointUrl = prepareUrl(message, destination); + return buildResponse( + requestJson, + metadata, + endpointUrl, + message.properties.requestType, + message.properties.encryptionInfo + ); +} + +function validateRequest(message) { + if (!message.properties) { + throw new CustomError( + "[CAMPAIGN MANAGER (DCM)]: properties must be present in event. Aborting message", + 400 + ); + } + + if ( + message.properties.requestType !== "batchinsert" && + message.properties.requestType !== "batchupdate" + ) { + throw new CustomError( + "[CAMPAIGN MANAGER (DCM)]: properties.requestType must be one of batchinsert or batchupdate.", + 400 + ); + } + + if ( + message.properties.encryptedUserId && + !message.properties.encryptionInfo + ) { + throw new CustomError( + "[CAMPAIGN MANAGER (DCM)]: encryptionInfo is a required field if encryptedUserId is used.", + 400 + ); + } +} + +function process(event) { + const { message, metadata, destination } = event; + + if (!message.type) { + throw new CustomError( + "[CAMPAIGN MANAGER (DCM)]: Message Type missing. Aborting message.", + 400 + ); + } + + validateRequest(message); + + const messageType = message.type.toLowerCase(); + + switch (messageType) { + case EventType.TRACK: + return processTrack(message, metadata, destination); + default: + throw new CustomError(`Message type ${messageType} not supported`, 400); + } +} + +const processRouterDest = async inputs => { + if (!Array.isArray(inputs) || inputs.length <= 0) { + const respEvents = getErrorRespEvents(null, 400, "Invalid event array"); + return [respEvents]; + } + + const respList = await Promise.all( + inputs.map(async input => { + try { + if (input.message.statusCode) { + // already transformed event + return getSuccessRespEvents( + input.message, + [input.metadata], + input.destination + ); + } + // if not transformed + return getSuccessRespEvents( + await process(input), + [input.metadata], + input.destination + ); + } catch (error) { + return getErrorRespEvents( + [input.metadata], + error.response + ? error.response.status + : error.code + ? error.code + : 400, + error.message || "Error occurred while processing payload." + ); + } + }) + ); + return respList; +}; + +module.exports = { process, processRouterDest };