diff --git a/.github/workflows/dt-test-and-report-code-coverage.yml b/.github/workflows/dt-test-and-report-code-coverage.yml index 2c76898882..12bd9ac78b 100644 --- a/.github/workflows/dt-test-and-report-code-coverage.yml +++ b/.github/workflows/dt-test-and-report-code-coverage.yml @@ -33,7 +33,7 @@ jobs: - name: Checkout uses: actions/checkout@v4.2.1 with: - fetch-depth: 1 + fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4.0.4 diff --git a/src/v0/destinations/google_adwords_offline_conversions/transform.js b/src/v0/destinations/google_adwords_offline_conversions/transform.js index 76b12587cd..2648f03e8a 100644 --- a/src/v0/destinations/google_adwords_offline_conversions/transform.js +++ b/src/v0/destinations/google_adwords_offline_conversions/transform.js @@ -18,6 +18,7 @@ const { getClickConversionPayloadAndEndpoint, getConsentsDataFromIntegrationObj, getCallConversionPayload, + updateConversion, } = require('./utils'); const helper = require('./helper'); @@ -48,6 +49,9 @@ const getConversions = (message, metadata, { Config }, event, conversionType) => filteredCustomerId, eventLevelConsentsData, ); + convertedPayload.payload.conversions[0] = updateConversion( + convertedPayload.payload.conversions[0], + ); payload = convertedPayload.payload; endpoint = convertedPayload.endpoint; } else if (conversionType === 'store') { diff --git a/src/v0/destinations/google_adwords_offline_conversions/utils.js b/src/v0/destinations/google_adwords_offline_conversions/utils.js index 89fe609df9..2d47095eea 100644 --- a/src/v0/destinations/google_adwords_offline_conversions/utils.js +++ b/src/v0/destinations/google_adwords_offline_conversions/utils.js @@ -317,6 +317,35 @@ const getStoreConversionPayload = (message, Config, event) => { return payload; }; +const hasClickId = (conversion) => { + const { gbraid, wbraid, gclid } = conversion; + return gclid || wbraid || gbraid; +}; +const populateUserIdentifier = ({ email, phone, properties, payload, UserIdentifierSource }) => { + const copiedPayload = cloneDeep(payload); + // userIdentifierSource + // if userIdentifierSource doesn't exist in properties + // then it is taken from the webapp config + if (!properties.userIdentifierSource && UserIdentifierSource !== 'none') { + set( + copiedPayload, + 'conversions[0].userIdentifiers[0].userIdentifierSource', + UserIdentifierSource, + ); + // one of email or phone must be provided when none of gclid, wbraid and gbraid provided + } + if (!email && !phone) { + if (!hasClickId(copiedPayload.conversions[0])) { + throw new InstrumentationError( + `Either an email address or a phone number is required for user identification when none of gclid, wbraid, or gbraid is provided.`, + ); + } else { + // we are deleting userIdentifiers if any one of gclid, wbraid and gbraid is there but email or phone is not present + delete copiedPayload.conversions[0].userIdentifiers; + } + } + return copiedPayload; +}; const getClickConversionPayloadAndEndpoint = ( message, Config, @@ -335,7 +364,7 @@ const getClickConversionPayloadAndEndpoint = ( updatedClickMapping = removeHashToSha256TypeFromMappingJson(updatedClickMapping); } - const payload = constructPayload(message, updatedClickMapping); + let payload = constructPayload(message, updatedClickMapping); const endpoint = CLICK_CONVERSION.replace(':customerId', filteredCustomerId); @@ -353,17 +382,8 @@ const getClickConversionPayloadAndEndpoint = ( set(payload, 'conversions[0].cartData.items', itemList); } - // userIdentifierSource - // if userIdentifierSource doesn't exist in properties - // then it is taken from the webapp config - if (!properties.userIdentifierSource && UserIdentifierSource !== 'none') { - set(payload, 'conversions[0].userIdentifiers[0].userIdentifierSource', UserIdentifierSource); + payload = populateUserIdentifier({ email, phone, properties, payload, UserIdentifierSource }); - // one of email or phone must be provided - if (!email && !phone) { - throw new InstrumentationError(`Either of email or phone is required for user identifier`); - } - } // either of email or phone should be passed // defaultUserIdentifier depends on the webapp configuration // Ref - https://developers.google.com/google-ads/api/rest/reference/rest/v11/customers/uploadClickConversions#ClickConversion @@ -411,6 +431,25 @@ const getConsentsDataFromIntegrationObj = (message) => { return integrationObj?.consents || {}; }; +/** + * remove redundant ids + * @param {*} conversionCopy + */ +const updateConversion = (conversion) => { + const conversionCopy = cloneDeep(conversion); + if (conversionCopy.gclid) { + delete conversionCopy.wbraid; + delete conversionCopy.gbraid; + } + if (conversionCopy.wbraid && conversionCopy.gbraid) { + throw new InstrumentationError(`You can't use both wbraid and gbraid.`); + } + if (conversionCopy.wbraid || conversionCopy.gbraid) { + delete conversionCopy.userIdentifiers; + } + return conversionCopy; +}; + module.exports = { validateDestinationConfig, generateItemListFromProducts, @@ -423,4 +462,5 @@ module.exports = { getExisitingUserIdentifier, getConsentsDataFromIntegrationObj, getCallConversionPayload, + updateConversion, }; diff --git a/src/v0/destinations/google_adwords_offline_conversions/utils.test.js b/src/v0/destinations/google_adwords_offline_conversions/utils.test.js index 2d1863413c..b6c6653782 100644 --- a/src/v0/destinations/google_adwords_offline_conversions/utils.test.js +++ b/src/v0/destinations/google_adwords_offline_conversions/utils.test.js @@ -244,7 +244,9 @@ describe('getClickConversionPayloadAndEndpoint util tests', () => { }; expect(() => getClickConversionPayloadAndEndpoint(fittingPayload, config, '9625812972'), - ).toThrow('Either of email or phone is required for user identifier'); + ).toThrow( + 'Either an email address or a phone number is required for user identification when none of gclid, wbraid, or gbraid is provided.', + ); }); it('finaliseConsent', () => { diff --git a/test/integrations/destinations/google_adwords_offline_conversions/processor/data.ts b/test/integrations/destinations/google_adwords_offline_conversions/processor/data.ts index ab3e19dc2f..3ecae2b92d 100644 --- a/test/integrations/destinations/google_adwords_offline_conversions/processor/data.ts +++ b/test/integrations/destinations/google_adwords_offline_conversions/processor/data.ts @@ -231,8 +231,6 @@ export const data = [ JSON: { conversions: [ { - gbraid: 'gbraid', - wbraid: 'wbraid', externalAttributionData: { externalAttributionCredit: 10, externalAttributionModel: 'externalAttributionModel', @@ -528,8 +526,6 @@ export const data = [ adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED', }, - gbraid: 'gbraid', - wbraid: 'wbraid', externalAttributionData: { externalAttributionCredit: 10, externalAttributionModel: 'externalAttributionModel', @@ -821,8 +817,6 @@ export const data = [ adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED', }, - gbraid: 'gbraid', - wbraid: 'wbraid', externalAttributionData: { externalAttributionCredit: 10, externalAttributionModel: 'externalAttributionModel', @@ -1911,7 +1905,6 @@ export const data = [ userId: '12345', properties: { gbraid: 'gbraid', - wbraid: 'wbraid', externalAttributionCredit: 10, externalAttributionModel: 'externalAttributionModel', conversionCustomVariable: 'conversionCustomVariable', @@ -1935,7 +1928,6 @@ export const data = [ ], userIdentifierSource: 'FIRST_PARTY', conversionEnvironment: 'WEB', - gclid: 'gclid', conversionDateTime: '2022-01-01 12:32:45-08:00', conversionValue: '1', currency: 'GBP', @@ -2052,7 +2044,6 @@ export const data = [ ], properties: { gbraid: 'gbraid', - wbraid: 'wbraid', externalAttributionCredit: 10, externalAttributionModel: 'externalAttributionModel', conversionCustomVariable: 'conversionCustomVariable', @@ -2076,7 +2067,6 @@ export const data = [ ], userIdentifierSource: 'FIRST_PARTY', conversionEnvironment: 'WEB', - gclid: 'gclid', conversionDateTime: '2022-01-01 12:32:45-08:00', conversionValue: '1', currency: 'GBP', @@ -2093,7 +2083,6 @@ export const data = [ adUserData: 'UNSPECIFIED', }, gbraid: 'gbraid', - wbraid: 'wbraid', externalAttributionData: { externalAttributionCredit: 10, externalAttributionModel: 'externalAttributionModel', @@ -2110,15 +2099,7 @@ export const data = [ }, ], }, - userIdentifiers: [ - { - userIdentifierSource: 'FIRST_PARTY', - hashedPhoneNumber: - '04e1dabb7c1348b72bfa87da179c9697c69af74827649266a5da8cdbb367abcd', - }, - ], conversionEnvironment: 'WEB', - gclid: 'gclid', conversionDateTime: '2022-01-01 12:32:45-08:00', conversionValue: 1, currencyCode: 'GBP', @@ -2169,7 +2150,6 @@ export const data = [ ], properties: { gbraid: 'gbraid', - wbraid: 'wbraid', externalAttributionCredit: 10, externalAttributionModel: 'externalAttributionModel', conversionCustomVariable: 'conversionCustomVariable', @@ -2193,7 +2173,6 @@ export const data = [ ], userIdentifierSource: 'FIRST_PARTY', conversionEnvironment: 'WEB', - gclid: 'gclid', conversionDateTime: '2022-01-01 12:32:45-08:00', conversionValue: '1', currency: 'GBP', @@ -3751,20 +3730,73 @@ export const data = [ metadata: { secret: { access_token: 'abcd1234', - refresh_token: 'efgh5678', developer_token: 'ijkl91011', + refresh_token: 'efgh5678', }, }, - statusCode: 400, - error: 'Either of email or phone is required for user identifier', - statTags: { - errorCategory: 'dataValidation', - errorType: 'instrumentation', - destType: 'GOOGLE_ADWORDS_OFFLINE_CONVERSIONS', - module: 'destination', - implementation: 'native', - feature: 'processor', + output: { + body: { + FORM: {}, + JSON: { + conversions: [ + { + cartData: { + items: [ + { + productId: '123445', + quantity: 123, + }, + ], + }, + consent: { + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }, + conversionDateTime: '2022-01-01 12:32:45-08:00', + conversionEnvironment: 'WEB', + gclid: 'gclid', + }, + ], + partialFailure: true, + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: + 'https://googleads.googleapis.com/v16/customers/9625812972:uploadClickConversions', + files: {}, + headers: { + Authorization: 'Bearer abcd1234', + 'Content-Type': 'application/json', + 'developer-token': 'ijkl91011', + 'login-customer-id': '8617859087', + }, + method: 'POST', + params: { + customVariables: [ + { + from: 'value', + to: 'revenue', + }, + { + from: 'total', + to: 'cost', + }, + ], + customerId: '9625812972', + event: 'Sign-up - click', + properties: { + conversionDateTime: '2022-01-01 12:32:45-08:00', + gclid: 'gclid', + product_id: '123445', + quantity: 123, + }, + }, + type: 'REST', + userId: '', + version: '1', }, + statusCode: 200, }, ], }, @@ -5704,4 +5736,197 @@ export const data = [ }, mockFns: timestampMock, }, + { + name: 'google_adwords_offline_conversions', + description: 'Test 28 : when both gbraid and wbraid are available', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + 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: { + phone: 'alex@example.com', + 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', + messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', + originalTimestamp: '2019-10-14T11:15:18.299Z', + anonymousId: '00000000000000000000000000', + userId: '12345', + properties: { + gbraid: 'gbraid', + wbraid: 'wbraid', + externalAttributionCredit: 10, + externalAttributionModel: 'externalAttributionModel', + conversionCustomVariable: 'conversionCustomVariable', + value: 'value', + merchantId: '9876merchantId', + feedCountryCode: 'feedCountryCode', + feedLanguageCode: 'feedLanguageCode', + localTransactionCost: 20, + products: [ + { + product_id: '507f1f77bcf86cd799439011', + quantity: '2', + price: '50', + sku: '45790-32', + name: 'Monopoly: 3rd Edition', + position: '1', + category: 'cars', + url: 'https://www.example.com/product/path', + image_url: 'https://www.example.com/product/path.jpg', + }, + ], + userIdentifierSource: 'FIRST_PARTY', + conversionEnvironment: 'WEB', + conversionValue: '1', + currency: 'GBP', + orderId: 'PL-123QR', + }, + 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: { + customerId: '962-581-2972', + subAccount: false, + eventsToOfflineConversionsTypeMapping: [ + { + from: 'Sign up completed', + to: 'click', + }, + { + from: 'Download', + to: 'call', + }, + { + from: 'Promotion Clicked', + to: 'click', + }, + { + from: 'Product Searched', + to: 'call', + }, + ], + eventsToConversionsNamesMapping: [ + { + from: 'Sign up completed', + to: 'Sign-up - click', + }, + { + from: 'Download', + to: 'Page view', + }, + { + from: 'Promotion Clicked', + to: 'Sign-up - click', + }, + { + from: 'Product Searched', + to: 'search', + }, + ], + customVariables: [ + { + from: 'value', + to: 'revenue', + }, + { + from: 'total', + to: 'cost', + }, + ], + UserIdentifierSource: 'THIRD_PARTY', + conversionEnvironment: 'WEB', + hashUserIdentifier: true, + defaultUserIdentifier: 'email', + validateOnly: false, + rudderAccountId: '2EOknn1JNH7WK1MfNku4fGYKkRK', + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: "You can't use both wbraid and gbraid.", + metadata: { + secret: { + access_token: 'abcd1234', + developer_token: 'ijkl91011', + refresh_token: 'efgh5678', + }, + }, + statTags: { + destType: 'GOOGLE_ADWORDS_OFFLINE_CONVERSIONS', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'native', + module: 'destination', + }, + statusCode: 400, + }, + ], + }, + }, + mockFns: timestampMock, + }, ]; diff --git a/test/integrations/destinations/google_adwords_offline_conversions/router/data.ts b/test/integrations/destinations/google_adwords_offline_conversions/router/data.ts index 9d1ba220c8..82c8e9b3ff 100644 --- a/test/integrations/destinations/google_adwords_offline_conversions/router/data.ts +++ b/test/integrations/destinations/google_adwords_offline_conversions/router/data.ts @@ -738,8 +738,6 @@ export const data = [ adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED', }, - gbraid: 'gbraid', - wbraid: 'wbraid', externalAttributionData: { externalAttributionCredit: 10, externalAttributionModel: 'externalAttributionModel',