From f79dfe74b447b8ccd80287e143ff771688c510be Mon Sep 17 00:00:00 2001 From: Sankeerth Date: Wed, 23 Oct 2024 12:39:17 +0530 Subject: [PATCH 1/2] feat: support for multiple zap urls (#3825) * feat: support for multiple zap urls * fix: update endpoint to array * fix: use forEach instead of map, remove unused dependency --------- Co-authored-by: Sai Sankeerth --- .../v2/destinations/zapier/procWorkflow.yaml | 20 +- src/cdk/v2/destinations/zapier/utils.js | 15 + .../destinations/zapier/processor/data.ts | 266 ++++++++++++++++++ 3 files changed, 288 insertions(+), 13 deletions(-) create mode 100644 src/cdk/v2/destinations/zapier/utils.js diff --git a/src/cdk/v2/destinations/zapier/procWorkflow.yaml b/src/cdk/v2/destinations/zapier/procWorkflow.yaml index c14c57378c4..d7a5429129a 100644 --- a/src/cdk/v2/destinations/zapier/procWorkflow.yaml +++ b/src/cdk/v2/destinations/zapier/procWorkflow.yaml @@ -1,10 +1,9 @@ bindings: - name: EventType path: ../../../../constants - - name: getHashFromArray + - name: getHashFromArrayWithDuplicate path: ../../../../v0/util/index - - name: defaultRequestConfig - path: ../../../../v0/util + - path: ./utils steps: - name: validateInput @@ -15,18 +14,18 @@ steps: - name: prepareContext template: | $.context.messageType = .message.type.toLowerCase(); - $.context.endpoint = .destination.Config.zapUrl; + $.context.endpoint = [.destination.Config.zapUrl]; - name: trackEndpoint condition: $.context.messageType === {{$.EventType.TRACK}} template: | - const trackEventsMap = $.getHashFromArray(.destination.Config.trackEventsToZap); + const trackEventsMap = $.getHashFromArrayWithDuplicate(.destination.Config.trackEventsToZap); const eventName = .message.event.toLowerCase(); (eventName && trackEventsMap[eventName]) ? ($.context.endpoint = trackEventsMap[eventName]) else: name: endpointForOthers template: | - const pageScreenEventsMap = $.getHashFromArray(.destination.Config.pageScreenEventsToZap); + const pageScreenEventsMap = $.getHashFromArrayWithDuplicate(.destination.Config.pageScreenEventsToZap); const pageName = .message.name.toLowerCase(); (pageName && pageScreenEventsMap[pageName]) ? ($.context.endpoint = pageScreenEventsMap[pageName]) @@ -37,10 +36,5 @@ steps: else: name: buildResponseForProcessTransformation template: | - const response = $.defaultRequestConfig(); - response.body.JSON = .message; - response.endpoint = $.context.endpoint; - response.headers = { - "content-type": "application/json" - }; - response + const responseList = $.buildResponseList(.message, $.context.endpoint) + responseList diff --git a/src/cdk/v2/destinations/zapier/utils.js b/src/cdk/v2/destinations/zapier/utils.js new file mode 100644 index 00000000000..43de6e58dd1 --- /dev/null +++ b/src/cdk/v2/destinations/zapier/utils.js @@ -0,0 +1,15 @@ +const { defaultRequestConfig } = require('../../../../v0/util'); + +const buildResponseList = (payload, endpointList) => { + const responseList = []; + endpointList.forEach((endpoint) => { + const response = defaultRequestConfig(); + response.body.JSON = payload; + response.endpoint = endpoint; + response.headers = { 'content-type': 'application/json' }; + responseList.push(response); + }); + return responseList; +}; + +module.exports = { buildResponseList }; diff --git a/test/integrations/destinations/zapier/processor/data.ts b/test/integrations/destinations/zapier/processor/data.ts index b938f68dc56..b4f5a2a8c57 100644 --- a/test/integrations/destinations/zapier/processor/data.ts +++ b/test/integrations/destinations/zapier/processor/data.ts @@ -643,6 +643,62 @@ export const data = [ response: { status: 200, body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'def.zap-hook', + headers: { + 'content-type': 'application/json', + }, + params: {}, + body: { + JSON: { + event: 'def', + userId: 'identified user id', + type: 'track', + anonymousId: 'anon-id-new', + context: { + traits: { + trait1: 'new-val', + }, + ip: '14.5.67.21', + library: { + name: 'http', + }, + }, + properties: { + abc: '123', + key: { + abc: 123, + }, + array: [ + { + abc: 123, + }, + { + def: 123, + }, + ], + }, + timestamp: '2020-02-02T00:23:09.544Z', + originalTimestamp: '2020-04-17T14:42:44.724Z', + sentAt: '2020-04-17T14:42:44.724Z', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, { output: { version: '1', @@ -703,4 +759,214 @@ export const data = [ }, }, }, + { + id: 'Test 5', + name: 'zapier', + description: 'Track call with Multiplexing', + scenario: 'Framework+Business', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + event: 'def', + userId: 'identified user id', + type: 'track', + anonymousId: 'anon-id-new', + timestamp: '2020-02-02T00:23:09.544Z', + originalTimestamp: '2020-04-17T14:42:44.724Z', + sentAt: '2020-04-17T14:42:44.724Z', + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + zapUrl: 'abcd.zap-hook', + trackEventsToZap: [ + { + from: 'def', + to: ['def.zap-hook', 'ghi.zap-hook'], + }, + ], + pageScreenEventsToZap: [{}], + }, + Enabled: true, + Transformations: [], + IsProcessorEnabled: true, + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: ['def.zap-hook', 'ghi.zap-hook'], + headers: { + 'content-type': 'application/json', + }, + params: {}, + body: { + JSON: { + event: 'def', + userId: 'identified user id', + type: 'track', + anonymousId: 'anon-id-new', + timestamp: '2020-02-02T00:23:09.544Z', + originalTimestamp: '2020-04-17T14:42:44.724Z', + sentAt: '2020-04-17T14:42:44.724Z', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + }, + { + id: 'Test 6', + name: 'zapier', + description: 'Page call with Multiplexing', + scenario: 'Framework+Business', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + name: 'abc_page_test', + userId: 'identified user id', + type: 'page', + anonymousId: 'anon-id-new', + properties: { + abc: '123', + key: { + abc: 123, + }, + array: [ + { + abc: 123, + }, + { + def: 123, + }, + ], + }, + timestamp: '2020-02-02T00:23:09.544Z', + originalTimestamp: '2020-04-17T14:42:44.724Z', + sentAt: '2020-04-17T14:42:44.724Z', + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + zapUrl: 'abcd.zap-hook', + trackEventsToZap: [{}], + pageScreenEventsToZap: [ + { + from: 'abc_page_test', + to: ['page.zap-hook', 'ghi.zap-hook'], + }, + ], + }, + Enabled: true, + Transformations: [], + IsProcessorEnabled: true, + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: ['page.zap-hook', 'ghi.zap-hook'], + headers: { + 'content-type': 'application/json', + }, + params: {}, + body: { + JSON: { + name: 'abc_page_test', + userId: 'identified user id', + type: 'page', + anonymousId: 'anon-id-new', + properties: { + abc: '123', + key: { + abc: 123, + }, + array: [ + { + abc: 123, + }, + { + def: 123, + }, + ], + }, + timestamp: '2020-02-02T00:23:09.544Z', + originalTimestamp: '2020-04-17T14:42:44.724Z', + sentAt: '2020-04-17T14:42:44.724Z', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + }, ]; From 8f18e1aca70ab68e3f157a4632d63ae7cec0e87b Mon Sep 17 00:00:00 2001 From: Manish Kumar <144022547+manish339k@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:24:11 +0530 Subject: [PATCH 2/2] fix: str replace is not a function error (#3799) * fix: str.replace is not a function * fix: resolving comments --- .../transform.js | 22 +- src/v0/util/index.js | 6 +- src/v0/util/index.test.js | 16 + .../router/data.ts | 307 ++++++++++++++++++ 4 files changed, 345 insertions(+), 6 deletions(-) diff --git a/src/v0/destinations/google_adwords_enhanced_conversions/transform.js b/src/v0/destinations/google_adwords_enhanced_conversions/transform.js index 55d0c16c8cc..007f16d7f8e 100644 --- a/src/v0/destinations/google_adwords_enhanced_conversions/transform.js +++ b/src/v0/destinations/google_adwords_enhanced_conversions/transform.js @@ -1,8 +1,9 @@ /* eslint-disable no-param-reassign */ const get = require('get-value'); -const { cloneDeep } = require('lodash'); +const { cloneDeep, isNumber } = require('lodash'); const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); +const isString = require('lodash/isString'); const { constructPayload, defaultRequestConfig, @@ -35,7 +36,18 @@ const updateMappingJson = (mapping) => { const responseBuilder = async (metadata, message, { Config }, payload) => { const response = defaultRequestConfig(); const { event } = message; - const filteredCustomerId = removeHyphens(Config.customerId); + const { subAccount } = Config; + let { customerId, loginCustomerId } = Config; + if (isNumber(customerId)) { + customerId = customerId.toString(); + } + if (isNumber(loginCustomerId)) { + loginCustomerId = loginCustomerId.toString(); + } + if (!isString(customerId) || !isString(loginCustomerId)) { + throw new InstrumentationError('customerId and loginCustomerId should be a string or number'); + } + const filteredCustomerId = removeHyphens(customerId); response.endpoint = `${BASE_ENDPOINT}/${filteredCustomerId}:uploadConversionAdjustments`; response.body.JSON = payload; const accessToken = getAccessToken(metadata, 'access_token'); @@ -45,9 +57,9 @@ const responseBuilder = async (metadata, message, { Config }, payload) => { 'developer-token': getValueFromMessage(metadata, 'secret.developer_token'), }; response.params = { event, customerId: filteredCustomerId }; - if (Config.subAccount) - if (Config.loginCustomerId) { - const filteredLoginCustomerId = removeHyphens(Config.loginCustomerId); + if (subAccount) + if (loginCustomerId) { + const filteredLoginCustomerId = removeHyphens(loginCustomerId); response.headers['login-customer-id'] = filteredLoginCustomerId; } else throw new ConfigurationError(`LoginCustomerId is required as subAccount is true.`); diff --git a/src/v0/util/index.js b/src/v0/util/index.js index ca81262f88e..f034ab802b2 100644 --- a/src/v0/util/index.js +++ b/src/v0/util/index.js @@ -26,6 +26,7 @@ const { } = require('@rudderstack/integrations-lib'); const { JsonTemplateEngine, PathType } = require('@rudderstack/json-template-engine'); +const isString = require('lodash/isString'); const logger = require('../../logger'); const stats = require('../../util/stats'); const { DestCanonicalNames, DestHandlerMap } = require('../../constants/destinationCanonicalNames'); @@ -1622,7 +1623,7 @@ function isHttpStatusRetryable(status) { function generateUUID() { return crypto.randomUUID({ disableEntropyCache: true, - }); /* using disableEntropyCache as true to not cache the generated uuids. + }); /* using disableEntropyCache as true to not cache the generated uuids. For more Info https://nodejs.org/api/crypto.html#cryptorandomuuidoptions:~:text=options%20%3CObject%3E-,disableEntropyCache,-%3Cboolean%3E%20By */ } @@ -1646,6 +1647,9 @@ function isAppleFamily(platform) { } function removeHyphens(str) { + if (!isString(str)) { + return str; + } return str.replace(/-/g, ''); } diff --git a/src/v0/util/index.test.js b/src/v0/util/index.test.js index 6bf689eca72..eaf8b79d54e 100644 --- a/src/v0/util/index.test.js +++ b/src/v0/util/index.test.js @@ -10,6 +10,7 @@ const { validateEventAndLowerCaseConversion, groupRouterTransformEvents, isAxiosError, + removeHyphens, } = require('./index'); const exp = require('constants'); @@ -968,3 +969,18 @@ describe('isAxiosError', () => { expect(isAxiosError(error)).toBe(false); }); }); + +describe('removeHyphens', () => { + const data = [ + { input: 'hello-w--orld', expected: 'helloworld' }, + { input: '', expected: '' }, + { input: null, expected: null }, + { input: undefined, expected: undefined }, + { input: 12345, expected: 12345 }, + ]; + it('should remove hyphens from string else return the input as it is', () => { + data.forEach(({ input, expected }) => { + expect(removeHyphens(input)).toBe(expected); + }); + }); +}); 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 1d77b5d7743..89ce06818b4 100644 --- a/test/integrations/destinations/google_adwords_enhanced_conversions/router/data.ts +++ b/test/integrations/destinations/google_adwords_enhanced_conversions/router/data.ts @@ -225,6 +225,194 @@ const events = [ sentAt: '2019-10-14T11:15:53.296Z', }, }, + { + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + jobId: 4, + userId: 'u1', + }, + destination: { + Config: { + rudderAccountId: '25u5whFH7gVTnCiAjn4ykoCLGoC', + customerId: '{{event.context.customerID || "" }}', + subAccount: true, + loginCustomerId: '{{event.context.subaccountID || "" }}', + 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', + ip: '0.0.0.0', + os: { name: '', version: '' }, + screen: { density: 2 }, + customerID: 1234567890, + subaccountID: 11, + }, + 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', + }, + }, + { + metadata: { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + jobId: 5, + userId: 'u1', + }, + destination: { + Config: { + rudderAccountId: '25u5whFH7gVTnCiAjn4ykoCLGoC', + customerId: '{{event.context.customerID || "" }}', + subAccount: true, + loginCustomerId: '{{event.context.subaccountID || "" }}', + 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', + ip: '0.0.0.0', + os: { name: '', version: '' }, + screen: { density: 2 }, + customerID: {}, + subaccountID: 11, + }, + 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', + }, + }, ]; const invalidRtTfCases = [ @@ -500,6 +688,125 @@ export const data = [ module: 'destination', }, }, + { + 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': '11', + }, + 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: 4, + userId: 'u1', + }, + ], + batched: false, + statusCode: 200, + destination: { + Config: { + rudderAccountId: '25u5whFH7gVTnCiAjn4ykoCLGoC', + customerId: 1234567890, + subAccount: true, + loginCustomerId: 11, + listOfConversions: [ + { conversions: 'Page View' }, + { conversions: 'Product Added' }, + ], + authStatus: 'active', + }, + }, + }, + { + batched: false, + statusCode: 400, + error: 'customerId and loginCustomerId should be a string or number', + statTags: { + destType: 'GOOGLE_ADWORDS_ENHANCED_CONVERSIONS', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'router', + implementation: 'native', + module: 'destination', + }, + metadata: [ + { + secret: { + access_token: 'abcd1234', + refresh_token: 'efgh5678', + developer_token: 'ijkl91011', + }, + jobId: 5, + userId: 'u1', + }, + ], + destination: { + Config: { + rudderAccountId: '25u5whFH7gVTnCiAjn4ykoCLGoC', + customerId: {}, + subAccount: true, + loginCustomerId: 11, + listOfConversions: [ + { conversions: 'Page View' }, + { conversions: 'Product Added' }, + ], + authStatus: 'active', + }, + }, + }, ], }, },