diff --git a/packages/GA4Client/src/commerce-handler.js b/packages/GA4Client/src/commerce-handler.js index 2356e99..4c0f825 100644 --- a/packages/GA4Client/src/commerce-handler.js +++ b/packages/GA4Client/src/commerce-handler.js @@ -54,18 +54,18 @@ CommerceHandler.prototype.logCommerceEvent = function (event) { event.CustomFlags[GA4_COMMERCE_EVENT_TYPE] === VIEW_CART ) { isViewCartEvent = true; - return logViewCart(event, affiliation); + return this.logViewCart(event, affiliation); } } // Handle Impressions if (event.EventCategory === ProductActionTypes.Impression) { - return logImpressionEvent(event, affiliation); + return this.logImpressionEvent(event, affiliation); // Handle Promotions } else if ( event.EventCategory === PromotionActionTypes.PromotionClick || event.EventCategory === PromotionActionTypes.PromotionView ) { - return logPromotionEvent(event); + return this.logPromotionEvent(event); } switch (event.EventCategory) { @@ -99,7 +99,7 @@ CommerceHandler.prototype.logCommerceEvent = function (event) { break; case ProductActionTypes.CheckoutOption: - return logCheckoutOptionEvent(event, affiliation); + return this.logCheckoutOptionEvent(event, affiliation); default: // a view cart event is handled at the beginning of this function @@ -136,10 +136,146 @@ CommerceHandler.prototype.logCommerceEvent = function (event) { null; } - gtag('event', mapGA4EcommerceEventName(event), ga4CommerceEventParameters); + return this.sendCommerceEventToGA4( + mapGA4EcommerceEventName(event), + ga4CommerceEventParameters + ); +}; + +CommerceHandler.prototype.sendCommerceEventToGA4 = function ( + eventName, + eventAttributes +) { + if (this.common.forwarderSettings.measurementId) { + eventAttributes.send_to = this.common.forwarderSettings.measurementId; + } + + gtag('event', eventName, eventAttributes); + return true; }; +// Google previously had a CheckoutOption event, and now this has been split into 2 GA4 events - add_shipping_info and add_payment_info +// Since MP still uses CheckoutOption, we must map this to the 2 events using custom flags. To prevent any data loss from customers +// migrating from UA to GA4, we will set a default `set_checkout_option` which matches Firebase's data model. +CommerceHandler.prototype.logCheckoutOptionEvent = function ( + event, + affiliation +) { + try { + var customFlags = event.CustomFlags, + GA4CommerceEventType = customFlags[GA4_COMMERCE_EVENT_TYPE], + ga4CommerceEventParameters; + + // if no custom flags exist or there is no GA4CommerceEventType, the user has not updated their code from using legacy GA which still supports checkout_option + if (!customFlags || !GA4CommerceEventType) { + console.error( + 'Your checkout option event for the Google Analytics 4 integration is missing a custom flag for "GA4.CommerceEventType". The event was sent using the deprecated set_checkout_option event. Review mParticle docs for GA4 for the custom flags to add.' + ); + } + + switch (GA4CommerceEventType) { + case ADD_SHIPPING_INFO: + ga4CommerceEventParameters = buildAddShippingInfo( + event, + affiliation + ); + break; + case ADD_PAYMENT_INFO: + ga4CommerceEventParameters = buildAddPaymentInfo( + event, + affiliation + ); + break; + default: + ga4CommerceEventParameters = buildCheckoutOptions( + event, + affiliation + ); + break; + } + } catch (error) { + console.error( + 'There is an issue with the custom flags in your CheckoutOption event. The event was not sent. Plrease review the docs and fix.', + error + ); + return false; + } + + return this.sendCommerceEventToGA4( + mapGA4EcommerceEventName(event), + ga4CommerceEventParameters + ); +}; + +CommerceHandler.prototype.logPromotionEvent = function (event) { + var self = this; + try { + var ga4CommerceEventParameters; + event.PromotionAction.PromotionList.forEach(function (promotion) { + ga4CommerceEventParameters = buildPromotion(promotion); + + self.sendCommerceEventToGA4( + mapGA4EcommerceEventName(event), + ga4CommerceEventParameters + ); + }); + + return true; + } catch (error) { + console.error( + 'Error logging Promotions to GA4. Promotions not logged.', + error + ); + return false; + } +}; + +CommerceHandler.prototype.logImpressionEvent = function (event, affiliation) { + var self = this; + try { + var ga4CommerceEventParameters; + event.ProductImpressions.forEach(function (impression) { + ga4CommerceEventParameters = parseImpression( + impression, + affiliation + ); + + self.sendCommerceEventToGA4( + mapGA4EcommerceEventName(event), + ga4CommerceEventParameters + ); + }); + + return true; + } catch (error) { + console.log( + 'Error logging Impressions to GA4. Impressions not logged', + error + ); + return false; + } +}; + +CommerceHandler.prototype.logViewCart = function (event, affiliation) { + var ga4CommerceEventParameters = buildViewCart(event, affiliation); + ga4CommerceEventParameters = this.common.mergeObjects( + ga4CommerceEventParameters, + this.common.limitEventAttributes(event.EventAttributes) + ); + ga4CommerceEventParameters.currency = event.CurrencyCode; + + ga4CommerceEventParameters.value = + (event.CustomFlags && event.CustomFlags['GA4.Value']) || + event.ProductAction.TotalAmount || + null; + + return this.sendCommerceEventToGA4( + mapGA4EcommerceEventName(event), + ga4CommerceEventParameters + ); +}; + function buildAddOrRemoveCartItem(event, affiliation) { return { items: buildProductsList(event.ProductAction.ProductList, affiliation), @@ -394,117 +530,6 @@ function getCheckoutOptionEventName(customFlags) { } } -// Google previously had a CheckoutOption event, and now this has been split into 2 GA4 events - add_shipping_info and add_payment_info -// Since MP still uses CheckoutOption, we must map this to the 2 events using custom flags. To prevent any data loss from customers -// migrating from UA to GA4, we will set a default `set_checkout_option` which matches Firebase's data model. -function logCheckoutOptionEvent(event, affiliation) { - try { - var customFlags = event.CustomFlags, - GA4CommerceEventType = customFlags[GA4_COMMERCE_EVENT_TYPE], - ga4CommerceEventParameters; - - // if no custom flags exist or there is no GA4CommerceEventType, the user has not updated their code from using legacy GA which still supports checkout_option - if (!customFlags || !GA4CommerceEventType) { - console.error( - 'Your checkout option event for the Google Analytics 4 integration is missing a custom flag for "GA4.CommerceEventType". The event was sent using the deprecated set_checkout_option event. Review mParticle docs for GA4 for the custom flags to add.' - ); - } - - switch (GA4CommerceEventType) { - case ADD_SHIPPING_INFO: - ga4CommerceEventParameters = buildAddShippingInfo( - event, - affiliation - ); - break; - case ADD_PAYMENT_INFO: - ga4CommerceEventParameters = buildAddPaymentInfo( - event, - affiliation - ); - break; - default: - ga4CommerceEventParameters = buildCheckoutOptions( - event, - affiliation - ); - break; - } - } catch (error) { - console.error( - 'There is an issue with the custom flags in your CheckoutOption event. The event was not sent. Plrease review the docs and fix.', - error - ); - return false; - } - - gtag('event', mapGA4EcommerceEventName(event), ga4CommerceEventParameters); - - return true; -} - -function logPromotionEvent(event) { - try { - var ga4CommerceEventParameters; - event.PromotionAction.PromotionList.forEach(function (promotion) { - ga4CommerceEventParameters = buildPromotion(promotion); - gtag( - 'event', - mapGA4EcommerceEventName(event), - ga4CommerceEventParameters - ); - }); - return true; - } catch (error) { - console.error( - 'Error logging Promotions to GA4. Promotions not logged.', - error - ); - } - return false; -} - -function logImpressionEvent(event, affiliation) { - try { - var ga4CommerceEventParameters; - event.ProductImpressions.forEach(function (impression) { - ga4CommerceEventParameters = parseImpression( - impression, - affiliation - ); - - gtag( - 'event', - mapGA4EcommerceEventName(event), - ga4CommerceEventParameters - ); - }); - } catch (error) { - console.log( - 'Error logging Impressions to GA4. Impressions not logged', - error - ); - return false; - } - return true; -} - -function logViewCart(event, affiliation) { - var ga4CommerceEventParameters = buildViewCart(event, affiliation); - ga4CommerceEventParameters = self.common.mergeObjects( - ga4CommerceEventParameters, - self.common.limitEventAttributes(event.EventAttributes) - ); - ga4CommerceEventParameters.currency = event.CurrencyCode; - - ga4CommerceEventParameters.value = - (event.CustomFlags && event.CustomFlags['GA4.Value']) || - event.ProductAction.TotalAmount || - null; - gtag('event', mapGA4EcommerceEventName(event), ga4CommerceEventParameters); - return true; -} - function buildViewCart(event, affiliation) { return { items: buildProductsList(event.ProductAction.ProductList, affiliation), diff --git a/packages/GA4Client/src/event-handler.js b/packages/GA4Client/src/event-handler.js index 9c875b5..2a47939 100644 --- a/packages/GA4Client/src/event-handler.js +++ b/packages/GA4Client/src/event-handler.js @@ -45,6 +45,11 @@ EventHandler.prototype.sendEventToGA4 = function (eventName, eventAttributes) { standardizedAttributes ); + if (this.common.forwarderSettings.measurementId) { + standardizedAttributes.send_to = + this.common.forwarderSettings.measurementId; + } + gtag( 'event', this.common.truncateEventName(standardizedEventName), diff --git a/packages/GA4Client/test/src/tests.js b/packages/GA4Client/test/src/tests.js index 0bcf74b..c6f8b6d 100644 --- a/packages/GA4Client/test/src/tests.js +++ b/packages/GA4Client/test/src/tests.js @@ -325,6 +325,7 @@ describe('Google Analytics 4 Event', function () { total_amount: 999, }, ], + send_to: 'testMeasurementId', }, ]; @@ -825,6 +826,7 @@ describe('Google Analytics 4 Event', function () { total_amount: 999, }, ], + send_to: 'testMeasurementId', }, ]; @@ -924,6 +926,7 @@ describe('Google Analytics 4 Event', function () { promotion_name: 'Summer Sale Banner', creative_name: 'Summer Sale', creative_slot: 'featured_app_1', + send_to: 'testMeasurementId', }, ]; @@ -935,9 +938,9 @@ describe('Google Analytics 4 Event', function () { promotion_name: 'Winter Sale Banner', creative_name: 'Winter Sale', creative_slot: 'featured_app_2', + send_to: 'testMeasurementId', }, ]; - window.dataLayer[0].should.eql(promotionResult1); window.dataLayer[1].should.eql(promotionResult2); @@ -977,6 +980,7 @@ describe('Google Analytics 4 Event', function () { promotion_name: 'Summer Sale Banner', creative_name: 'Summer Sale', creative_slot: 'featured_app_1', + send_to: 'testMeasurementId', }, ]; @@ -988,6 +992,7 @@ describe('Google Analytics 4 Event', function () { promotion_name: 'Winter Sale Banner', creative_name: 'Winter Sale', creative_slot: 'featured_app_2', + send_to: 'testMeasurementId', }, ]; @@ -1086,6 +1091,7 @@ describe('Google Analytics 4 Event', function () { total_amount: 999, }, ], + send_to: 'testMeasurementId', }, ]; @@ -1183,6 +1189,7 @@ describe('Google Analytics 4 Event', function () { total_amount: 999, }, ], + send_to: 'testMeasurementId', }, ]; @@ -1242,6 +1249,7 @@ describe('Google Analytics 4 Event', function () { shipping_tier: null, coupon: null, items: [], + send_to: 'testMeasurementId', }, ]; window.dataLayer[0].should.eql(result); @@ -1271,6 +1279,7 @@ describe('Google Analytics 4 Event', function () { payment_type: null, coupon: null, items: [], + send_to: 'testMeasurementId', }, ]; window.dataLayer[0].should.eql(result); @@ -1363,6 +1372,7 @@ describe('Google Analytics 4 Event', function () { total_amount: 999, }, ], + send_to: 'testMeasurementId', }, ]; @@ -1458,7 +1468,7 @@ describe('Google Analytics 4 Event', function () { EventAttributes: {}, }); - var result = ['event', 'Unmapped Event Name', {}]; + var result = ['event', 'Unmapped Event Name', { send_to: 'testMeasurementId', }]; window.dataLayer[0].should.eql(result); done(); @@ -1472,7 +1482,7 @@ describe('Google Analytics 4 Event', function () { EventAttributes: null, }); - var result = ['event', 'Unmapped Event Name', {}]; + var result = ['event', 'Unmapped Event Name', { send_to: 'testMeasurementId', }]; window.dataLayer[0].should.eql(result); done(); @@ -1488,7 +1498,7 @@ describe('Google Analytics 4 Event', function () { }, }); - var result = ['event', 'Unmapped Event Name', { foo: 'bar' }]; + var result = ['event', 'Unmapped Event Name', { foo: 'bar', send_to: 'testMeasurementId', }]; window.dataLayer[0].should.eql(result); done(); @@ -1512,6 +1522,7 @@ describe('Google Analytics 4 Event', function () { { page_title: 'Mocha Tests', page_location: location.href, + send_to: 'testMeasurementId', }, ]; window.dataLayer[0].should.eql(result); @@ -1541,6 +1552,7 @@ describe('Google Analytics 4 Event', function () { page_location: '/foo', eventKey1: 'test1', eventKey2: 'test2', + send_to: 'testMeasurementId', }, ]; window.dataLayer[0].should.eql(result); @@ -1568,6 +1580,7 @@ describe('Google Analytics 4 Event', function () { foo: 'bar', superLongEventAttributeNameThatShouldBeT: 'Super Long Event Attribute value that should be truncated because we do not want super long attribut', + send_to: 'testMeasurementId', }; window.dataLayer[0][1].should.eql(expectedEventName); @@ -1605,6 +1618,7 @@ describe('Google Analytics 4 Event', function () { var expectedEventAttributes = { foo: 'bar', '1?test4ever!!!': 'tester', + send_to: 'testMeasurementId', }; window.dataLayer[0][1].should.eql(expectedEventName); @@ -1630,6 +1644,7 @@ describe('Google Analytics 4 Event', function () { { page_title: 'Foo Page Title', page_location: '/foo', + send_to: 'testMeasurementId', }, ]; @@ -1654,6 +1669,7 @@ describe('Google Analytics 4 Event', function () { { page_title: 'Foo Page Title', page_location: '/foo', + send_to: 'testMeasurementId', }, ]; window.dataLayer[0].should.eql(result); @@ -1764,7 +1780,7 @@ describe('Google Analytics 4 Event', function () { 'dt', 'du', 'dv', - 'dw', + 'dw' ]; it('should limit the number of event attribute keys', function (done) { @@ -1782,6 +1798,13 @@ describe('Google Analytics 4 Event', function () { mParticle.forwarder.process(event); var resultEventAttributeKeys = Object.keys(dataLayer[0][2]); + // confirm measurmentId as part of GA4 parameters + resultEventAttributeKeys.includes('send_to').should.equal(true); + // remove send_to to test the 100 event attribute limit since send_to is a reserved GA4 param + delete (dataLayer[0][2]).send_to + + // re-assign resultEventAttributeKeys after removing send_to from batch to only count for non-reserved params + resultEventAttributeKeys = Object.keys(dataLayer[0][2]); resultEventAttributeKeys.length.should.eql(100); // dw is the 101st item. The limit is 100, so resultEventAttributeKeys.should.not.have.property('dw'); @@ -1812,6 +1835,13 @@ describe('Google Analytics 4 Event', function () { }); mParticle.forwarder.process(event); var resultEventAttributeKeys = Object.keys(dataLayer[0][2]); + // confirm measurmentId as part of GA4 parameters + resultEventAttributeKeys.includes('send_to').should.equal(true); + // remove send_to to test the 100 event attribute limit since send_to is a reserved GA4 param + delete (dataLayer[0][2]).send_to + + // re-assign resultEventAttributeKeys after removing send_to from batch to only count for non-reserved params + resultEventAttributeKeys = Object.keys(dataLayer[0][2]); // confirm event attribuets have been successfully set resultEventAttributeKeys.includes('aa').should.equal(true); // dw is the 101st item. The limit is 100, so @@ -1841,6 +1871,13 @@ describe('Google Analytics 4 Event', function () { mParticle.forwarder.process(event); var resultEventAttributeKeys = Object.keys(dataLayer[0][2]); + // confirm measurmentId as part of GA4 parameters + resultEventAttributeKeys.includes('send_to').should.equal(true); + // remove send_to to test the 100 event attribute limit since send_to is a reserved GA4 param + delete (dataLayer[0][2]).send_to + + // re-assign resultEventAttributeKeys after removing send_to from batch to only count for non-reserved params + resultEventAttributeKeys = Object.keys(dataLayer[0][2]); // confirm event attribuets have been successfully set resultEventAttributeKeys.includes('aa').should.equal(true); // dw is the 101st item. The limit is 100, so @@ -2032,6 +2069,7 @@ describe('Google Analytics 4 Event', function () { var expectedEventAttributes = { foo: 'bar', test4ever___: 'tester', + send_to: 'testMeasurementId', }; window.dataLayer[0][1].should.eql(expectedEventName); @@ -2060,6 +2098,7 @@ describe('Google Analytics 4 Event', function () { var expectedEventAttributes = { fo: 'bar', test4ever__: 'tester', + send_to: 'testMeasurementId', }; window.dataLayer[0][1].should.eql(expectedEventName); @@ -2193,6 +2232,7 @@ describe('Google Analytics 4 Event', function () { }, ], currency: 'USD', + send_to: 'testMeasurementId', }, ]; @@ -2294,6 +2334,7 @@ describe('Google Analytics 4 Event', function () { total_amount: 999, }, ], + send_to: 'testMeasurementId' }, ];