From dedca0796ee12478d57e020ef3c254afabc6e105 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Thu, 25 Jul 2024 10:44:01 +0530 Subject: [PATCH 1/2] fix: added support for ga4 v2 hybrid mode (#3586) --- src/constants/destinationCanonicalNames.js | 8 ++ src/v0/destinations/ga4/transform.js | 21 +-- src/v0/destinations/ga4/utils.js | 23 ++- .../ga4_v2/customMappingsHandler.js | 14 +- src/v0/destinations/ga4_v2/transform.ts | 4 +- .../ga4_v2/processor/customMappings.ts | 134 +++++++++++++++++- test/integrations/testUtils.ts | 4 +- 7 files changed, 181 insertions(+), 27 deletions(-) diff --git a/src/constants/destinationCanonicalNames.js b/src/constants/destinationCanonicalNames.js index 58bf35539ac..1f10f45a38e 100644 --- a/src/constants/destinationCanonicalNames.js +++ b/src/constants/destinationCanonicalNames.js @@ -101,6 +101,14 @@ const DestCanonicalNames = { awin: ['awin', 'Awin', 'AWIN'], sendinblue: ['sendinblue', 'SENDINBLUE', 'Sendinblue', 'SendinBlue'], ga4: ['GA4', 'ga4', 'Ga4', 'Google Analytics 4', 'googleAnalytics4', 'Google Analytics 4 (GA4)'], + ga4_v2: [ + 'GA4_V2', + 'ga4_v2', + 'Ga4_v2', + 'Google Analytics 4 V2', + 'googleAnalytics4V2', + 'Google Analytics 4 (GA4) V2', + ], pipedream: ['Pipedream', 'PipeDream', 'pipedream', 'PIPEDREAM'], pagerduty: ['pagerduty', 'PAGERDUTY', 'PagerDuty', 'Pagerduty', 'pagerDuty'], adobe_analytics: [ diff --git a/src/v0/destinations/ga4/transform.js b/src/v0/destinations/ga4/transform.js index e4dad805642..feafc4c45f5 100644 --- a/src/v0/destinations/ga4/transform.js +++ b/src/v0/destinations/ga4/transform.js @@ -39,7 +39,7 @@ require('../../util/constant'); * @param {*} Config * @returns */ -const responseBuilder = (message, { Config }) => { +const responseBuilder = (message, { Config }, destType) => { let event = get(message, 'event'); basicValidation(event); @@ -54,7 +54,7 @@ const responseBuilder = (message, { Config }) => { // get common top level rawPayload let rawPayload = constructPayload(message, trackCommonConfig); - rawPayload = addClientDetails(rawPayload, message, Config); + rawPayload = addClientDetails(rawPayload, message, Config, destType); let payload = {}; const eventConfig = ConfigCategory[`${event.toUpperCase()}`]; @@ -162,7 +162,7 @@ const responseBuilder = (message, { Config }) => { } removeReservedParameterPrefixNames(payload.params); - const integrationsObj = getIntegrationsObj(message, 'ga4'); + const integrationsObj = getIntegrationsObj(message, destType); if (isHybridModeEnabled(Config) && integrationsObj && integrationsObj.sessionId) { payload.params.session_id = integrationsObj.sessionId; } @@ -186,7 +186,7 @@ const responseBuilder = (message, { Config }) => { } // Prepare GA4 consents - const consents = prepareUserConsents(message); + const consents = prepareUserConsents(message, destType); if (!isEmptyObject(consents)) { rawPayload.consent = consents; } @@ -197,7 +197,7 @@ const responseBuilder = (message, { Config }) => { return buildDeliverablePayload(rawPayload, Config); }; -const process = (event) => { +const processEvents = ({ event, destType = 'ga4' }) => { const { message, destination } = event; const { Config } = destination; @@ -212,13 +212,13 @@ const process = (event) => { let response; switch (messageType) { case EventType.TRACK: - response = responseBuilder(message, destination); + response = responseBuilder(message, destination, destType); break; case EventType.PAGE: // GA4 custom event 'page_view' is fired for page if (!isHybridModeEnabled(Config)) { message.event = 'page_view'; - response = responseBuilder(message, destination); + response = responseBuilder(message, destination, destType); } else { throw new UnsupportedEventError( 'GA4 Hybrid mode is enabled, page calls will be sent through device mode', @@ -228,7 +228,7 @@ const process = (event) => { case EventType.GROUP: // GA4 standard event 'join_group' is fired for group message.event = 'join_group'; - response = responseBuilder(message, destination); + response = responseBuilder(message, destination, destType); break; default: throw new InstrumentationError(`Message type ${messageType} not supported`); @@ -236,4 +236,7 @@ const process = (event) => { return response; }; -module.exports = { process }; +// Keeping this for other params which comes as part of process args +const process = (event) => processEvents({ event }); + +module.exports = { process, processEvents }; diff --git a/src/v0/destinations/ga4/utils.js b/src/v0/destinations/ga4/utils.js index 18994efd7fc..7b9528143c2 100644 --- a/src/v0/destinations/ga4/utils.js +++ b/src/v0/destinations/ga4/utils.js @@ -448,8 +448,8 @@ const prepareUserProperties = (message, piiPropertiesToIgnore = []) => { * @param {*} message * @returns */ -const prepareUserConsents = (message) => { - const integrationObj = getIntegrationsObj(message, 'ga4') || {}; +const prepareUserConsents = (message, destType = 'ga4') => { + const integrationObj = getIntegrationsObj(message, destType) || {}; const eventLevelConsentsData = integrationObj?.consents || {}; const consentConfigMap = { analyticsPersonalizationConsent: 'ad_user_data', @@ -474,11 +474,11 @@ const basicValidation = (event) => { * @param {*} message * @returns */ -const getGA4ClientId = (message, Config) => { +const getGA4ClientId = (message, Config, destType) => { let clientId; if (isHybridModeEnabled(Config)) { - const integrationsObj = getIntegrationsObj(message, 'ga4'); + const integrationsObj = getIntegrationsObj(message, destType); if (integrationsObj?.clientId) { clientId = integrationsObj.clientId; } @@ -494,14 +494,14 @@ const getGA4ClientId = (message, Config) => { return clientId; }; -const addClientDetails = (payload, message, Config) => { +const addClientDetails = (payload, message, Config, destType = 'ga4') => { const { typesOfClient } = Config; const rawPayload = cloneDeep(payload); switch (typesOfClient) { case 'gtag': // gtag.js uses client_id // GA4 uses it as an identifier to distinguish site visitors. - rawPayload.client_id = getGA4ClientId(message, Config); + rawPayload.client_id = getGA4ClientId(message, Config, destType); if (!isDefinedAndNotNull(rawPayload.client_id)) { throw new ConfigurationError('ga4ClientId, anonymousId or messageId must be provided'); } @@ -581,6 +581,16 @@ const basicConfigvalidaiton = (Config) => { } }; +const addSessionDetailsForHybridMode = (ga4Payload, message, Config, destType = 'ga4') => { + const integrationsObj = getIntegrationsObj(message, destType); + if (isHybridModeEnabled(Config) && integrationsObj?.sessionId) { + ga4Payload.events[0].params.session_id = `${integrationsObj.sessionId}`; + } + if (integrationsObj?.sessionNumber) { + ga4Payload.events[0].params.session_number = integrationsObj.sessionNumber; + } +}; + module.exports = { addClientDetails, basicValidation, @@ -603,4 +613,5 @@ module.exports = { GA4_RESERVED_PARAMETER_EXCLUSION, removeReservedParameterPrefixNames, GA4_RESERVED_USER_PROPERTY_EXCLUSION, + addSessionDetailsForHybridMode, }; diff --git a/src/v0/destinations/ga4_v2/customMappingsHandler.js b/src/v0/destinations/ga4_v2/customMappingsHandler.js index 9e827881501..b5818d6fff1 100644 --- a/src/v0/destinations/ga4_v2/customMappingsHandler.js +++ b/src/v0/destinations/ga4_v2/customMappingsHandler.js @@ -13,6 +13,7 @@ const { GA4_PARAMETERS_EXCLUSION, prepareUserProperties, sanitizeUserProperties, + addSessionDetailsForHybridMode, } = require('../ga4/utils'); const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); const { @@ -71,6 +72,7 @@ const handleCustomMappings = (message, Config) => { // Default mapping let rawPayload = constructPayload(message, trackCommonConfig); + rawPayload = addClientDetails(rawPayload, message, Config, 'ga4_v2'); const ga4EventPayload = {}; @@ -113,7 +115,7 @@ const handleCustomMappings = (message, Config) => { // Add common top level payload let ga4BasicPayload = constructPayload(message, trackCommonConfig); - ga4BasicPayload = addClientDetails(ga4BasicPayload, message, Config); + ga4BasicPayload = addClientDetails(ga4BasicPayload, message, Config, 'ga4_v2'); const eventPropertiesMappings = mapping.eventProperties || []; @@ -143,11 +145,7 @@ const handleCustomMappings = (message, Config) => { const boilerplateOperations = (ga4Payload, message, Config, eventName) => { removeReservedParameterPrefixNames(ga4Payload.events[0].params); ga4Payload.events[0].name = eventName; - const integrationsObj = getIntegrationsObj(message, 'ga4'); - - if (isHybridModeEnabled(Config) && integrationsObj?.sessionId) { - ga4Payload.events[0].params.session_id = integrationsObj.sessionId; - } + const integrationsObj = getIntegrationsObj(message, 'ga4_v2'); if (ga4Payload.events[0].params) { ga4Payload.events[0].params = removeInvalidParams( @@ -160,7 +158,7 @@ const boilerplateOperations = (ga4Payload, message, Config, eventName) => { } // Prepare GA4 consents - const consents = prepareUserConsents(message); + const consents = prepareUserConsents(message, 'ga4_v2'); if (!isEmptyObject(consents)) { ga4Payload.consent = consents; } @@ -169,6 +167,8 @@ const boilerplateOperations = (ga4Payload, message, Config, eventName) => { if (isDefinedAndNotNull(ga4Payload.user_properties)) { ga4Payload.user_properties = sanitizeUserProperties(ga4Payload.user_properties); } + + addSessionDetailsForHybridMode(ga4Payload, message, Config, 'ga4_v2'); }; module.exports = { diff --git a/src/v0/destinations/ga4_v2/transform.ts b/src/v0/destinations/ga4_v2/transform.ts index 79892d791ef..06d4bd90236 100644 --- a/src/v0/destinations/ga4_v2/transform.ts +++ b/src/v0/destinations/ga4_v2/transform.ts @@ -5,7 +5,7 @@ import { } from '@rudderstack/integrations-lib'; import { ProcessorTransformationRequest } from '../../../types'; import { handleCustomMappings } from './customMappingsHandler'; -import { process as ga4Process } from '../ga4/transform'; +import { processEvents as ga4Process } from '../ga4/transform'; import { basicConfigvalidaiton } from '../ga4/utils'; export function process(event: ProcessorTransformationRequest) { @@ -30,7 +30,7 @@ export function process(event: ProcessorTransformationRequest) { } if (eventPayload.type !== 'track') { - return ga4Process(event); + return ga4Process({ event, destType: 'ga4_v2' }); } basicConfigvalidaiton(Config); diff --git a/test/integrations/destinations/ga4_v2/processor/customMappings.ts b/test/integrations/destinations/ga4_v2/processor/customMappings.ts index b1db2121eaa..28e8c5a5b4c 100644 --- a/test/integrations/destinations/ga4_v2/processor/customMappings.ts +++ b/test/integrations/destinations/ga4_v2/processor/customMappings.ts @@ -1,3 +1,5 @@ +import { Destination } from '../../../../../src/types'; +import { overrideDestination } from '../../../testUtils'; import { defaultMockFns } from '../mocks'; const traits = { @@ -65,7 +67,10 @@ const properties = { }; const integrations = { - GA4: { + 'Google Analytics 4 (GA4) V2': { + clientId: '6d374e5d-78d3-4169-991d-2573ec0d1c67', + sessionId: '123', + sessionNumber: 3, consents: { ad_personalization: 'GRANTED', ad_user_data: 'DENIED', @@ -212,7 +217,7 @@ const eventsMapping = [ }, ]; -const destination = { +const destination: Destination = { Config: { apiSecret: 'dummyApiSecret', measurementId: 'G-T40PE6KET4', @@ -224,6 +229,19 @@ const destination = { eventFilteringOption: 'disable', eventsMapping, }, + ID: 'ga4_v2', + Name: 'Google Analytics 4', + Enabled: true, + WorkspaceID: '123', + DestinationDefinition: { + ID: '123', + Name: 'Google Analytics 4', + DisplayName: 'Google Analytics 4', + Config: {}, + }, + IsProcessorEnabled: true, + Transformations: [], + IsConnectionEnabled: true, }; export const customMappingTestCases = [ { @@ -297,6 +315,7 @@ export const customMappingTestCases = [ }, ], prices: 456, + session_number: 3, }, }, ], @@ -398,6 +417,7 @@ export const customMappingTestCases = [ }, ], prices: 456, + session_number: 3, }, }, ], @@ -461,6 +481,7 @@ export const customMappingTestCases = [ }, ], prices: 456, + session_number: 3, }, }, ], @@ -540,6 +561,7 @@ export const customMappingTestCases = [ }, body: { JSON: { + client_id: 'root_anonId', user_id: 'root_user', timestamp_micros: 1651105389000000, non_personalized_ads: false, @@ -594,6 +616,7 @@ export const customMappingTestCases = [ products_1_item_category2: 'regulars', products_1_item_category3: 'grocery', products_1_some_data: 'someValue', + session_number: 3, }, }, ], @@ -668,6 +691,111 @@ export const customMappingTestCases = [ timestamp_micros: 1651105389000000, non_personalized_ads: false, client_id: 'root_anonId', + events: [ + { + name: 'join_group', + params: { + city: 'London', + engagement_time_msec: 1, + firstName: 'John', + group: 'test group', + lastName: 'Gomes', + session_number: 3, + state: 'UK', + streetAddress: '71 Cherry Court SOUTHAMPTON SO53 5PD UK', + }, + }, + ], + user_properties: { + firstName: { + value: 'John', + }, + lastName: { + value: 'Gomes', + }, + city: { + value: 'London', + }, + state: { + value: 'UK', + }, + group: { + value: 'test group', + }, + }, + consent: { + ad_user_data: 'DENIED', + ad_personalization: 'GRANTED', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + name: 'ga4_v2', + id: 'ga4_custom_mapping_test_4', + description: 'Custom Mapping Test For Hybrid mode', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'group', + userId: 'root_user', + anonymousId: 'root_anonId', + context: { + device, + traits, + }, + properties, + originalTimestamp: '2022-04-28T00:23:09.544Z', + integrations, + }, + destination: overrideDestination(destination, { + connectionMode: 'hybrid', + }), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.google-analytics.com/mp/collect', + headers: { + HOST: 'www.google-analytics.com', + 'Content-Type': 'application/json', + }, + params: { + api_secret: 'dummyApiSecret', + measurement_id: 'G-T40PE6KET4', + }, + body: { + JSON: { + user_id: 'root_user', + timestamp_micros: 1651105389000000, + non_personalized_ads: false, + client_id: '6d374e5d-78d3-4169-991d-2573ec0d1c67', events: [ { name: 'join_group', @@ -679,6 +807,8 @@ export const customMappingTestCases = [ lastName: 'Gomes', state: 'UK', streetAddress: '71 Cherry Court SOUTHAMPTON SO53 5PD UK', + session_id: '123', + session_number: 3, }, }, ], diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 5e0df874f89..8fd077ba7b1 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -25,7 +25,9 @@ export const getTestDataFilePaths = (dirPath: string, opts: OptionValues): strin const globPattern = join(dirPath, '**', 'data.ts'); let testFilePaths = globSync(globPattern); if (opts.destination) { - testFilePaths = testFilePaths.filter((testFile) => testFile.includes(opts.destination)); + testFilePaths = testFilePaths.filter((testFile) => + testFile.includes(`/destinations/${opts.destination}/`), + ); } if (opts.feature) { testFilePaths = testFilePaths.filter((testFile) => testFile.includes(opts.feature)); From be496eed05304dea5b31d945fa5b8028fea7b36f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 25 Jul 2024 05:55:06 +0000 Subject: [PATCH 2/2] chore(release): 1.72.4 --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2223b8aa2d7..6bdbe979209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.72.4](https://github.com/rudderlabs/rudder-transformer/compare/v1.72.3...v1.72.4) (2024-07-25) + + +### Bug Fixes + +* added support for ga4 v2 hybrid mode ([#3586](https://github.com/rudderlabs/rudder-transformer/issues/3586)) ([dedca07](https://github.com/rudderlabs/rudder-transformer/commit/dedca0796ee12478d57e020ef3c254afabc6e105)) + ### [1.72.3](https://github.com/rudderlabs/rudder-transformer/compare/v1.72.2...v1.72.3) (2024-07-24) diff --git a/package-lock.json b/package-lock.json index 7987a1a1ad5..5d5d0048371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rudder-transformer", - "version": "1.72.3", + "version": "1.72.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rudder-transformer", - "version": "1.72.3", + "version": "1.72.4", "license": "ISC", "dependencies": { "@amplitude/ua-parser-js": "0.7.24", diff --git a/package.json b/package.json index f47b1eb1249..7244147d323 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rudder-transformer", - "version": "1.72.3", + "version": "1.72.4", "description": "", "homepage": "https://github.com/rudderlabs/rudder-transformer#readme", "bugs": {